001package jmri.jmrit.symbolicprog;
002
003import java.awt.event.ActionListener;
004import java.util.ArrayList;
005import java.util.List;
006import javax.swing.BoxLayout;
007import javax.swing.JComboBox;
008import javax.swing.JLabel;
009import javax.swing.JList;
010import javax.swing.JPanel;
011import javax.swing.JScrollPane;
012import javax.swing.JToggleButton;
013import javax.swing.ListSelectionModel;
014import javax.swing.event.ListSelectionEvent;
015import javax.swing.event.ListSelectionListener;
016import jmri.GlobalProgrammerManager;
017import jmri.InstanceManager;
018import jmri.Programmer;
019import jmri.jmrit.decoderdefn.DecoderFile;
020import jmri.jmrit.decoderdefn.DecoderIndexFile;
021import jmri.jmrit.progsupport.ProgModeSelector;
022import jmri.jmrit.roster.Roster;
023import jmri.jmrit.roster.RosterEntry;
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027/**
028 * Provide GUI controls to select a known loco and/or new decoder.
029 * <p>
030 * This is an extension of the CombinedLocoSelPane class to use a JList instead
031 * of a JComboBox for the decoder selection. Also, this uses separate JLists for
032 * manufacturer and decoder model. The loco selection (Roster manipulation)
033 * parts are unchanged.
034 * <p>
035 * The JComboBox implementation always had to have selected entries, so we added
036 * dummy "select from .." items at the top and used those to indicate
037 * that there was no selection in that box. Here, the lack of a selection
038 * indicates there's no selection.
039 *
040 * @author Bob Jacobsen Copyright (C) 2001, 2002
041 */
042public class CombinedLocoSelListPane extends CombinedLocoSelPane {
043
044    public CombinedLocoSelListPane(JLabel s, ProgModeSelector selector) {
045        super(s, selector);
046    }
047
048    /**
049     * Create the panel used to select the decoder
050     */
051    @Override
052    protected JPanel layoutDecoderSelection() {
053        JPanel pane1a = new JPanel();
054        pane1a.setLayout(new BoxLayout(pane1a, BoxLayout.X_AXIS));
055        pane1a.add(new JLabel("Decoder installed: "));
056        // create the list of manufacturers
057        mMfgList = new JList<>();
058        updateMfgListContents(null);
059        mMfgList.clearSelection();
060        mMfgList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
061        mMfgListener = new ListSelectionListener() {
062            @Override
063            public void valueChanged(ListSelectionEvent e) {
064                if (!mMfgList.isSelectionEmpty()) {
065                    // manufacturer selected, update decoder list
066                    String vMfg = mMfgList.getSelectedValue();
067                    try {
068                        int vMfgID = Integer.parseInt(
069                                InstanceManager.getDefault(DecoderIndexFile.class).mfgIdFromName(vMfg));
070
071                        listDecodersFromMfg(vMfgID, vMfg);
072                    } catch (java.lang.NumberFormatException ex) {
073                        // mfg number lookup failed for some reason
074                    }
075                } else {
076                    // no manufacturer selected, do nothing
077                }
078            }
079        };
080        mMfgList.addListSelectionListener(mMfgListener);
081
082        mDecoderList = new JList<String>(InstanceManager.getDefault(DecoderIndexFile.class)
083                .matchingComboBox(null, null, null, null, null, null).getModel());
084        mDecoderList.clearSelection();
085        mDecoderList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
086        mDecoderListener = new ListSelectionListener() {
087            @Override
088            public void valueChanged(ListSelectionEvent e) {
089                if (!mDecoderList.isSelectionEmpty()) {
090                    // decoder selected - reset and disable loco selection
091                    locoBox.setSelectedIndex(0);
092                    go2.setEnabled(true);
093                    go2.setToolTipText(Bundle.getMessage("TipClickToOpen"));
094                    updateMfgListToSelectedDecoder();
095                } else {
096                    // decoder not selected - require one
097                    go2.setEnabled(false);
098                    go2.setToolTipText(Bundle.getMessage("TipSelectLoco"));
099                }
100            }
101        };
102        mDecoderList.addListSelectionListener(mDecoderListener);
103
104        pane1a.add(new JScrollPane(mMfgList));
105        pane1a.add(new JScrollPane(mDecoderList));
106        iddecoder = new JToggleButton("Ident");
107        iddecoder.setToolTipText("Read the decoders mfg and version, then attempt to select its type");
108        if (InstanceManager.getNullableDefault(GlobalProgrammerManager.class) != null) {
109            Programmer p = InstanceManager.getDefault(GlobalProgrammerManager.class).getGlobalProgrammer();
110            if (p != null && !p.getCanRead()) {
111                // can't read, disable the button
112                iddecoder.setEnabled(false);
113                iddecoder.setToolTipText("Button disabled because configured command station can't read CVs");
114            }
115        }
116        iddecoder.addActionListener(new ActionListener() {
117            @Override
118            public void actionPerformed(java.awt.event.ActionEvent e) {
119                if (log.isDebugEnabled()) {
120                    log.debug("identify decoder pressed");
121                }
122                startIdentifyDecoder();
123            }
124        });
125        pane1a.add(iddecoder);
126        pane1a.setAlignmentX(JLabel.RIGHT_ALIGNMENT);
127        return pane1a;
128    }
129
130    /**
131     * Update the contents of the manufacturer list to make sure it contains a
132     * specific value. Normally the list does not contain mfgs with no defined
133     * decoders; this allows you to also show a specific mfg that's of interest,
134     * even though there's no definitions for it. This is protected against
135     * invoking any listeners, as the change is meant to be transparent; the
136     * original selection is set back.
137     * @param specific The value to update
138     */
139    void updateMfgListContents(String specific) {
140        if (mMfgListener != null) {
141            mMfgList.removeListSelectionListener(mMfgListener);
142        }
143        String currentValue = mMfgList.getSelectedValue();
144
145        List<String> allMfgList = InstanceManager.getDefault(DecoderIndexFile.class).getMfgNameList();
146        List<String> theMfgList = new ArrayList<>();
147
148        for (int i = 0; i < allMfgList.size(); i++) {
149            // see if this qualifies; either a non-zero set of decoders, or
150            // matches the specific name
151            if ((specific != null && (allMfgList.get(i).equals(specific)))
152                    || (0 != InstanceManager.getDefault(DecoderIndexFile.class)
153                            .matchingDecoderList(allMfgList.get(i), null, null, null, null, null)
154                            .size())) {
155                theMfgList.add(allMfgList.get(i));
156            }
157        }
158        mMfgList.setListData(theMfgList.toArray(new String[0]));
159
160        mMfgList.setSelectedValue(currentValue, true);
161        if (mMfgListener != null) {
162            mMfgList.addListSelectionListener(mMfgListener);
163        }
164
165    }
166
167    /**
168     * Force the manufacturer list to select the mfg of the currently selected
169     * decoder. Note that this is complicated by the need to not trigger an
170     * update of the decoder list.
171     */
172    void updateMfgListToSelectedDecoder() {
173        // update to point at this mfg, _without_ changing the decoder list
174        DecoderFile df = InstanceManager.getDefault(DecoderIndexFile.class)
175                .fileFromTitle(mDecoderList.getSelectedValue());
176        if (log.isDebugEnabled()) {
177            log.debug("decoder selection changed to {}", mDecoderList.getSelectedValue());
178        }
179        if (df != null) {
180            if (log.isDebugEnabled()) {
181                log.debug("matching mfg is {}", df.getMfg());
182            }
183            updateMfgListWithoutTrigger(df.getMfg());
184        }
185    }
186
187    /**
188     * Set a selection in the manufacturer list, without triggering an update of
189     * the decoder panel.
190     * @param mfg Selected manufacturer code
191     */
192    void updateMfgListWithoutTrigger(String mfg) {
193        mMfgList.removeListSelectionListener(mMfgListener);
194        mMfgList.setSelectedValue(mfg, true);
195        mMfgList.addListSelectionListener(mMfgListener);
196    }
197
198    /**
199     * Decoder identify has matched one or more specific types
200     */
201    @Override
202    void updateForDecoderTypeID(List<DecoderFile> pModelList) {
203        // use a DefaultComboBoxModel to get the efficient ctor
204        mDecoderList.setModel(DecoderIndexFile.jComboBoxModelFromList(pModelList));
205        mDecoderList.setSelectedIndex(0);
206    }
207
208    /**
209     * Decoder identify has not matched specific types, but did find
210     * manufacturer match
211     *
212     * @param pMfg     Manufacturer name. This is passed to save time, as it has
213     *                 already been determined once.
214     * @param pMfgID   Manufacturer ID number (CV8)
215     * @param pModelID Model ID number (CV7)
216     */
217    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
218        justification="String also built for display in _statusLabel")
219    @Override
220    void updateForDecoderMfgID(String pMfg, int pMfgID, int pModelID) {
221        String msg = "Found mfg " + pMfgID + " (" + pMfg + ") version " + pModelID + "; no such decoder defined";
222        log.warn(msg);
223        _statusLabel.setText(msg);
224        // ensure manufacturer shows & is selected
225        updateMfgListContents(pMfg);
226        updateMfgListWithoutTrigger(pMfg);
227        // list all other decoders available, without a selection
228        listDecodersFromMfg(pMfgID, pMfg);
229    }
230
231    void listDecodersFromMfg(int pMfgID, String pMfg) {
232        // try to select all decoders from that MFG
233        JComboBox<String> temp = InstanceManager.getDefault(DecoderIndexFile.class).matchingComboBox(null, null, Integer.toString(pMfgID), null, null, null);
234        if (log.isDebugEnabled()) {
235            log.debug("mfg-only selectDecoder found {} matches", temp.getItemCount());
236        }
237        // install all those in the JComboBox in place of the longer, original list
238        mDecoderList.setModel(temp.getModel());
239        mDecoderList.clearSelection();
240        updateMfgListWithoutTrigger(pMfg);
241    }
242
243    /**
244     * Decoder identify did not match anything, warn and show all
245     */
246    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
247        justification="String also built for display in _statusLabel")
248    @Override
249    void updateForDecoderNotID(int pMfgID, int pModelID) {
250        String msg = "Found mfg " + pMfgID + " version " + pModelID + "; no such manufacterer defined";
251        log.warn(msg);
252        _statusLabel.setText(msg);
253        mMfgList.setSelectedIndex(1);
254        mMfgList.clearSelection();
255        JComboBox<String> temp = InstanceManager.getDefault(DecoderIndexFile.class).matchingComboBox(null, null, null, null, null, null);
256        mDecoderList.setModel(temp.getModel());
257        mDecoderList.clearSelection();
258    }
259
260    /**
261     * Set the decoder selection to a specific decoder from a selected Loco
262     */
263    @Override
264    void setDecoderSelectionFromLoco(String loco) {
265        // if there's a valid loco entry...
266        RosterEntry locoEntry = Roster.getDefault().entryFromTitle(loco);
267        if (locoEntry == null) {
268            return;
269        }
270        // get the decoder type, it has to be there (assumption!),
271        String modelString = locoEntry.getDecoderModel();
272        // find the decoder mfg
273        String mfgString = InstanceManager.getDefault(DecoderIndexFile.class).fileFromTitle(modelString)
274                .getMfg();
275
276        // then select it
277        updateMfgListWithoutTrigger(mfgString);
278
279        // decoder has to be there (assumption!)
280        // so load it into the list directly
281        String[] tempArray = new String[1];
282        tempArray[0] = modelString;
283        mDecoderList.setListData(tempArray);
284        // select the entry you just put in, but don't trigger anything!
285        mDecoderList.removeListSelectionListener(mDecoderListener);
286        mDecoderList.setSelectedIndex(0);
287        mDecoderList.addListSelectionListener(mDecoderListener);
288    }
289
290    /**
291     * Has the user selected a decoder type, either manually or via a successful
292     * event?
293     *
294     * @return true if a decoder type is selected
295     */
296    @Override
297    boolean isDecoderSelected() {
298        return !mDecoderList.isSelectionEmpty();
299    }
300
301    /**
302     * Convert the decoder selection UI result into a name.
303     *
304     * @return The selected decoder type name, or null if none selected.
305     */
306    @Override
307    protected String selectedDecoderType() {
308        if (!isDecoderSelected()) {
309            return null;
310        } else {
311            return mDecoderList.getSelectedValue();
312        }
313    }
314
315    JList<String> mDecoderList;
316    ListSelectionListener mDecoderListener;
317
318    JList<String> mMfgList;
319    ListSelectionListener mMfgListener;
320
321    private final static Logger log = LoggerFactory.getLogger(CombinedLocoSelListPane.class);
322
323}