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.ArrayList;
012import java.util.List;
013import java.util.Map;
014
015import javax.annotation.Nonnull;
016import javax.swing.JComboBox;
017import javax.swing.JComponent;
018import javax.swing.JLabel;
019import javax.swing.JList;
020import javax.swing.JPanel;
021import javax.swing.JViewport;
022import javax.swing.ListCellRenderer;
023
024import jmri.InstanceManager;
025import jmri.UserPreferencesManager;
026import jmri.util.swing.JmriJOptionPane;
027
028/**
029 * Abstract base class for common implementation of the ConnectionConfig
030 *
031 * @author Bob Jacobsen Copyright (C) 2001, 2003
032 * @author George Warner Copyright (c) 2017-2018
033 */
034abstract public class AbstractUsbConnectionConfig extends AbstractConnectionConfig {
035
036    /**
037     * Create a connection configuration with a preexisting adapter. This is
038     * used principally when loading a configuration that defines this
039     * connection.
040     *
041     * @param p the adapter to create a connection configuration for
042     */
043    public AbstractUsbConnectionConfig(UsbPortAdapter p) {
044        adapter = p;
045        //addToActionList();
046        log.debug("*   AbstractUSBConnectionConfig({})", p);
047    }
048
049    /**
050     * Ctor for a functional object with no preexisting adapter. Expect that the
051     * subclass setInstance() will fill the adapter member.
052     */
053    public AbstractUsbConnectionConfig() {
054        this(null);
055        log.debug("*   AbstractUSBConnectionConfig()");
056    }
057
058    protected UsbPortAdapter adapter;
059
060    @Override
061    public UsbPortAdapter getAdapter() {
062        log.debug("*   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            addNameEntryCheckers(adapter);
076            portBox.addFocusListener(new FocusListener() {
077                @Override
078                public void focusGained(FocusEvent e) {
079                    refreshPortBox();
080                }
081
082                @Override
083                public void focusLost(FocusEvent e) {
084                }
085
086            });
087
088            for (Map.Entry<String, Option> entry : options.entrySet()) {
089                final String item = entry.getKey();
090                if (entry.getValue().getComponent() instanceof JComboBox) {
091                    ((JComboBox<?>) entry.getValue().getComponent()).addActionListener((ActionEvent e) -> {
092                        adapter.setOptionState(item, options.get(item).getItem());
093                    });
094                }
095            }
096            init = true;
097        }
098    }
099
100    @Override
101    public void updateAdapter() {
102        log.debug("*   updateAdapter()");
103    }
104
105    protected UserPreferencesManager p = InstanceManager.getDefault(UserPreferencesManager.class);
106    protected JComboBox<String> portBox = new JComboBox<>();
107    protected JLabel portBoxLabel;
108
109    @Override
110    public String getInfo() {
111        log.debug("*   getInfo()");
112        String t = (String) portBox.getSelectedItem();
113        if (t != null) {
114            return t;
115        } else if ((adapter != null) && (adapter.getCurrentPortName() != null)) {
116            return adapter.getCurrentPortName();
117        }
118
119        return JmrixConfigPane.NONE;
120    }
121
122    List<String> newList = null;
123    List<String> originalList = null;
124    String invalidPort = null;
125
126    public void refreshPortBox() {
127        log.debug("*   refreshPortBox()");
128        if (!init) {
129            newList = getPortNames();
130            portBox.setRenderer(new ComboBoxRenderer());
131            // Add this line to ensure that the combo box header isn't made too narrow
132            portBox.setPrototypeDisplayValue("A fairly long port name of 40 characters"); //NO18N
133        } else {
134            List<String> v2 = getPortNames();
135            if (v2.equals(originalList)) {
136                log.debug("List of valid Ports has not changed, therefore we will not refresh the port list");
137                // but we will insist on setting the current value into the port
138                adapter.setPort((String) portBox.getSelectedItem());
139                return;
140            }
141            log.debug("List of valid Ports has been changed, therefore we will refresh the port list");
142            newList = new ArrayList<>(v2);
143        }
144
145        if (newList == null) {
146            log.error("port name List v is null!");
147            return;
148        }
149
150        /* As we make amendments to the list of ports in newList, we keep a copy of it before
151         modification. This copy is then used to validate against any changes in the port lists.
152         */
153        originalList = new ArrayList<>(newList);
154        if (portBox.getActionListeners().length > 0) {
155            portBox.removeActionListener(portBox.getActionListeners()[0]);
156        }
157        portBox.removeAllItems();
158        log.debug("getting fresh list of available Serial Ports");
159
160        if (newList.isEmpty()) {
161            newList.add(0, Bundle.getMessage("noPortsFound"));
162        }
163        String portName = adapter.getCurrentPortName();
164        if (portName != null && !portName.equals(Bundle.getMessage("noneSelected")) && !portName.equals(Bundle.getMessage("noPortsFound"))) {
165            if (!newList.contains(portName)) {
166                newList.add(0, portName);
167                invalidPort = portName;
168                portBox.setForeground(Color.red);
169            } else if (invalidPort != null && invalidPort.equals(portName)) {
170                invalidPort = null;
171            }
172        } else {
173            if (!newList.contains(portName)) {
174                newList.add(0, Bundle.getMessage("noneSelected"));
175            } else if (p.getComboBoxLastSelection(adapter.getClass().getName() + ".port") == null) {
176                newList.add(0, Bundle.getMessage("noneSelected"));
177            }
178        }
179
180        updateUsbPortNames(portName, portBox, newList);
181
182        // If no name is selected, select one that seems most likely
183        boolean didSetName = false;
184        if ((portName == null)
185                || portName.equals(Bundle.getMessage("noneSelected"))
186                || portName.equals(Bundle.getMessage("noPortsFound"))) {
187//            for (int i = 0; i < portBox.getItemCount(); i++) {
188//                for (String friendlyName : getPortFriendlyNames()) {
189//                    if ((portBox.getItemAt(i)).contains(friendlyName)) {
190//                        portBox.setSelectedIndex(i);
191//                        adapter.setPort(portBox.getItemAt(i));
192//                        didSetName = true;
193//                        break;
194//                    }
195//                }
196//            }
197            // if didn't set name, don't leave it hanging
198            if (!didSetName) {
199                portBox.setSelectedIndex(0);
200            }
201        }
202        // finally, insist on synchronization of selected port name with underlying port
203
204        adapter.setPort((String) portBox.getSelectedItem());
205
206        // add a listener for later changes
207        portBox.addActionListener(
208                (ActionEvent e) -> {
209                    String port = (String) portBox.getSelectedItem();
210                    adapter.setPort(port);
211                }
212        );
213    }
214
215    /**
216     * {@inheritDoc}
217     */
218    @Override
219    public void loadDetails(final JPanel details) {
220        log.debug("*   loadDetails()");
221        _details = details;
222        setInstance();
223        if (!init) {
224            //Build up list of options
225            String[] optionsAvailable = adapter.getOptions();
226            options.clear();
227            for (String i : optionsAvailable) {
228                JComboBox<String> opt = new JComboBox<>(adapter.getOptionChoices(i));
229                opt.setSelectedItem(adapter.getOptionState(i));
230                // check that it worked
231                if (!adapter.getOptionState(i).equals(opt.getSelectedItem())) {
232                    // no, set 1st option choice
233                    opt.setSelectedIndex(0);
234                    // log before setting new value to show old value
235                    log.warn("Loading found invalid value for option {}, found \"{}\", setting to \"{}\"", i, adapter.getOptionState(i), opt.getSelectedItem());
236                    adapter.setOptionState(i, (String) opt.getSelectedItem());
237                }
238                options.put(i, new Option(adapter.getOptionDisplayName(i), opt, adapter.isOptionAdvanced(i)));
239            }
240        }
241
242        try {
243            newList = getPortNames();
244            if (log.isDebugEnabled()) {
245                log.debug("loadDetails called in class {}", this.getClass().getName());
246                log.debug("adapter class: {}", adapter.getClass().getName());
247                log.debug("loadDetails called for {}", name());
248                if (newList != null) {
249                    log.debug("Found {} ports", newList.size());
250                } else {
251                    log.debug("Zero-length port List");
252                }
253            }
254        }
255        catch (UnsatisfiedLinkError e1) {
256            log.error("UnsatisfiedLinkError - the serial library has not been installed properly");
257            log.error("java.library.path={}", System.getProperty("java.library.path", "<unknown>"));
258            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("ErrorComLibLoad"));
259            return;
260        }
261
262        if (adapter.getSystemConnectionMemo() != null) {
263            systemPrefixField.setText(adapter.getSystemConnectionMemo().getSystemPrefix());
264            connectionNameField.setText(adapter.getSystemConnectionMemo().getUserName());
265            NUMOPTIONS = NUMOPTIONS + 2;
266        }
267
268        refreshPortBox();
269
270        NUMOPTIONS = NUMOPTIONS + options.size();
271
272        portBoxLabel = new JLabel(Bundle.getMessage("UsbPortLocationLabel"));
273
274        showAdvanced.setFont(showAdvanced.getFont().deriveFont(9f));
275        showAdvanced.setForeground(Color.blue);
276        showAdvanced.addItemListener((ItemEvent e) -> showAdvancedItems());
277        showAdvancedItems();
278        init = false;       // need to reload action listeners
279        checkInitDone();
280    }
281
282    @Override
283    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE",
284            justification = "Type is checked before casting")
285    protected void showAdvancedItems() {
286        log.debug("*   showAdvancedItems()");
287        _details.removeAll();
288        cL.anchor = GridBagConstraints.WEST;
289        cL.insets = new Insets(2, 5, 0, 5);
290        cR.insets = new Insets(2, 0, 0, 5);
291        cR.anchor = GridBagConstraints.WEST;
292        cR.gridx = 1;
293        cL.gridx = 0;
294        int i = 0;
295
296        boolean incAdvancedOptions = isPortAdvanced();
297
298        if (!incAdvancedOptions) {
299            for (Map.Entry<String, Option> entry : options.entrySet()) {
300                if (entry.getValue().isAdvanced()) {
301                    incAdvancedOptions = true;
302                    break;
303                }
304            }
305        }
306
307        _details.setLayout(gbLayout);
308
309        i = addStandardDetails(incAdvancedOptions, i);
310
311        showAdvanced.setVisible(incAdvancedOptions);
312
313        if (incAdvancedOptions && showAdvanced.isSelected()) {
314            if (isPortAdvanced()) {
315                cR.gridy = i;
316                cL.gridy = i;
317                gbLayout.setConstraints(portBoxLabel, cL);
318                gbLayout.setConstraints(portBox, cR);
319
320                //panel.add(row1Label);
321                _details.add(portBoxLabel);
322                _details.add(portBox);
323                i++;
324            }
325
326            for (Map.Entry<String, Option> entry : options.entrySet()) {
327                if (entry.getValue().isAdvanced()) {
328                    cR.gridy = i;
329                    cL.gridy = i;
330                    gbLayout.setConstraints(entry.getValue().getLabel(), cL);
331                    gbLayout.setConstraints(entry.getValue().getComponent(), cR);
332                    _details.add(entry.getValue().getLabel());
333                    _details.add(entry.getValue().getComponent());
334                    i++;
335                }
336            }
337        }
338        cL.gridwidth = 2;
339        for (JComponent item : additionalItems) {
340            cL.gridy = i;
341            gbLayout.setConstraints(item, cL);
342            _details.add(item);
343            i++;
344        }
345        cL.gridwidth = 1;
346
347        if ((_details.getParent() != null) && (_details.getParent() instanceof JViewport)) {
348            JViewport vp = (JViewport) _details.getParent();
349            vp.revalidate();
350            vp.repaint();
351        }
352    }
353
354    protected int addStandardDetails(boolean incAdvanced, int i) {
355        log.debug("*   addStandardDetails()");
356        if (!isPortAdvanced()) {
357            cR.gridy = i;
358            cL.gridy = i;
359            gbLayout.setConstraints(portBoxLabel, cL);
360            gbLayout.setConstraints(portBox, cR);
361            _details.add(portBoxLabel);
362            _details.add(portBox);
363            i++;
364        }
365
366        return addStandardDetails(adapter, incAdvanced, i);
367    }
368
369    public boolean isPortAdvanced() {
370        log.debug("*   isPortAdvanced()");
371        return false;
372    }
373
374    @Override
375    public String getManufacturer() {
376        log.debug("*   getManufacturer()");
377        return adapter.getManufacturer();
378    }
379
380    @Override
381    public void setManufacturer(String manufacturer) {
382        setInstance();
383        log.debug("*   setManufacturer('{}')", manufacturer);
384        adapter.setManufacturer(manufacturer);
385    }
386
387    @Override
388    public boolean getDisabled() {
389        log.debug("*   getDisabled()");
390        if (adapter == null) {
391            return true;
392        }
393        return adapter.getDisabled();
394    }
395
396    @Override
397    public void setDisabled(boolean disabled) {
398        log.debug("*   setDisabled({})", disabled ? "True" : "False");
399        if (adapter != null) {
400            adapter.setDisabled(disabled);
401        }
402    }
403
404    @Override
405    public String getConnectionName() {
406        log.debug("*   getConnectionName()");
407        if ((adapter != null) && (adapter.getSystemConnectionMemo() != null)) {
408            return adapter.getSystemConnectionMemo().getUserName();
409        } else {
410            return name();
411        }
412    }
413
414    @Override
415    public void dispose() {
416        log.debug("*   dispose()");
417        if (adapter != null) {
418            adapter.dispose();
419            adapter = null;
420        }
421        //removeFromActionList();
422        super.dispose();
423
424    }
425
426    class ComboBoxRenderer extends JLabel
427            implements ListCellRenderer<String> {
428
429        public ComboBoxRenderer() {
430            setHorizontalAlignment(LEFT);
431            setVerticalAlignment(CENTER);
432        }
433
434        /*
435         * This method finds the image and text corresponding
436         * to the selected value and returns the label, set up
437         * to display the text and image.
438         */
439        @Override
440        public Component getListCellRendererComponent(
441                JList<? extends String> list,
442                String name,
443                int index,
444                boolean isSelected,
445                boolean cellHasFocus) {
446
447            setOpaque(index > -1);
448            setForeground(Color.black);
449            list.setSelectionForeground(Color.black);
450            if (isSelected && index > -1) {
451                setBackground(list.getSelectionBackground());
452            } else {
453                setBackground(list.getBackground());
454            }
455            if (invalidPort != null) {
456                if ((name == null) || name.isEmpty() || name.equals(invalidPort)) {
457                    list.setSelectionForeground(Color.red);
458                    setForeground(Color.red);
459                }
460            }
461
462            setText(name);
463
464            return this;
465        }
466    }
467
468    /**
469     * Handle friendly port names. Note that this changes the selection in
470     * portCombo, so that should be tracked after this returns.
471     *
472     * @param portName  The currently-selected port name
473     * @param portCombo The combo box that's displaying the available ports
474     * @param portList  The list of valid (unfriendly) port names
475     */
476    protected synchronized static void updateUsbPortNames(String portName, JComboBox<String> portCombo, List<String> portList) {
477        for (int i = 0; i < portList.size(); i++) {
478            String commPort = portList.get(i);
479            portCombo.addItem(commPort);
480            if (commPort.equals(portName)) {
481                portCombo.setSelectedIndex(i);
482            }
483        }
484    }
485
486    @Nonnull
487    protected List<String> getPortNames() {
488        log.error("getPortNames() called in abstract class; should be overridden.");
489        return new ArrayList<>();
490    }
491
492    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractUsbConnectionConfig.class);
493
494}