001package jmri.jmrit.vsdecoder.swing;
002
003import java.awt.Dimension;
004import java.awt.event.ActionEvent;
005import java.awt.event.ActionListener;
006import java.awt.event.KeyEvent;
007import java.beans.PropertyChangeEvent;
008import java.beans.PropertyChangeListener;
009import java.util.ArrayList;
010import java.util.HashMap;
011import java.util.List;
012import java.util.Map;
013
014import javax.swing.BoxLayout;
015import javax.swing.JButton;
016import javax.swing.JLabel;
017import javax.swing.JMenu;
018import javax.swing.JMenuBar;
019import javax.swing.JPanel;
020import javax.swing.JSlider;
021import javax.swing.JToggleButton;
022import javax.swing.event.ChangeEvent;
023import javax.swing.event.ChangeListener;
024
025import jmri.jmrit.roster.Roster;
026import jmri.jmrit.roster.RosterEntry;
027import jmri.jmrit.vsdecoder.LoadVSDFileAction;
028import jmri.jmrit.vsdecoder.SoundEvent;
029import jmri.jmrit.vsdecoder.VSDConfig;
030import jmri.jmrit.vsdecoder.VSDecoder;
031import jmri.jmrit.vsdecoder.VSDecoderManager;
032import jmri.util.JmriJFrame;
033import jmri.util.swing.JmriJOptionPane;
034
035/**
036 * Main frame for the GUI VSDecoder Manager.
037 *
038 * <hr>
039 * This file is part of JMRI.
040 * <p>
041 * JMRI is free software; you can redistribute it and/or modify it under
042 * the terms of version 2 of the GNU General Public License as published
043 * by the Free Software Foundation. See the "COPYING" file for a copy
044 * of this license.
045 * <p>
046 * JMRI is distributed in the hope that it will be useful, but WITHOUT
047 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
048 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
049 * for more details.
050 *
051 * @author Mark Underwood Copyright (C) 2011
052 */
053public class VSDManagerFrame extends JmriJFrame {
054
055    public static final String MUTE = "VSDMF:Mute"; // NOI18N
056    public static final String VOLUME_CHANGE = "VSDMF:VolumeChange"; // NOI18N
057    public static final String REMOVE_DECODER = "VSDMF:RemoveDecoder"; // NOI18N
058    public static final String CLOSE_WINDOW = "VSDMF:CloseWindow"; // NOI18N
059
060    // Map of Mnemonic KeyEvent values to GUI Components
061    private static final Map<String, Integer> Mnemonics = new HashMap<>();
062
063    static {
064        // Menu
065        Mnemonics.put("FileMenu", KeyEvent.VK_F);
066        Mnemonics.put("EditMenu", KeyEvent.VK_E);
067        // Other GUI
068        Mnemonics.put("MuteButton", KeyEvent.VK_M);
069        Mnemonics.put("AddButton", KeyEvent.VK_A);
070    }
071
072    private int master_volume;
073
074    JPanel decoderPane;
075    JPanel volumePane;
076    JPanel decoderBlank;
077
078    private VSDConfig config;
079    private VSDConfigDialog cd;
080    private List<JMenu> menuList;
081    private boolean is_auto_loading;
082    private boolean is_viewing;
083    private List<VSDecoder> vsdlist;
084
085    /**
086     * Constructor
087     */
088    public VSDManagerFrame() {
089        super(true, true);
090        this.addPropertyChangeListener(VSDecoderManager.instance());
091        is_auto_loading = VSDecoderManager.instance().getVSDecoderPreferences().isAutoLoadingVSDFile();
092        is_viewing = VSDecoderManager.instance().getVSDecoderList().isEmpty() ? false : true;
093        initGUI();
094    }
095
096    @Override
097    public void initComponents() {
098        //this.initGUI();
099    }
100
101    /**
102     * Build the GUI components
103     */
104    private void initGUI() {
105        log.debug("initGUI");
106        this.setTitle(Bundle.getMessage("VSDManagerFrameTitle"));
107        this.buildMenu();
108        this.setLayout(new BoxLayout(this.getContentPane(), BoxLayout.PAGE_AXIS));
109
110        decoderPane = new JPanel();
111        decoderPane.setLayout(new BoxLayout(decoderPane, BoxLayout.PAGE_AXIS));
112        decoderBlank = VSDControl.generateBlank();
113        decoderPane.add(decoderBlank);
114
115        volumePane = new JPanel();
116        volumePane.setLayout(new BoxLayout(volumePane, BoxLayout.LINE_AXIS));
117        JToggleButton muteButton = new JToggleButton(Bundle.getMessage("MuteButtonLabel"));
118        JButton addButton = new JButton(Bundle.getMessage("AddButtonLabel"));
119        final JSlider volume = new JSlider(0, 100);
120        volume.setMinorTickSpacing(10);
121        volume.setPaintTicks(true);
122        master_volume = VSDecoderManager.instance().getMasterVolume();
123        volume.setValue(master_volume);
124        volume.setPreferredSize(new Dimension(200, 20));
125        volume.setToolTipText(Bundle.getMessage("MgrVolumeToolTip"));
126        volume.addChangeListener(new ChangeListener() {
127            @Override
128            public void stateChanged(ChangeEvent e) {
129                volumeChange(e); // slider in real time
130            }
131        });
132        volumePane.add(new JLabel(Bundle.getMessage("VolumePaneLabel")));
133        volumePane.add(volume);
134        volumePane.add(muteButton);
135        muteButton.setToolTipText(Bundle.getMessage("MgrMuteToolTip"));
136        muteButton.setMnemonic(Mnemonics.get("MuteButton"));
137        muteButton.addActionListener(new ActionListener() {
138            @Override
139            public void actionPerformed(ActionEvent e) {
140                muteButtonPressed(e);
141            }
142        });
143        volumePane.add(addButton);
144        addButton.setToolTipText(Bundle.getMessage("MgrAddButtonToolTip"));
145        addButton.setMnemonic(Mnemonics.get("AddButton"));
146        addButton.addActionListener(new ActionListener() {
147            @Override
148            public void actionPerformed(ActionEvent e) {
149                addButtonPressed(e);
150            }
151        });
152
153        this.add(decoderPane);
154        this.add(volumePane);
155
156        addWindowListener(new java.awt.event.WindowAdapter() {
157            @Override
158            public void windowClosing(java.awt.event.WindowEvent e) {
159                firePropertyChange(CLOSE_WINDOW, null, null);
160            }
161        });
162
163        log.debug("pane size + {}", decoderPane.getPreferredSize());
164        this.pack();
165        this.setVisible(true);
166
167        log.debug("done...");
168
169        // first, check Viewing Mode
170        if (is_viewing) {
171            vsdlist = new ArrayList<>(); // List of VSDecoders with uncomplete configuration (no Roster Entry reference)
172            for (VSDecoder vsd : VSDecoderManager.instance().getVSDecoderList()) {
173                if (vsd.getRosterEntry() != null) {
174                    // VSDecoder configuration is complete and will be listed
175                    addButton.doClick(); // simulate an Add-button-click
176                    cd.setRosterItem(vsd.getRosterEntry()); // forward the roster entry
177                } else {
178                    vsdlist.add(vsd); // VSDecoder with uncomplete configuration
179                }
180            }
181            // delete VSDecoder(s) with uncomplete configuration
182            for (VSDecoder v : vsdlist) {
183                VSDecoderManager.instance().deleteDecoder(v.getAddress().toString());
184            }
185            // change back to Edit mode
186            is_viewing = false;
187        } else if (is_auto_loading) {
188            // Auto-Load
189            log.info("Auto-Loading VSDecoder");
190            String vsdRosterGroup = "VSD";
191            String msg = "";
192            if (Roster.getDefault().getRosterGroupList().contains(vsdRosterGroup)) {
193                List<RosterEntry> rosterList;
194                rosterList = Roster.getDefault().getEntriesInGroup(vsdRosterGroup);
195                if (!rosterList.isEmpty()) {
196                    // Allow <max_decoder> roster entries
197                    int entry_counter = 1;
198                    for (RosterEntry entry : rosterList) {
199                        if (entry_counter <= VSDecoderManager.max_decoder) {
200                            addButton.doClick(); // simulate an Add-button-click
201                            cd.setRosterItem(entry); // forward the roster entry
202                            entry_counter++;
203                        } else {
204                            msg = "Only " + VSDecoderManager.max_decoder + " Roster Entries allowed. Discarded "
205                                    + (rosterList.size() - VSDecoderManager.max_decoder);
206                        }
207                    }
208                } else {
209                    msg = "No Roster Entry found in Roster Group " + vsdRosterGroup;
210                }
211            } else {
212                msg = "Roster Group \"" + vsdRosterGroup + "\" not found";
213            }
214            if (!msg.isEmpty()) {
215                JmriJOptionPane.showMessageDialog(null, "Auto-Loading: " + msg);
216                log.warn("Auto-Loading VSDecoder aborted");
217            }
218        }
219    }
220
221    /**
222     * Handle "Mute" button press.
223     * @param e Event that kicked this off.
224     */
225    protected void muteButtonPressed(ActionEvent e) {
226        JToggleButton b = (JToggleButton) e.getSource();
227        log.debug("Mute button pressed. value: {}", b.isSelected());
228        firePropertyChange(MUTE, !b.isSelected(), b.isSelected());
229    }
230
231    /**
232     * Handle "Add" button press
233     * @param e Event that fired this change
234     */
235    protected void addButtonPressed(ActionEvent e) {
236        log.debug("Add button pressed");
237
238        // If the maximum number of VSDecoders (Controls) is reached, don't create a new Control
239        // In Viewing Mode up to 4 existing VSDecoders are possible, so skip the check
240        if (! is_viewing && VSDecoderManager.instance().getVSDecoderList().size() >= VSDecoderManager.max_decoder) {
241            JmriJOptionPane.showMessageDialog(null,
242                    "VSDecoder cannot be created. Maximal number is " + String.valueOf(VSDecoderManager.max_decoder));
243        } else {
244            config = new VSDConfig(); // Create a new Config for the new VSDecoder.
245            // Do something here.  Create a new VSDecoder and add it to the window.
246            cd = new VSDConfigDialog(decoderPane, Bundle.getMessage("NewDecoderConfigPaneTitle"), config, is_auto_loading, is_viewing);
247            cd.addPropertyChangeListener(new PropertyChangeListener() {
248                @Override
249                public void propertyChange(PropertyChangeEvent event) {
250                    log.debug("property change name {}, old: {}, new: {}", event.getPropertyName(),
251                            event.getOldValue(), event.getNewValue());
252                    addButtonPropertyChange(event);
253                }
254            });
255        }
256    }
257
258    /**
259     * Callback for the Config Dialog
260     * @param event Event that fired this change
261     */
262    protected void addButtonPropertyChange(PropertyChangeEvent event) {
263        log.debug("internal config dialog handler");
264        // If this decoder already exists, don't create a new Control
265        // In Viewing Mode up to 4 existing VSDecoders are possible, so skip the check
266        if (! is_viewing && VSDecoderManager.instance().getVSDecoderByAddress(config.getLocoAddress().toString()) != null) {
267            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("MgrAddDuplicateMessage"));
268        } else {
269            VSDecoder newDecoder = VSDecoderManager.instance().getVSDecoder(config);
270            if (newDecoder == null) {
271                log.error("Lost context, VSDecoder is null. Quit JMRI and start over. No New Decoder constructed! Address: {}, profile: {}",
272                        config.getLocoAddress(), config.getProfileName());
273                return;
274            }
275            VSDControl newControl = new VSDControl(config);
276            // Set the Decoder to listen to PropertyChanges from the control
277            newControl.addPropertyChangeListener(newDecoder);
278            this.addPropertyChangeListener(newDecoder);
279            // Set US to listen to PropertyChanges from the control (mainly for DELETE)
280            newControl.addPropertyChangeListener(new PropertyChangeListener() {
281                @Override
282                public void propertyChange(PropertyChangeEvent event) {
283                    log.debug("property change name {}, old: {}, new: {}",
284                            event.getPropertyName(), event.getOldValue(), event.getNewValue());
285                    vsdControlPropertyChange(event);
286                }
287            });
288            if (decoderPane.isAncestorOf(decoderBlank)) {
289                decoderPane.remove(decoderBlank);
290            }
291
292            decoderPane.add(newControl);
293            newControl.addSoundButtons(new ArrayList<SoundEvent>(newDecoder.getEventList()));
294
295            firePropertyChange(VOLUME_CHANGE, master_volume, null);
296            log.debug("Master volume set to {}", master_volume);
297
298            decoderPane.revalidate();
299            decoderPane.repaint();
300
301            this.pack();
302            //this.setVisible(true);
303            // Do we need to make newControl a listener to newDecoder?
304        }
305    }
306
307    /**
308     * Handle property change event from one of the VSDControls
309     * @param event Event that fired this change
310     */
311    protected void vsdControlPropertyChange(PropertyChangeEvent event) {
312        String property = event.getPropertyName();
313        if (property.equals(VSDControl.DELETE)) {
314            String ov = (String) event.getOldValue();
315            log.debug("vsdControlPropertyChange. ID: {}, old: {}", VSDControl.DELETE, ov);
316            VSDecoder vsd = VSDecoderManager.instance().getVSDecoderByAddress(ov);
317            if (vsd == null) {
318                log.warn("Lost context, VSDecoder is null. Quit JMRI and start over.");
319                return;
320            }
321            if (vsd.getEngineSound().isEngineStarted()) {
322                JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("MgrDeleteWhenEngineStopped"));
323                return;
324            } else {
325                this.removePropertyChangeListener(vsd);
326                log.debug("vsdControlPropertyChange. ID: {}, old: {}", REMOVE_DECODER, ov);
327                firePropertyChange(REMOVE_DECODER, ov, null);
328                decoderPane.remove((VSDControl) event.getSource());
329                if (decoderPane.getComponentCount() == 0) {
330                    decoderPane.add(decoderBlank);
331                }
332                //debugPrintDecoderList();
333                decoderPane.revalidate();
334                decoderPane.repaint();
335
336                this.pack();
337            }
338        }
339    }
340
341    /**
342     * Handle master volume slider change
343     * @param event Event that fired this change
344     */
345    protected void volumeChange(ChangeEvent event) {
346        JSlider v = (JSlider) event.getSource();
347        log.debug("Volume slider moved. value: {}", v.getValue());
348        master_volume = v.getValue();
349        firePropertyChange(VOLUME_CHANGE, master_volume, null);
350        // todo? do you want to save?
351        if (VSDecoderManager.instance().getMasterVolume() != v.getValue()) {
352            VSDecoderManager.instance().setMasterVolume(v.getValue());
353            VSDecoderManager.instance().getVSDecoderPreferences().save();
354            log.debug("VSD Preferences saved");
355        }
356    }
357
358    private void buildMenu() {
359        JMenu fileMenu = new JMenu(Bundle.getMessage("MenuFile")); // uses NamedBeanBundle
360        fileMenu.setMnemonic(Mnemonics.get("FileMenu")); // OK to use this different key name for Mnemonics
361
362        fileMenu.add(new LoadVSDFileAction(Bundle.getMessage("VSDecoderFileMenuLoadVSDFile")));
363
364        JMenu editMenu = new JMenu(Bundle.getMessage("MenuEdit"));
365        editMenu.setMnemonic(Mnemonics.get("EditMenu")); // OK to use this different key name for Mnemonics
366        editMenu.add(new VSDPreferencesAction(Bundle.getMessage("VSDecoderFileMenuPreferences")));
367
368        menuList = new ArrayList<>(2);
369
370        menuList.add(fileMenu);
371        menuList.add(editMenu);
372
373        this.setJMenuBar(new JMenuBar());
374
375        this.getJMenuBar().add(fileMenu);
376        this.getJMenuBar().add(editMenu);
377
378        this.addHelpMenu("package.jmri.jmrit.vsdecoder.swing.VSDManagerFrame", true);
379    }
380
381    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(VSDManagerFrame.class);
382
383}