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-2025
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    private JPanel decoderPane;
077    private JPanel volumePane;
078    private JPanel decoderBlank;
079    private JButton addButton;
080
081    private VSDConfig config;
082    private VSDConfigDialog cd;
083    private List<JMenu> menuList;
084    private boolean is_auto_loading;
085    private boolean is_block_using;
086    private boolean is_viewing;
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        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        jmri.util.ThreadingUtil.runOnGUI(() -> {
172            if (is_viewing) {
173                this.runViewing();
174            } else if (is_auto_loading) {
175                this.runAutoLoad();
176            }
177        });
178    }
179
180    private void runViewing() {
181        log.debug("Viewing mode");
182        RosterEntry roster;
183        for (VSDecoder vsd : VSDecoderManager.instance().getVSDecoderList()) {
184            if (vsd.getRosterEntry() != null) {
185                // take the existing Roster entry; all is set
186                roster = vsd.getRosterEntry();
187            } else {
188                // take a Roster entry temporarily to trigger the process
189                roster = new RosterEntry(vsd.getAddress().toString());
190                roster.setId(vsd.getId());
191                roster.setDccAddress(String.valueOf(vsd.getAddress().getNumber()));
192                roster.putAttribute("VSDecoder_Path", vsd.getVSDFilePath());
193                roster.putAttribute("VSDecoder_Profile", vsd.getProfileName());
194                roster.putAttribute("VSDecoder_LaunchThrottle", "no");
195            }
196            addButton.doClick(); // simulate an Add-button-click
197            cd.setRosterItem(roster); // forward the roster entry
198        }
199        // change back to Edit mode
200        is_viewing = false;
201    }
202
203    private void runAutoLoad() {
204        log.debug("Auto-Loading VSDecoder");
205        String vsdRosterGroup = "VSD";
206        String msg = "";
207        if (Roster.getDefault().getRosterGroupList().contains(vsdRosterGroup)) {
208            List<RosterEntry> rosterList;
209            rosterList = Roster.getDefault().getEntriesInGroup(vsdRosterGroup);
210            if (!rosterList.isEmpty()) {
211                // Allow <max_decoder> roster entries
212                int entry_counter = 1;
213                for (RosterEntry entry : rosterList) {
214                    if (entry_counter <= VSDecoderManager.max_decoder) {
215                        addButton.doClick(); // simulate an Add-button-click
216                        cd.setRosterItem(entry); // forward the roster entry
217                        entry_counter++;
218                    } else {
219                        msg = "Only " + VSDecoderManager.max_decoder + " Roster Entries allowed. Discarded "
220                                + (rosterList.size() - VSDecoderManager.max_decoder);
221                    }
222                }
223            } else {
224                msg = "No Roster Entry found in Roster Group " + vsdRosterGroup;
225            }
226        } else {
227            msg = "Roster Group \"" + vsdRosterGroup + "\" not found";
228        }
229        if (!msg.isEmpty()) {
230            JmriJOptionPane.showMessageDialog(null, "Auto-Loading: " + msg);
231            log.warn("Auto-Loading VSDecoder aborted");
232        }
233    }
234
235    /**
236     * Handle "Mute" button press.
237     * @param e Event that kicked this off.
238     */
239    protected void muteButtonPressed(ActionEvent e) {
240        JToggleButton b = (JToggleButton) e.getSource();
241        log.debug("Mute button pressed. value: {}", b.isSelected());
242        firePropertyChange(MUTE, !b.isSelected(), b.isSelected());
243    }
244
245    /**
246     * Handle "Add" button press
247     * @param e Event that fired this change
248     */
249    protected void addButtonPressed(ActionEvent e) {
250        log.debug("Add button pressed");
251        // If the maximum number of VSDecoders (Controls) is reached, don't create a new Control
252        // In Viewing Mode up to <max_decoder> existing VSDecoders are possible, so skip the check
253        if (! is_viewing && VSDecoderManager.instance().getVSDecoderList().size() >= VSDecoderManager.max_decoder) {
254            JmriJOptionPane.showMessageDialog(null,
255                    "VSDecoder cannot be created. Maximal number is " + String.valueOf(VSDecoderManager.max_decoder));
256        } else if (jmri.InstanceManager.getDefault(jmri.AudioManager.class).
257                getNamedBeanSet(jmri.Audio.BUFFER).size() == jmri.AudioManager.MAX_BUFFERS) {
258            JmriJOptionPane.showMessageDialog(null, "Decoder cannot be created! No more free buffers.");
259        } else {
260            config = new VSDConfig(); // Create a new Config for the new VSDecoder.
261            // Do something here.  Create a new VSDecoder and add it to the window.
262            cd = new VSDConfigDialog(decoderPane, Bundle.getMessage("NewDecoderConfigPaneTitle"),
263                    config, is_auto_loading, is_viewing);
264            cd.addPropertyChangeListener(new PropertyChangeListener() {
265                @Override
266                public void propertyChange(PropertyChangeEvent event) {
267                    log.debug("property change name {}, old: {}, new: {}", event.getPropertyName(),
268                            event.getOldValue(), event.getNewValue());
269                    addButtonPropertyChange(event);
270                }
271            });
272        }
273    }
274
275    /**
276     * Callback for the Config Dialog
277     * @param event Event that fired this change
278     */
279    protected void addButtonPropertyChange(PropertyChangeEvent event) {
280        log.debug("internal config dialog handler");
281        // If this decoder already exists, don't create a new Control
282        // In Viewing Mode up to <max_decoder> existing VSDecoders are allowed, so skip the check
283        if (! is_viewing && VSDecoderManager.instance().getVSDecoderByAddress(config.getLocoAddress().toString()) != null) {
284            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("MgrAddDuplicateMessage"));
285        } else {
286            VSDecoder newDecoder = VSDecoderManager.instance().getVSDecoder(config);
287            if (newDecoder != null) {
288                VSDControl newControl = new VSDControl(config);
289                // Set the Decoder to listen to PropertyChanges from the control
290                newControl.addPropertyChangeListener(newDecoder);
291                this.addPropertyChangeListener(newDecoder);
292                // Set US to listen to PropertyChanges from the control (mainly for DELETE)
293                newControl.addPropertyChangeListener(new PropertyChangeListener() {
294                    @Override
295                    public void propertyChange(PropertyChangeEvent event) {
296                        log.debug("property change name {}, old: {}, new: {}",
297                                event.getPropertyName(), event.getOldValue(), event.getNewValue());
298                        vsdControlPropertyChange(event);
299                    }
300                });
301                if (decoderPane.isAncestorOf(decoderBlank)) {
302                    decoderPane.remove(decoderBlank);
303                }
304
305                decoderPane.add(newControl);
306                newControl.addSoundButtons(new ArrayList<SoundEvent>(newDecoder.getEventList()));
307
308                firePropertyChange(VOLUME_CHANGE, master_volume, null);
309                log.debug("Master volume set to {}", master_volume);
310
311                decoderPane.revalidate();
312                decoderPane.repaint();
313
314                this.pack();
315                //this.setVisible(true);
316                // Do we need to make newControl a listener to newDecoder?
317
318                if (is_viewing) {
319                    VSDecoderManager.instance().doResume();
320                } else {
321                    getStartBlock(newDecoder);
322                }
323            }
324        }
325    }
326
327    private void getStartBlock(VSDecoder vsd) {
328        jmri.Block start_block = null;
329        for (jmri.Block blk : jmri.InstanceManager.getDefault(jmri.BlockManager.class).getNamedBeanSet()) {
330            if (VSDecoderManager.instance().checkForPossibleStartblock(blk)) {
331                int locoAddress = VSDecoderManager.instance().getLocoAddr(blk);
332                if (locoAddress == vsd.getAddress().getNumber()) {
333                    log.debug("found start block: {}, loco address: {}", blk, locoAddress);
334                    Sensor s = blk.getSensor();
335                    if (s != null && is_block_using) {
336                        if (s.getKnownState() == Sensor.UNKNOWN) {
337                            try {
338                                s.setState(Sensor.ACTIVE);
339                            } catch (jmri.JmriException ex) {
340                                log.debug("Exception setting sensor");
341                            }
342                        }
343                    }
344                    start_block = blk;
345                    break; // one loco address per block
346                }
347            }
348        }
349        if (start_block != null) {
350            VSDecoderManager.instance().atStart(start_block);
351        }
352    }
353
354    /**
355     * Handle property change event from one of the VSDControls
356     * @param event Event that fired this change
357     */
358    protected void vsdControlPropertyChange(PropertyChangeEvent event) {
359        String property = event.getPropertyName();
360        if (property.equals(VSDControl.DELETE)) {
361            String ov = (String) event.getOldValue();
362            log.debug("vsdControlPropertyChange. ID: {}, old: {}", VSDControl.DELETE, ov);
363            VSDecoder vsd = VSDecoderManager.instance().getVSDecoderByAddress(ov);
364            if (vsd == null) {
365                log.warn("Lost context, VSDecoder is null. Quit JMRI and start over.");
366                return;
367            }
368            if (vsd.getEngineSound().isEngineStarted()) {
369                JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("MgrDeleteWhenEngineStopped"));
370                return;
371            } else {
372                this.removePropertyChangeListener(vsd);
373                log.debug("vsdControlPropertyChange. ID: {}, old: {}", REMOVE_DECODER, ov);
374                firePropertyChange(REMOVE_DECODER, ov, null);
375                decoderPane.remove((VSDControl) event.getSource());
376                if (decoderPane.getComponentCount() == 0) {
377                    decoderPane.add(decoderBlank);
378                }
379                decoderPane.revalidate();
380                decoderPane.repaint();
381
382                this.pack();
383            }
384        }
385    }
386
387    /**
388     * Handle master volume slider change
389     * @param event Event that fired this change
390     */
391    protected void volumeChange(ChangeEvent event) {
392        JSlider v = (JSlider) event.getSource();
393        log.debug("Volume slider moved. value: {}", v.getValue());
394        master_volume = v.getValue();
395        firePropertyChange(VOLUME_CHANGE, master_volume, null);
396        // todo? do you want to save?
397        if (VSDecoderManager.instance().getMasterVolume() != v.getValue()) {
398            VSDecoderManager.instance().setMasterVolume(v.getValue());
399            VSDecoderManager.instance().getVSDecoderPreferences().save();
400            log.debug("VSD Preferences saved");
401        }
402    }
403
404    private void buildMenu() {
405        JMenu fileMenu = new JMenu(Bundle.getMessage("MenuFile")); // uses NamedBeanBundle
406        fileMenu.setMnemonic(Mnemonics.get("FileMenu")); // OK to use this different key name for Mnemonics
407
408        fileMenu.add(new LoadVSDFileAction(Bundle.getMessage("VSDecoderFileMenuLoadVSDFile")));
409
410        JMenu editMenu = new JMenu(Bundle.getMessage("MenuEdit"));
411        editMenu.setMnemonic(Mnemonics.get("EditMenu")); // OK to use this different key name for Mnemonics
412        editMenu.add(new VSDPreferencesAction(Bundle.getMessage("VSDecoderFileMenuPreferences")));
413
414        menuList = new ArrayList<>(2);
415
416        menuList.add(fileMenu);
417        menuList.add(editMenu);
418
419        this.setJMenuBar(new JMenuBar());
420
421        this.getJMenuBar().add(fileMenu);
422        this.getJMenuBar().add(editMenu);
423
424        this.addHelpMenu("package.jmri.jmrit.vsdecoder.swing.VSDManagerFrame", true);
425    }
426
427    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(VSDManagerFrame.class);
428
429}