001package jmri.jmrit.vsdecoder.swing;
002
003import java.awt.event.ActionEvent;
004import java.awt.event.ActionListener;
005import java.awt.event.KeyEvent;
006import java.beans.PropertyChangeEvent;
007import java.beans.PropertyChangeListener;
008import java.text.MessageFormat;
009import java.util.ArrayList;
010import java.util.HashMap;
011import java.util.Iterator;
012import java.util.Map;
013
014import javax.swing.BorderFactory;
015import javax.swing.BoxLayout;
016import javax.swing.JButton;
017import javax.swing.JDialog;
018import javax.swing.JPanel;
019import javax.swing.JTabbedPane;
020import javax.swing.SwingUtilities;
021import javax.swing.border.TitledBorder;
022
023import jmri.InstanceManager;
024import jmri.jmrit.DccLocoAddressSelector;
025import jmri.jmrit.roster.Roster;
026import jmri.jmrit.roster.RosterEntry;
027import jmri.jmrit.roster.swing.RosterEntrySelectorPanel;
028import jmri.jmrit.vsdecoder.LoadVSDFileAction;
029import jmri.jmrit.vsdecoder.VSDConfig;
030import jmri.jmrit.vsdecoder.VSDManagerEvent;
031import jmri.jmrit.vsdecoder.VSDManagerListener;
032import jmri.jmrit.vsdecoder.VSDecoderManager;
033import jmri.util.swing.JmriJOptionPane;
034import jmri.jmrit.throttle.ThrottleControllerUI;
035
036/**
037 * Configuration dialog for setting up a new VSDecoder
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 */
054public class VSDConfigDialog extends JDialog {
055
056    private static final String CONFIG_PROPERTY = "Config";
057
058    // Map of Mnemonic KeyEvent values to GUI Components
059    private static final Map<String, Integer> Mnemonics = new HashMap<>();
060
061    static {
062        Mnemonics.put("RosterTab", KeyEvent.VK_R);
063        Mnemonics.put("ManualTab", KeyEvent.VK_M);
064        Mnemonics.put("AddressSet", KeyEvent.VK_T);
065        Mnemonics.put("ProfileLoad", KeyEvent.VK_L);
066        Mnemonics.put("RosterSave", KeyEvent.VK_S);
067        Mnemonics.put("CloseButton", KeyEvent.VK_O);
068        Mnemonics.put("CancelButton", KeyEvent.VK_C);
069    }
070
071    // GUI Elements
072    private javax.swing.JLabel addressLabel;
073    private javax.swing.JButton addressSetButton;
074    private DccLocoAddressSelector addressSelector;
075    private RosterEntrySelectorPanel rosterSelector;
076    private javax.swing.JLabel rosterLabel;
077    private javax.swing.JButton rosterSaveButton;
078    private javax.swing.JComboBox<Object> profileComboBox;
079    private javax.swing.JButton profileLoadButton;
080    private javax.swing.JPanel rosterPanel;
081    private javax.swing.JPanel profilePanel;
082    private javax.swing.JPanel addressPanel;
083    private javax.swing.JTabbedPane locoSelectPanel;
084    private javax.swing.JButton closeButton;
085
086    private NullProfileBoxItem loadProfilePrompt; // dummy profileComboBox entry
087    private VSDConfig config; // local reference to the config being constructed by this dialog
088    private RosterEntry rosterEntry; // local reference to the selected RosterEntry
089
090    private RosterEntry rosterEntrySelected;
091    private boolean is_auto_loading;
092    private boolean is_viewing;
093
094    /**
095     * Constructor
096     *
097     * @param parent Ancestor panel
098     * @param title  title for the dialog
099     * @param c      Config object to be set by the dialog
100     * @param ial    Is Auto Loading
101     * @param viewing Viewing mode flag
102     */
103    public VSDConfigDialog(JPanel parent, String title, VSDConfig c, boolean ial, boolean viewing) {
104        super(SwingUtilities.getWindowAncestor(parent), title);
105        config = c;
106        is_auto_loading = ial;
107        is_viewing = viewing;
108        VSDecoderManager.instance().addEventListener(new VSDManagerListener() {
109            @Override
110            public void eventAction(VSDManagerEvent evt) {
111                vsdecoderManagerEventAction(evt);
112            }
113        });
114        initComponents();
115        setLocationRelativeTo(parent);
116    }
117
118    /**
119     * Init the GUI components
120     */
121    protected void initComponents() {
122        this.setLayout(new BoxLayout(this.getContentPane(), BoxLayout.PAGE_AXIS));
123
124        // Tabbed pane for loco select (Roster or Manual)
125        locoSelectPanel = new JTabbedPane();
126        TitledBorder title = BorderFactory.createTitledBorder(BorderFactory.createLoweredBevelBorder(),
127                Bundle.getMessage("LocoTabbedPaneTitle"));
128        title.setTitlePosition(TitledBorder.DEFAULT_POSITION);
129        locoSelectPanel.setBorder(title);
130
131        // Roster Tab and Address Tab
132        rosterPanel = new JPanel();
133        rosterPanel.setLayout(new BoxLayout(rosterPanel, BoxLayout.LINE_AXIS));
134        addressPanel = new JPanel();
135        addressPanel.setLayout(new BoxLayout(addressPanel, BoxLayout.LINE_AXIS));
136        locoSelectPanel.addTab(Bundle.getMessage("RosterLabel"), rosterPanel); // tab name
137        locoSelectPanel.addTab(Bundle.getMessage("LocoTabbedPaneManualTab"), addressPanel);
138        //NOTE: There appears to be a bug in Swing that doesn't let Mnemonics work on a JTabbedPane when a sibling component
139        // has the focus.  Oh well.
140        try {
141            locoSelectPanel.setToolTipTextAt(locoSelectPanel.indexOfTab(Bundle.getMessage("RosterLabel")), Bundle.getMessage("LTPRosterTabToolTip"));
142            locoSelectPanel.setMnemonicAt(locoSelectPanel.indexOfTab(Bundle.getMessage("RosterLabel")), Mnemonics.get("RosterTab"));
143            locoSelectPanel.setToolTipTextAt(locoSelectPanel.indexOfTab(Bundle.getMessage("LocoTabbedPaneManualTab")), Bundle.getMessage("LTPManualTabToolTip"));
144            locoSelectPanel.setMnemonicAt(locoSelectPanel.indexOfTab(Bundle.getMessage("LocoTabbedPaneManualTab")), Mnemonics.get("ManualTab"));
145        } catch (IndexOutOfBoundsException iobe) {
146            log.debug("Index out of bounds setting up tabbed Pane", iobe);
147            // Ignore out-of-bounds exception.  We just won't have mnemonics or tool tips this go round
148        }
149        // Roster Tab components
150        rosterSelector = new RosterEntrySelectorPanel();
151        rosterSelector.setNonSelectedItem(Bundle.getMessage("EmptyRosterBox"));
152        rosterSelector.setToolTipText(Bundle.getMessage("LTPRosterSelectorToolTip"));
153        //rosterComboBox.setToolTipText("tool tip for roster box");
154        rosterSelector.addPropertyChangeListener("selectedRosterEntries", new PropertyChangeListener() {
155            @Override
156            public void propertyChange(PropertyChangeEvent pce) {
157                rosterItemSelectAction(null);
158            }
159        });
160        rosterPanel.add(rosterSelector);
161        rosterLabel = new javax.swing.JLabel();
162        rosterLabel.setText(Bundle.getMessage("RosterLabel"));
163
164        // Address Tab Components
165        addressLabel = new javax.swing.JLabel();
166        addressSelector = new DccLocoAddressSelector();
167        addressSelector.setToolTipText(Bundle.getMessage("LTPAddressSelectorToolTip", Bundle.getMessage("ButtonSet")));
168        addressSetButton = new javax.swing.JButton();
169        addressSetButton.setText(Bundle.getMessage("ButtonSet"));
170        addressSetButton.setEnabled(true);
171        addressSetButton.setToolTipText(Bundle.getMessage("AddressSetButtonToolTip"));
172        addressSetButton.setMnemonic(Mnemonics.get("AddressSet"));
173        addressSetButton.addActionListener(new java.awt.event.ActionListener() {
174            @Override
175            public void actionPerformed(java.awt.event.ActionEvent evt) {
176                addressSetButtonActionPerformed(evt);
177            }
178        });
179
180        addressPanel.add(addressSelector.getCombinedJPanel());
181        addressPanel.add(addressSetButton);
182        addressPanel.add(addressLabel);
183
184        // Profile select Pane
185        profilePanel = new JPanel();
186        profilePanel.setLayout(new BoxLayout(profilePanel, BoxLayout.PAGE_AXIS));
187        profileComboBox = new javax.swing.JComboBox<>();
188        profileComboBox.setToolTipText(Bundle.getMessage("ProfileComboBoxToolTip"));
189        profileLoadButton = new JButton(Bundle.getMessage("VSDecoderFileMenuLoadVSDFile"));
190        profileLoadButton.setToolTipText(Bundle.getMessage("ProfileLoadButtonToolTip"));
191        profileLoadButton.setMnemonic(Mnemonics.get("ProfileLoad"));
192        profileLoadButton.setEnabled(true);
193        TitledBorder title2 = BorderFactory.createTitledBorder(BorderFactory.createLoweredBevelBorder(),
194                Bundle.getMessage("ProfileSelectorPaneTitle"));
195        title.setTitlePosition(TitledBorder.DEFAULT_POSITION);
196        profilePanel.setBorder(title2);
197
198        profileComboBox.setModel(new javax.swing.DefaultComboBoxModel<>());
199        // Add any already-loaded profile names
200        ArrayList<String> sl = VSDecoderManager.instance().getVSDProfileNames();
201        if (sl.isEmpty()) {
202            profileComboBox.setEnabled(false);
203        } else {
204            profileComboBox.setEnabled(true);
205        }
206        updateProfileList(sl);
207        profileComboBox.addItem((loadProfilePrompt = new NullProfileBoxItem()));
208        profileComboBox.setSelectedItem(loadProfilePrompt);
209        profileComboBox.addActionListener(new java.awt.event.ActionListener() {
210            @Override
211            public void actionPerformed(java.awt.event.ActionEvent evt) {
212                profileComboBoxActionPerformed(evt);
213            }
214        });
215        profilePanel.add(profileComboBox);
216        profilePanel.add(profileLoadButton);
217        profileLoadButton.addActionListener(new java.awt.event.ActionListener() {
218            @Override
219            public void actionPerformed(java.awt.event.ActionEvent evt) {
220                profileLoadButtonActionPerformed(evt);
221            }
222        });
223
224        rosterSaveButton = new javax.swing.JButton();
225        rosterSaveButton.setText(Bundle.getMessage("ConfigSaveButtonLabel"));
226        rosterSaveButton.addActionListener(new ActionListener() {
227            @Override
228            public void actionPerformed(ActionEvent e) {
229                rosterSaveButtonAction(e);
230            }
231        });
232        rosterSaveButton.setEnabled(false); // temporarily disable this until we update the RosterEntry
233        rosterSaveButton.setToolTipText(Bundle.getMessage("RosterSaveButtonToolTip"));
234        rosterSaveButton.setMnemonic(Mnemonics.get("RosterSave"));
235
236        JPanel cbPanel = new JPanel();
237        closeButton = new JButton(Bundle.getMessage("ButtonOK"));
238        closeButton.setEnabled(false);
239        closeButton.setToolTipText(Bundle.getMessage("CD_CloseButtonToolTip"));
240        closeButton.setMnemonic(Mnemonics.get("CloseButton"));
241        closeButton.addActionListener(new java.awt.event.ActionListener() {
242            @Override
243            public void actionPerformed(java.awt.event.ActionEvent e) {
244                closeButtonActionPerformed(e);
245            }
246        });
247
248        JButton cancelButton = new JButton(Bundle.getMessage("ButtonCancel"));
249        cancelButton.setToolTipText(Bundle.getMessage("CD_CancelButtonToolTip"));
250        cancelButton.setMnemonic(Mnemonics.get("CancelButton"));
251        cancelButton.addActionListener(new java.awt.event.ActionListener() {
252            @Override
253            public void actionPerformed(java.awt.event.ActionEvent evt) {
254                cancelButtonActionPerformed(evt);
255            }
256        });
257        cbPanel.add(cancelButton);
258        cbPanel.add(rosterSaveButton);
259        cbPanel.add(closeButton);
260
261        this.add(locoSelectPanel);
262        this.add(profilePanel);
263        //this.add(rosterSaveButton);
264        this.add(cbPanel);
265        this.pack();
266        this.setVisible(true);
267    }
268
269    private void cancelButtonActionPerformed(java.awt.event.ActionEvent ae) {
270        dispose();
271    }
272
273    /**
274     * Handle the "Close" (or "OK") button action
275     */
276    private void closeButtonActionPerformed(java.awt.event.ActionEvent ae) {
277        if (profileComboBox.getSelectedItem() == null) {
278            log.debug("Profile item selected: {}", profileComboBox.getSelectedItem());
279            JmriJOptionPane.showMessageDialog(null, "Please select a valid Profile");
280            rosterSaveButton.setEnabled(false);
281            closeButton.setEnabled(false);
282        } else {
283            config.setProfileName(profileComboBox.getSelectedItem().toString());
284            log.debug("Profile item selected: {}", config.getProfileName());
285
286            config.setLocoAddress(addressSelector.getAddress());
287            if (getSelectedRosterItem() != null) {
288                config.setRosterEntry(getSelectedRosterItem());
289                // decoder volume
290                String dv = config.getRosterEntry().getAttribute("VSDecoder_Volume");
291                if (dv !=null && !dv.isEmpty()) {
292                    config.setVolume(Float.parseFloat(dv));
293                }
294                log.debug("Decoder volume in config: {}", config.getVolume());
295            } else {
296                config.setRosterEntry(null);
297            }
298            firePropertyChange(CONFIG_PROPERTY, config, null); // open the new VSDControl
299            dispose();
300        }
301    }
302
303    // class NullComboBoxItem
304    //
305    // little object to insert into profileComboBox when it's empty
306    static class NullProfileBoxItem {
307        @Override
308        public String toString() {
309            return Bundle.getMessage("NoLocoSelectedText");
310        }
311    }
312
313    private void enableProfileStuff(Boolean t) {
314        closeButton.setEnabled(t);
315        profileComboBox.setEnabled(t);
316        profileLoadButton.setEnabled(t);
317        rosterSaveButton.setEnabled(t);
318    }
319
320    /**
321     * rosterItemSelectAction()
322     *
323     * ActionEventListener function for rosterSelector
324     * Chooses a RosterEntry from the list and loads its relevant info.
325     * If all VSD Infos are provided, close the Config Dialog.
326     */
327    private void rosterItemSelectAction(ActionEvent e) {
328        if (getSelectedRosterItem() != null) {
329            log.debug("Roster Entry selected... {}", getSelectedRosterItem().getId());
330            setRosterEntry(getSelectedRosterItem());
331            enableProfileStuff(true);
332
333            log.debug("profile ComboBox selected item: {}", profileComboBox.getSelectedItem());
334            // undo the close button enable if there's no profile selected (this would
335            // be when selecting a RosterEntry that doesn't have predefined VSD info)
336            if ((profileComboBox.getSelectedIndex() == -1)
337                    || (profileComboBox.getSelectedItem() instanceof NullProfileBoxItem)) {
338                rosterSaveButton.setEnabled(false);
339                closeButton.setEnabled(false);
340                log.warn("No Profile found");
341            } else {
342                closeButton.doClick(); // All done
343            }
344        }
345    }
346
347    // Roster Entry via Auto-Load from VSDManagerFrame
348    void setRosterItem(RosterEntry s) {
349        rosterEntrySelected = s;
350        log.debug("Auto-Load selected roster id: {}, profile: {}", rosterEntrySelected.getId(),
351                rosterEntrySelected.getAttribute("VSDecoder_Profile"));
352        rosterItemSelectAction(null); // trigger the next step for Auto-Load (works, but does not seem to be implemented correctly)
353    }
354
355    private RosterEntry getRosterItem() {
356        return rosterEntrySelected;
357    }
358
359    private RosterEntry getSelectedRosterItem() {
360        // Used by Auto-Load and non Auto-Load
361        if ((is_auto_loading || is_viewing) && getRosterItem() != null) {
362            rosterEntrySelected = getRosterItem();
363        } else {
364            if (rosterSelector.getSelectedRosterEntries().length != 0) {
365                rosterEntrySelected = rosterSelector.getSelectedRosterEntries()[0];
366            } else {
367                rosterEntrySelected = null;
368            }
369        }
370        return rosterEntrySelected;
371    }
372
373    /**
374     * rosterSaveButtonAction()
375     *
376     * ActionEventListener method for rosterSaveButton Writes VSDecoder info to
377     * the RosterEntry.
378     */
379    private void rosterSaveButtonAction(ActionEvent e) {
380        log.debug("rosterSaveButton pressed");
381        if (rosterSelector.getSelectedRosterEntries().length != 0) {
382            RosterEntry r = rosterSelector.getSelectedRosterEntries()[0];
383            String profile = profileComboBox.getSelectedItem().toString();
384            String path = VSDecoderManager.instance().getProfilePath(profile);
385            if (path == null) {
386                log.warn("Path not selected.  Ignore Save button press.");
387                return;
388            } else {
389                int value = JmriJOptionPane.showConfirmDialog(null,
390                        MessageFormat.format(Bundle.getMessage("UpdateRoster"),
391                        new Object[]{r.titleString()}),
392                        Bundle.getMessage("SaveRoster?"), JmriJOptionPane.YES_NO_OPTION);
393                if (value == JmriJOptionPane.YES_OPTION) {
394                    r.putAttribute("VSDecoder_Path", path);
395                    r.putAttribute("VSDecoder_Profile", profile);
396                    if (r.getAttribute("VSDecoder_LaunchThrottle") == null) {
397                        r.putAttribute("VSDecoder_LaunchThrottle", "no");
398                    }
399                    if (r.getAttribute("VSDecoder_Volume") == null) {
400                        // convert Float to String without decimal places
401                        r.putAttribute("VSDecoder_Volume", String.valueOf(config.DEFAULT_VOLUME));
402                    }
403                    r.updateFile(); // write and update timestamp
404                    log.info("Roster Media updated for {}", r.getDisplayName());
405                    closeButton.doClick(); // All done
406                } else {
407                    log.info("Roster Media not saved");
408                }
409            }
410        }
411    }
412
413    // Probably the last setting step of the manually "Add Decoder" process
414    // (but the user also can load a VSD file and then set the address).
415    // Enable the OK button (closeButton) and the Roster Save button.
416    // note: a selected roster entry sets an Address too
417    private void profileComboBoxActionPerformed(java.awt.event.ActionEvent evt) {
418        // if there's also an Address entered, then enable the OK button.
419        if (addressSelector.getAddress() != null
420                && !(profileComboBox.getSelectedItem() instanceof NullProfileBoxItem)) {
421            closeButton.setEnabled(true);
422            // Roster Entry is required to enable the Roster Save button
423            if (rosterSelector.getSelectedRosterEntries().length != 0) {
424                rosterSaveButton.setEnabled(true);
425            }
426        }
427    }
428
429    private void profileLoadButtonActionPerformed(java.awt.event.ActionEvent evt) {
430        LoadVSDFileAction vfa = new LoadVSDFileAction();
431        vfa.actionPerformed(evt);
432        // Note: This will trigger a PROFILE_LIST_CHANGE event from VSDecoderManager
433    }
434
435    /**
436     * handle the address "Set" button
437     */
438    private void addressSetButtonActionPerformed(java.awt.event.ActionEvent evt) {
439        // address should be an integer, not a string
440        if (addressSelector.getAddress() == null) {
441            log.warn("Address is not valid");
442        }
443        // if a profile is already selected enable the OK button (closeButton)
444        if ((profileComboBox.getSelectedIndex() != -1)
445                && (!(profileComboBox.getSelectedItem() instanceof NullProfileBoxItem))) {
446            closeButton.setEnabled(true);
447        }
448    }
449
450    /**
451     * handle profile list changes from the VSDecoderManager
452     */
453    @SuppressWarnings("unchecked")
454    private void vsdecoderManagerEventAction(VSDManagerEvent evt) {
455        if (evt.getType() == VSDManagerEvent.EventType.PROFILE_LIST_CHANGE) {
456            log.debug("Received Profile List Change Event");
457            updateProfileList((ArrayList<String>) evt.getData());
458        }
459    }
460
461    /**
462     * Update the profile combo box
463     */
464    private void updateProfileList(ArrayList<String> s) {
465        // There's got to be a more efficient way to do this.
466        // Most of this is about merging the new array list with
467        // the entries already in the ComboBox.
468        if (s == null) {
469            return;
470        }
471
472        // This is a bit tedious...
473        // Pull all of the existing names from the Profile ComboBox
474        ArrayList<String> ce_list = new ArrayList<>();
475        for (int i = 0; i < profileComboBox.getItemCount(); i++) {
476            if (!(profileComboBox.getItemAt(i) instanceof NullProfileBoxItem)) {
477                ce_list.add(profileComboBox.getItemAt(i).toString());
478            }
479        }
480
481        // Cycle through the list provided as "s" and add only
482        // those profiles that aren't already there.
483        Iterator<String> itr = s.iterator();
484        while (itr.hasNext()) {
485            String st = itr.next();
486            if (!ce_list.contains(st)) {
487                log.debug("added item {}", st);
488                profileComboBox.addItem(st);
489            }
490        }
491
492        // If the combo box isn't empty, enable it and enable it
493        if (profileComboBox.getItemCount() > 0) {
494            profileComboBox.setEnabled(true);
495            // select a profile if roster items are available
496            if (getSelectedRosterItem() != null) {
497                RosterEntry r = getSelectedRosterItem();
498                String profile = r.getAttribute("VSDecoder_Profile");
499                log.debug("Trying to set the ProfileComboBox to this Profile: {}", profile);
500                if (profile != null) {
501                    profileComboBox.setSelectedItem(profile);
502                }
503            }
504        }
505    }
506
507    /**
508     * setRosterEntry()
509     *
510     * Respond to the user choosing an entry from the rosterSelector
511     * Launch a JMRI throttle (optional)
512     */
513    private void setRosterEntry(RosterEntry entry) {
514        // Update the roster entry local var.
515        rosterEntry = entry;
516
517        // Get VSD info from Roster
518        String vsd_path = rosterEntry.getAttribute("VSDecoder_Path");
519        String vsd_launch_throttle = rosterEntry.getAttribute("VSDecoder_LaunchThrottle");
520
521        log.debug("Roster entry path: {}, LaunchThrottle: {}", vsd_path, vsd_launch_throttle);
522
523        // If the roster entry has VSD info stored, load it.
524        if (vsd_path == null || vsd_path.isEmpty()) {
525            log.warn("No VSD Path found for Roster Entry \"{}\". Use the \"Save to Roster\" button to add the VSD info.",
526                    rosterEntry.getId());
527        } else {
528            // Load the indicated VSDecoder Profile and update the Profile combo box
529            // This will trigger a PROFILE_LIST_CHANGE event from the VSDecoderManager.
530            boolean is_loaded = LoadVSDFileAction.loadVSDFile(vsd_path);
531
532            if (is_loaded &&
533                    vsd_launch_throttle != null &&
534                    vsd_launch_throttle.equals("yes") &&
535                    InstanceManager.throttleManagerInstance().getThrottleUsageCount(rosterEntry) == 0) {
536                // Launch a JMRI Throttle (if setup by the Roster media attribut and a throttle not already exists).
537                ThrottleControllerUI tf = InstanceManager.getDefault(jmri.jmrit.throttle.ThrottleFrameManager.class).createThrottleController();
538                tf.toFront();
539                tf.setRosterEntry(Roster.getDefault().entryFromTitle(rosterEntry.getId()));
540            }
541        }
542
543        // Set the Address box from the Roster entry.
544        // Do this after the VSDecoder create, so it will see the change.
545        addressSelector.setAddress(entry.getDccLocoAddress());
546        addressSelector.setEnabled(true);
547        addressSetButton.setEnabled(true);
548    }
549
550    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(VSDConfigDialog.class);
551
552}