001package jmri.jmrix;
002
003import java.awt.Color;
004import java.awt.Component;
005import java.awt.GridBagConstraints;
006import java.awt.Insets;
007import java.awt.event.ActionEvent;
008import java.awt.event.FocusEvent;
009import java.awt.event.FocusListener;
010import java.awt.event.ItemEvent;
011import java.util.Collections;
012import java.util.Map;
013import java.util.ResourceBundle;
014import java.util.Vector;
015
016import javax.swing.JButton;
017import javax.swing.JComboBox;
018import javax.swing.JComponent;
019import javax.swing.JLabel;
020import javax.swing.JList;
021import javax.swing.JPanel;
022import javax.swing.JSpinner;
023import javax.swing.JTextField;
024import javax.swing.ListCellRenderer;
025import javax.swing.SpinnerNumberModel;
026
027import jmri.util.PortNameMapper;
028import jmri.util.PortNameMapper.SerialPortFriendlyName;
029import jmri.util.swing.JComboBoxUtil;
030
031/**
032 * Abstract base class for common implementation of the SerialConnectionConfig.
033 *
034 * @author Bob Jacobsen Copyright (C) 2001, 2003
035 */
036abstract public class AbstractSerialConnectionConfig extends AbstractConnectionConfig {
037
038    /**
039     * Ctor for an object being created during load process.
040     *
041     * @param p port being configured
042     */
043    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST", justification = "Thought to be safe as default connection config")
044    public AbstractSerialConnectionConfig(jmri.jmrix.PortAdapter p) {
045        this((jmri.jmrix.SerialPortAdapter) p);
046    }
047
048    public AbstractSerialConnectionConfig(jmri.jmrix.SerialPortAdapter p) {
049        adapter = p;
050    }
051
052    /**
053     * Ctor for a functional object with no preexisting adapter. Expect that the
054     * subclass setInstance() will fill the adapter member.
055     */
056    public AbstractSerialConnectionConfig() {
057        adapter = null;
058
059    }
060
061    @Override
062    public jmri.jmrix.SerialPortAdapter getAdapter() {
063        return adapter;
064    }
065
066    protected boolean init = false;
067
068    /**
069     * {@inheritDoc}
070     */
071    @Override
072    protected void checkInitDone() {
073        log.debug("init called for {}", name());
074        if (init) {
075            return;
076        }
077
078        baudBox.addActionListener(e -> {
079            adapter.configureBaudRate((String) baudBox.getSelectedItem());
080            p.setComboBoxLastSelection(adapter.getClass().getName() + ".baud", (String) baudBox.getSelectedItem()); // NOI18N
081        });
082
083        addNameEntryCheckers(adapter);
084
085        portBox.addFocusListener(new FocusListener() {
086            @Override
087            public void focusGained(FocusEvent e) {
088                refreshPortBox();
089            }
090
091            @Override
092            public void focusLost(FocusEvent e) {
093            }
094        });
095
096        // set/change delay interval between (actually before) output (Turnout) commands
097        outputIntervalSpinner.addChangeListener(e -> adapter.getSystemConnectionMemo().setOutputInterval((Integer) outputIntervalSpinner.getValue()));
098
099        for (Map.Entry<String, Option> entry : options.entrySet()) {
100            final String item = entry.getKey();
101            if (entry.getValue().getComponent() instanceof JComboBox) {
102                ((JComboBox<?>) entry.getValue().getComponent()).addActionListener((ActionEvent e) -> {
103                    adapter.setOptionState(item, options.get(item).getItem());
104                });
105                JComboBoxUtil.setupComboBoxMaxRows((JComboBox<?>) entry.getValue().getComponent());
106            }
107        }
108
109        init = true;
110    }
111
112    @Override
113    public void updateAdapter() {
114        log.debug("updateAdapter() to {}", systemPrefixField.getText());
115        adapter.setPort(PortNameMapper.getPortFromName((String) portBox.getSelectedItem()));
116        adapter.configureBaudRateFromIndex(baudBox.getSelectedIndex()); // manage by index, not item value
117        for (Map.Entry<String, Option> entry : options.entrySet()) {
118            adapter.setOptionState(entry.getKey(), entry.getValue().getItem());
119        }
120
121        if (adapter.getSystemConnectionMemo() != null && !adapter.getSystemConnectionMemo().setSystemPrefix(systemPrefixField.getText())) {
122            systemPrefixField.setText(adapter.getSystemConnectionMemo().getSystemPrefix());
123            connectionNameField.setText(adapter.getSystemConnectionMemo().getUserName());
124        }
125    }
126
127    jmri.UserPreferencesManager p = jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class);
128    protected JComboBox<String> portBox = new JComboBox<>();
129    protected JLabel portBoxLabel;
130    protected JComboBox<String> baudBox = new JComboBox<>();
131    protected JLabel baudBoxLabel;
132    protected String[] baudList;
133
134    private final SpinnerNumberModel intervalSpinner = new SpinnerNumberModel(250, 0, 10000, 1); // 10 sec max seems long enough
135    // the following items are protected so they can be hidden when not applicable from a specific ConnectionConfig (ie. Simulator) implementation
136    protected JSpinner outputIntervalSpinner = new JSpinner(intervalSpinner);
137    protected JLabel outputIntervalLabel;
138    protected JButton outputIntervalReset = new JButton(Bundle.getMessage("ButtonReset"));
139
140    protected jmri.jmrix.SerialPortAdapter adapter;
141
142    /**
143     * {@inheritDoc}
144     */
145    @Override
146    abstract protected void setInstance();
147
148    @Override
149    public String getInfo() {
150        String t = (String) portBox.getSelectedItem();
151        if (t != null) {
152            return PortNameMapper.getPortFromName(t);
153            //return t;
154        } else if ((adapter != null) && (adapter.getCurrentPortName() != null)) {
155            return adapter.getCurrentPortName();
156        }
157
158        return JmrixConfigPane.NONE;
159    }
160
161//    @SuppressWarnings("UseOfObsoleteCollectionType")
162    Vector<String> v;
163//    @SuppressWarnings("UseOfObsoleteCollectionType")
164    Vector<String> originalList;
165    String invalidPort = null;
166
167//    @SuppressWarnings("UseOfObsoleteCollectionType")
168    public void refreshPortBox() {
169        if (!init) {
170            v = getPortNames();
171            portBox.setRenderer(new ComboBoxRenderer());
172            // Add this line to ensure that the combo box header isn't made too narrow
173            portBox.setPrototypeDisplayValue("A fairly long port name of 40 characters"); //NO18N
174        } else {
175            Vector<String> v2 = getPortNames();
176            if (v2.equals(originalList)) {
177                log.debug("List of valid Ports has not changed, therefore we will not refresh the port list");
178                // but we will insist on setting the current value into the port
179                adapter.setPort(PortNameMapper.getPortFromName((String) portBox.getSelectedItem()));
180                return;
181            }
182            log.debug("List of valid Ports has been changed, therefore we will refresh the port list");
183            v = new Vector<>();
184            v.setSize(v2.size());
185            Collections.copy(v, v2);
186        }
187
188        if (v == null) {
189            log.error("port name Vector v is null!");
190            return;
191        }
192
193        /* as we make amendments to the list of port in vector v, we keep a copy of it before
194         modification, this copy is then used to validate against any changes in the port lists.
195         */
196        originalList = new Vector<>();
197        originalList.setSize(v.size());
198        Collections.copy(originalList, v);
199        if (portBox.getActionListeners().length > 0) {
200            portBox.removeActionListener(portBox.getActionListeners()[0]);
201        }
202        portBox.removeAllItems();
203        log.debug("getting fresh list of available Serial Ports");
204
205        if (v.isEmpty()) {
206            v.add(0, Bundle.getMessage("noPortsFound"));
207        }
208        String portName = adapter.getCurrentPortName();
209        if (portName != null && !portName.equals(Bundle.getMessage("noneSelected")) && !portName.equals(Bundle.getMessage("noPortsFound"))) {
210            if (!v.contains(portName)) {
211                v.add(0, portName);
212                invalidPort = portName;
213                portBox.setForeground(Color.red);
214            } else if (invalidPort != null && invalidPort.equals(portName)) {
215                invalidPort = null;
216            }
217        } else {
218            if (!v.contains(portName)) {
219                v.add(0, Bundle.getMessage("noneSelected"));
220            } else if (p.getComboBoxLastSelection(adapter.getClass().getName() + ".port") == null) {
221                v.add(0, Bundle.getMessage("noneSelected"));
222            }
223        }
224        updateSerialPortNames(portName, portBox, v);
225        JComboBoxUtil.setupComboBoxMaxRows(portBox);
226
227        // If there's no name selected, select one that seems most likely
228        boolean didSetName = false;
229        if (portName == null || portName.equals(Bundle.getMessage("noneSelected")) || portName.equals(Bundle.getMessage("noPortsFound"))) {
230            for (int i = 0; i < portBox.getItemCount(); i++) {
231                for (String friendlyName : getPortFriendlyNames()) {
232                    if ((portBox.getItemAt(i)).contains(friendlyName)) {
233                        portBox.setSelectedIndex(i);
234                        adapter.setPort(PortNameMapper.getPortFromName(portBox.getItemAt(i)));
235                        didSetName = true;
236                        break;
237                    }
238                }
239            }
240            // if didn't set name, don't leave it hanging
241            if (!didSetName) {
242                portBox.setSelectedIndex(0);
243            }
244        }
245        // finally, insist on synchronization of selected port name with underlying port
246        adapter.setPort(PortNameMapper.getPortFromName((String) portBox.getSelectedItem()));
247
248        // add a listener for later changes
249        portBox.addActionListener((ActionEvent e) -> {
250            String port = PortNameMapper.getPortFromName((String) portBox.getSelectedItem());
251            adapter.setPort(port);
252        });
253    }
254
255    /**
256     * {@inheritDoc}
257     */
258    @Override
259//    @SuppressWarnings("UseOfObsoleteCollectionType")
260    public void loadDetails(final JPanel details) {
261        _details = details;
262        setInstance();
263        if (!init) {
264            //Build up list of options
265            String[] optionsAvailable = adapter.getOptions();
266            options.clear();
267            for (String i : optionsAvailable) {
268                JComboBox<String> opt = new JComboBox<>(adapter.getOptionChoices(i));
269                opt.setSelectedItem(adapter.getOptionState(i));
270                // check that it worked
271                if (!adapter.getOptionState(i).equals(opt.getSelectedItem())) {
272                    // no, set 1st option choice
273                    opt.setSelectedIndex(0);
274                    // log before setting new value to show old value
275                    log.warn("Loading found invalid value for option {}, found \"{}\", setting to \"{}\"", i, adapter.getOptionState(i), opt.getSelectedItem());
276                    adapter.setOptionState(i, (String) opt.getSelectedItem());
277                }
278                options.put(i, new Option(adapter.getOptionDisplayName(i), opt, adapter.isOptionAdvanced(i)));
279            }
280        }
281
282        try {
283            v = getPortNames();
284            if (log.isDebugEnabled()) {
285                log.debug("loadDetails called in class {}", this.getClass().getName());
286                log.debug("adapter class: {}", adapter.getClass().getName());
287                log.debug("loadDetails called for {}", name());
288                if (v != null) {
289                    log.debug("Found {} ports", v.size());
290                } else {
291                    log.debug("Zero-length port vector");
292                }
293            }
294        } catch (java.lang.UnsatisfiedLinkError e1) {
295            log.error("UnsatisfiedLinkError - the serial library has not been installed properly");
296            log.error("java.library.path={}", System.getProperty("java.library.path", "<unknown>"));
297            jmri.util.swing.JmriJOptionPane.showMessageDialog(null, "Failed to load comm library.\nYou have to fix that before setting preferences.");
298            return;
299        }
300
301        if (adapter.getSystemConnectionMemo() != null) {
302            systemPrefixField.setText(adapter.getSystemConnectionMemo().getSystemPrefix());
303            connectionNameField.setText(adapter.getSystemConnectionMemo().getUserName());
304            NUMOPTIONS = NUMOPTIONS + 2;
305        }
306
307        refreshPortBox();
308
309        baudList = adapter.validBaudRates(); // when not supported should not return null, but an empty String[] {}
310        // need to remove ActionListener before addItem() or action event will occur
311        if (baudBox.getActionListeners().length > 0) {
312            baudBox.removeActionListener(baudBox.getActionListeners()[0]);
313        }
314        // rebuild baudBox combo list
315        baudBox.removeAllItems();
316        if (log.isDebugEnabled()) {
317            log.debug("after remove, {} items, first is {}", baudBox.getItemCount(),
318                    baudBox.getItemAt(0));
319        }
320
321        // empty array means: baud not supported by adapter (but extends serialConnConfig)
322        if (baudList.length == 0) {
323            log.debug("empty array received from adapter");
324        }
325        for (String baudList1 : baudList) {
326            baudBox.addItem(baudList1);
327        }
328        if (log.isDebugEnabled()) {
329            log.debug("after reload, {} items, first is {}", baudBox.getItemCount(),
330                    baudBox.getItemAt(0));
331        }
332
333        if (baudList.length > 1) {
334            baudBox.setToolTipText(Bundle.getMessage("TipBaudRateMatch"));
335            baudBox.setEnabled(true);
336        } else {
337            baudBox.setToolTipText(Bundle.getMessage("TipBaudRateFixed"));
338            baudBox.setEnabled(false);
339        }
340
341        NUMOPTIONS = NUMOPTIONS + options.size();
342
343        portBoxLabel = new JLabel(Bundle.getMessage("SerialPortLabel"));
344        baudBoxLabel = new JLabel(Bundle.getMessage("BaudRateLabel"));
345        if (baudBox.getItemCount() > 0) { // skip when adapter returned an empty array (= spotbug's preference)
346            baudBox.setSelectedIndex(adapter.getCurrentBaudIndex());
347        }
348        // connection (memo) specific output command delay option, calls jmri.jmrix.SystemConnectionMemo#setOutputInterval(int)
349        outputIntervalLabel = new JLabel(Bundle.getMessage("OutputIntervalLabel"));
350        outputIntervalSpinner.setToolTipText(Bundle.getMessage("OutputIntervalTooltip",
351                adapter.getSystemConnectionMemo().getDefaultOutputInterval(),adapter.getManufacturer()));
352        JTextField field = ((JSpinner.DefaultEditor) outputIntervalSpinner.getEditor()).getTextField();
353        field.setColumns(6);
354        outputIntervalSpinner.setMaximumSize(outputIntervalSpinner.getPreferredSize()); // set spinner JTextField width
355        outputIntervalSpinner.setValue(adapter.getSystemConnectionMemo().getOutputInterval());
356        outputIntervalSpinner.setEnabled(true);
357        outputIntervalReset.addActionListener((ActionEvent event) -> {
358            outputIntervalSpinner.setValue(adapter.getSystemConnectionMemo().getDefaultOutputInterval());
359            adapter.getSystemConnectionMemo().setOutputInterval(adapter.getSystemConnectionMemo().getDefaultOutputInterval());
360        });
361
362        showAdvanced.setFont(showAdvanced.getFont().deriveFont(9f));
363        showAdvanced.setForeground(Color.blue);
364        showAdvanced.addItemListener((ItemEvent e) -> {
365            showAdvancedItems();
366        });
367        showAdvancedItems();
368        init = false;       // need to reload action listeners
369        checkInitDone();
370    }
371
372    @Override
373    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE",
374        justification = "Type is checked before casting")
375    protected void showAdvancedItems() {
376        _details.removeAll();
377        cL.anchor = GridBagConstraints.WEST;
378        cL.insets = new Insets(2, 5, 0, 5);
379        cR.insets = new Insets(2, 0, 0, 5);
380        cR.anchor = GridBagConstraints.WEST;
381        cR.gridx = 1;
382        cL.gridx = 0;
383        int i = 0;
384        int stdrows = 0;
385        boolean incAdvancedOptions = true;
386        if (!isBaudAdvanced()) {
387            stdrows++;
388        }
389        if (!isPortAdvanced()) {
390            stdrows++;
391        }
392        for (Map.Entry<String, Option> entry : options.entrySet()) {
393            if (!entry.getValue().isAdvanced()) {
394                stdrows++;
395            }
396        }
397
398        if (adapter.getSystemConnectionMemo() != null) {
399            stdrows = stdrows + 2;
400        }
401        if (stdrows == NUMOPTIONS) {
402            incAdvancedOptions = false;
403        }
404        _details.setLayout(gbLayout);
405        i = addStandardDetails(incAdvancedOptions, i);
406        if (showAdvanced.isSelected()) {
407
408            if (isPortAdvanced()) {
409                cR.gridy = i;
410                cL.gridy = i;
411                gbLayout.setConstraints(portBoxLabel, cL);
412                gbLayout.setConstraints(portBox, cR);
413
414                _details.add(portBoxLabel);
415                _details.add(portBox);
416                i++;
417            }
418
419            if (isBaudAdvanced()) {
420                cR.gridy = i;
421                cL.gridy = i;
422                gbLayout.setConstraints(baudBoxLabel, cL);
423                gbLayout.setConstraints(baudBox, cR);
424                _details.add(baudBoxLabel);
425                _details.add(baudBox);
426                i++;
427            }
428
429            for (Map.Entry<String, Option> entry : options.entrySet()) {
430                if (entry.getValue().isAdvanced()) {
431                    cR.gridy = i;
432                    cL.gridy = i;
433                    gbLayout.setConstraints(entry.getValue().getLabel(), cL);
434                    gbLayout.setConstraints(entry.getValue().getComponent(), cR);
435                    _details.add(entry.getValue().getLabel());
436                    _details.add(entry.getValue().getComponent());
437                    i++;
438                }
439            }
440
441            // interval config field
442            cR.gridy = i;
443            cL.gridy = i;
444            gbLayout.setConstraints(outputIntervalLabel, cL);
445            _details.add(outputIntervalLabel);
446            JPanel intervalPanel = new JPanel();
447            gbLayout.setConstraints(intervalPanel, cR);
448            intervalPanel.add(outputIntervalSpinner);
449            intervalPanel.add(outputIntervalReset);
450            _details.add(intervalPanel);
451            i++;
452
453        }
454        cL.gridwidth = 2;
455        for (JComponent item : additionalItems) {
456            cL.gridy = i;
457            gbLayout.setConstraints(item, cL);
458            _details.add(item);
459            i++;
460        }
461        cL.gridwidth = 1;
462
463        if (_details.getParent() != null && _details.getParent() instanceof javax.swing.JViewport) {
464            javax.swing.JViewport vp = (javax.swing.JViewport) _details.getParent();
465            vp.revalidate();
466            vp.repaint();
467        }
468    }
469
470    protected int addStandardDetails(boolean incAdvanced, int i) {
471        if (!isPortAdvanced()) {
472            cR.gridy = i;
473            cL.gridy = i;
474            gbLayout.setConstraints(portBoxLabel, cL);
475            gbLayout.setConstraints(portBox, cR);
476            _details.add(portBoxLabel);
477            _details.add(portBox);
478            i++;
479        }
480
481        if (!isBaudAdvanced()) {
482            cR.gridy = i;
483            cL.gridy = i;
484            gbLayout.setConstraints(baudBoxLabel, cL);
485            gbLayout.setConstraints(baudBox, cR);
486            _details.add(baudBoxLabel);
487            _details.add(baudBox);
488            i++;
489        }
490
491        return addStandardDetails(adapter, incAdvanced, i);
492    }
493
494    public boolean isPortAdvanced() {
495        return false;
496    }
497
498    public boolean isBaudAdvanced() {
499        return true;
500    }
501
502    @Override
503    public String getManufacturer() {
504        return adapter.getManufacturer();
505    }
506
507    @Override
508    public void setManufacturer(String manufacturer) {
509        setInstance();
510        adapter.setManufacturer(manufacturer);
511    }
512
513    @Override
514    public boolean getDisabled() {
515        if (adapter == null) {
516            return true;
517        }
518        return adapter.getDisabled();
519    }
520
521    @Override
522    public void setDisabled(boolean disabled) {
523        if (adapter != null) {
524            adapter.setDisabled(disabled);
525        }
526    }
527
528    @Override
529    public String getConnectionName() {
530        if ((adapter != null) && (adapter.getSystemConnectionMemo() != null)) {
531            return adapter.getSystemConnectionMemo().getUserName();
532        } else {
533            return name();
534        }
535    }
536
537    @Override
538    public void dispose() {
539        super.dispose();
540        if (adapter != null) {
541            adapter.dispose();
542            adapter = null;
543        }
544    }
545
546    class ComboBoxRenderer extends JLabel
547            implements ListCellRenderer<String> {
548
549        public ComboBoxRenderer() {
550            setHorizontalAlignment(LEFT);
551            setVerticalAlignment(CENTER);
552        }
553
554        /*
555         * This method finds the image and text corresponding
556         * to the selected value and returns the label, set up
557         * to display the text and image.
558         */
559        @Override
560        public Component getListCellRendererComponent(
561                JList<? extends String> list,
562                String name,
563                int index,
564                boolean isSelected,
565                boolean cellHasFocus) {
566
567            setOpaque(index > -1);
568            setForeground(Color.black);
569            list.setSelectionForeground(Color.black);
570            if (isSelected && index > -1) {
571                setBackground(list.getSelectionBackground());
572            } else {
573                setBackground(list.getBackground());
574            }
575            if (invalidPort != null) {
576                String port = PortNameMapper.getPortFromName(name);
577                if (port.equals(invalidPort)) {
578                    list.setSelectionForeground(Color.red);
579                    setForeground(Color.red);
580                }
581            }
582
583            setText(name);
584
585            return this;
586        }
587    }
588
589    /**
590     * Handle friendly port names. Note that this
591     * changes the selection in portCombo, so
592     * that should be tracked after this returns.
593     *
594     * @param portName The currently-selected port name
595     * @param portCombo The combo box that's displaying the available ports
596     * @param portList The list of valid (unfriendly) port names
597     */
598//    @SuppressWarnings("UseOfObsoleteCollectionType")
599    protected synchronized static void updateSerialPortNames(String portName, JComboBox<String> portCombo, Vector<String> portList) {
600        for (Map.Entry<String, SerialPortFriendlyName> en : PortNameMapper.getPortNameMap().entrySet()) {
601            en.getValue().setValidPort(false);
602        }
603        for (int i = 0; i < portList.size(); i++) {
604            String commPort = portList.elementAt(i);
605            SerialPortFriendlyName port = PortNameMapper.getPortNameMap().get(commPort);
606            if (port == null) {
607                port = new SerialPortFriendlyName(commPort, null);
608                PortNameMapper.getPortNameMap().put(commPort, port);
609            }
610            port.setValidPort(true);
611            portCombo.addItem(port.getDisplayName());
612            if (commPort.equals(portName)) {
613                portCombo.setSelectedIndex(i);
614            }
615        }
616    }
617
618    /**
619     * Provide a vector of valid port names, each a String.
620     * This may be implemented differently in subclasses 
621     * that e.g. do loopback or use a custom port-access library.
622     * @return Valid port names in the form used to select them later.
623     */
624//    @SuppressWarnings("UseOfObsoleteCollectionType") // historical interface
625    protected Vector<String> getPortNames() {
626        return AbstractSerialPortController.getActualPortNames();
627    }
628        
629    /**
630     * This provides a method to return potentially meaningful names that are
631     * used in OS to help identify ports against Hardware.
632     *
633     * @return array of friendly port names
634     */
635    protected String[] getPortFriendlyNames() {
636        return new String[]{};
637    }
638
639    /**
640     * This is purely here for systems that do not implement the
641     * SystemConnectionMemo and can be removed once they have been migrated.
642     *
643     * @return Resource bundle for action model
644     */
645    protected ResourceBundle getActionModelResourceBundle() {
646        return null;
647    }
648
649    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractSerialConnectionConfig.class);
650
651}