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            log.error("Exception: ", e1);
259            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("ErrorComLibLoad"));
260            return;
261        }
262
263        if (adapter.getSystemConnectionMemo() != null) {
264            systemPrefixField.setText(adapter.getSystemConnectionMemo().getSystemPrefix());
265            connectionNameField.setText(adapter.getSystemConnectionMemo().getUserName());
266            NUMOPTIONS = NUMOPTIONS + 2;
267        }
268
269        refreshPortBox();
270
271        NUMOPTIONS = NUMOPTIONS + options.size();
272
273        portBoxLabel = new JLabel(Bundle.getMessage("UsbPortLocationLabel"));
274
275        showAdvanced.setFont(showAdvanced.getFont().deriveFont(9f));
276        showAdvanced.setForeground(Color.blue);
277        showAdvanced.addItemListener((ItemEvent e) -> showAdvancedItems());
278        showAdvancedItems();
279        init = false;       // need to reload action listeners
280        checkInitDone();
281    }
282
283    @Override
284    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE",
285            justification = "Type is checked before casting")
286    protected void showAdvancedItems() {
287        log.debug("*   showAdvancedItems()");
288        _details.removeAll();
289        cL.anchor = GridBagConstraints.WEST;
290        cL.insets = new Insets(2, 5, 0, 5);
291        cR.insets = new Insets(2, 0, 0, 5);
292        cR.anchor = GridBagConstraints.WEST;
293        cR.gridx = 1;
294        cL.gridx = 0;
295        int i = 0;
296
297        boolean incAdvancedOptions = isPortAdvanced();
298
299        if (!incAdvancedOptions) {
300            for (Map.Entry<String, Option> entry : options.entrySet()) {
301                if (entry.getValue().isAdvanced()) {
302                    incAdvancedOptions = true;
303                    break;
304                }
305            }
306        }
307
308        _details.setLayout(gbLayout);
309
310        i = addStandardDetails(incAdvancedOptions, i);
311
312        showAdvanced.setVisible(incAdvancedOptions);
313
314        if (incAdvancedOptions && showAdvanced.isSelected()) {
315            if (isPortAdvanced()) {
316                cR.gridy = i;
317                cL.gridy = i;
318                gbLayout.setConstraints(portBoxLabel, cL);
319                gbLayout.setConstraints(portBox, cR);
320
321                //panel.add(row1Label);
322                _details.add(portBoxLabel);
323                _details.add(portBox);
324                i++;
325            }
326
327            for (Map.Entry<String, Option> entry : options.entrySet()) {
328                if (entry.getValue().isAdvanced()) {
329                    cR.gridy = i;
330                    cL.gridy = i;
331                    gbLayout.setConstraints(entry.getValue().getLabel(), cL);
332                    gbLayout.setConstraints(entry.getValue().getComponent(), cR);
333                    _details.add(entry.getValue().getLabel());
334                    _details.add(entry.getValue().getComponent());
335                    i++;
336                }
337            }
338        }
339        cL.gridwidth = 2;
340        for (JComponent item : additionalItems) {
341            cL.gridy = i;
342            gbLayout.setConstraints(item, cL);
343            _details.add(item);
344            i++;
345        }
346        cL.gridwidth = 1;
347
348        if ((_details.getParent() != null) && (_details.getParent() instanceof JViewport)) {
349            JViewport vp = (JViewport) _details.getParent();
350            vp.revalidate();
351            vp.repaint();
352        }
353    }
354
355    protected int addStandardDetails(boolean incAdvanced, int i) {
356        log.debug("*   addStandardDetails()");
357        if (!isPortAdvanced()) {
358            cR.gridy = i;
359            cL.gridy = i;
360            gbLayout.setConstraints(portBoxLabel, cL);
361            gbLayout.setConstraints(portBox, cR);
362            _details.add(portBoxLabel);
363            _details.add(portBox);
364            i++;
365        }
366
367        return addStandardDetails(adapter, incAdvanced, i);
368    }
369
370    public boolean isPortAdvanced() {
371        log.debug("*   isPortAdvanced()");
372        return false;
373    }
374
375    @Override
376    public String getManufacturer() {
377        log.debug("*   getManufacturer()");
378        return adapter.getManufacturer();
379    }
380
381    @Override
382    public void setManufacturer(String manufacturer) {
383        setInstance();
384        log.debug("*   setManufacturer('{}')", manufacturer);
385        adapter.setManufacturer(manufacturer);
386    }
387
388    @Override
389    public boolean getDisabled() {
390        log.debug("*   getDisabled()");
391        if (adapter == null) {
392            return true;
393        }
394        return adapter.getDisabled();
395    }
396
397    @Override
398    public void setDisabled(boolean disabled) {
399        log.debug("*   setDisabled({})", disabled ? "True" : "False");
400        if (adapter != null) {
401            adapter.setDisabled(disabled);
402        }
403    }
404
405    @Override
406    public String getConnectionName() {
407        log.debug("*   getConnectionName()");
408        if ((adapter != null) && (adapter.getSystemConnectionMemo() != null)) {
409            return adapter.getSystemConnectionMemo().getUserName();
410        } else {
411            return name();
412        }
413    }
414
415    @Override
416    public void dispose() {
417        log.debug("*   dispose()");
418        if (adapter != null) {
419            adapter.dispose();
420            adapter = null;
421        }
422        //removeFromActionList();
423        super.dispose();
424
425    }
426
427    class ComboBoxRenderer extends JLabel
428            implements ListCellRenderer<String> {
429
430        public ComboBoxRenderer() {
431            setHorizontalAlignment(LEFT);
432            setVerticalAlignment(CENTER);
433        }
434
435        /*
436         * This method finds the image and text corresponding
437         * to the selected value and returns the label, set up
438         * to display the text and image.
439         */
440        @Override
441        public Component getListCellRendererComponent(
442                JList<? extends String> list,
443                String name,
444                int index,
445                boolean isSelected,
446                boolean cellHasFocus) {
447
448            setOpaque(index > -1);
449            setForeground(Color.black);
450            list.setSelectionForeground(Color.black);
451            if (isSelected && index > -1) {
452                setBackground(list.getSelectionBackground());
453            } else {
454                setBackground(list.getBackground());
455            }
456            if (invalidPort != null) {
457                if ((name == null) || name.isEmpty() || name.equals(invalidPort)) {
458                    list.setSelectionForeground(Color.red);
459                    setForeground(Color.red);
460                }
461            }
462
463            setText(name);
464
465            return this;
466        }
467    }
468
469    /**
470     * Handle friendly port names. Note that this changes the selection in
471     * portCombo, so that should be tracked after this returns.
472     *
473     * @param portName  The currently-selected port name
474     * @param portCombo The combo box that's displaying the available ports
475     * @param portList  The list of valid (unfriendly) port names
476     */
477    protected synchronized static void updateUsbPortNames(String portName, JComboBox<String> portCombo, List<String> portList) {
478        for (int i = 0; i < portList.size(); i++) {
479            String commPort = portList.get(i);
480            portCombo.addItem(commPort);
481            if (commPort.equals(portName)) {
482                portCombo.setSelectedIndex(i);
483            }
484        }
485    }
486
487    @Nonnull
488    protected List<String> getPortNames() {
489        log.error("getPortNames() called in abstract class; should be overridden.");
490        return new ArrayList<>();
491    }
492
493    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractUsbConnectionConfig.class);
494
495}