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