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