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