001package jmri.jmrit.roster;
002
003import java.awt.*;
004import java.awt.event.FocusEvent;
005import java.awt.event.FocusListener;
006import java.text.DateFormat;
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.List;
010
011import javax.swing.*;
012
013import jmri.DccLocoAddress;
014import jmri.InstanceManager;
015import jmri.LocoAddress;
016import jmri.jmrit.DccLocoAddressSelector;
017import jmri.jmrit.decoderdefn.DecoderFile;
018import jmri.jmrit.decoderdefn.DecoderIndexFile;
019import jmri.util.swing.JmriJOptionPane;
020
021/**
022 * Display and enable editing a RosterEntry panel to display on first tab "Roster Entry".
023 * Called from {@link jmri.jmrit.symbolicprog.tabbedframe.PaneProgFrame}#makeInfoPane(RosterEntry)
024 *
025 * @author Bob Jacobsen Copyright (C) 2001
026 * @author Dennis Miller Copyright 2004, 2005
027 */
028public class RosterEntryPane extends javax.swing.JPanel {
029
030    // Field sizes expanded to 30 from 12 to match comment
031    // fields and allow for more text to be displayed
032    JTextField id = new JTextField(30);
033    JTextField roadName = new JTextField(30);
034    JTextField maxSpeed = new JTextField(3);
035    JSpinner maxSpeedSpinner = new JSpinner(); // percentage stored as fraction
036
037    JTextField roadNumber = new JTextField(30);
038    JTextField mfg = new JTextField(30);
039    JTextField model = new JTextField(30);
040    JTextField owner = new JTextField(30);
041    DccLocoAddressSelector addrSel = new DccLocoAddressSelector();
042
043    JTextArea comment = new JTextArea(3, 50);
044    public String getComment() {return comment.getText();}
045    public void setComment(String text) {comment.setText(text);}
046
047    // JScrollPanes are defined with scroll bars on always to avoid undesirable resizing behavior
048    // Without this the field will shrink to minimum size any time the scroll bars become needed and
049    // the scroll bars are inside, not outside the field area, obscuring their contents.
050    // This way the shrinking does not happen and the scroll bars are outside the field area,
051    // leaving the contents visible
052    JScrollPane commentScroller = new JScrollPane(comment, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
053    JLabel dateUpdated = new JLabel();
054    JLabel decoderModel = new JLabel();
055    JLabel decoderFamily = new JLabel();
056    JLabel decoderProgModes = new JLabel();
057    JTextArea decoderComment = new JTextArea(3, 50);
058    JScrollPane decoderCommentScroller = new JScrollPane(decoderComment, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
059
060    Component pane;
061    RosterEntry re;
062
063    public RosterEntryPane(RosterEntry r) {
064
065        maxSpeedSpinner.setModel(new SpinnerNumberModel(1.00d, 0.00d, 1.00d, 0.01d));
066        maxSpeedSpinner.setEditor(new JSpinner.NumberEditor(maxSpeedSpinner, "# %"));
067        id.setText(r.getId());
068
069        if (r.getDccAddress().isEmpty()) {
070            // null address, so clear selector
071            addrSel.reset();
072        } else {
073            // non-null address, so load
074            DccLocoAddress tempAddr = new DccLocoAddress(
075                    Integer.parseInt(r.getDccAddress()), r.getProtocol());
076            addrSel.setAddress(tempAddr);
077        }
078
079        // fill contents
080        RosterEntryPane.this.updateGUI(r);
081
082        pane = this;
083        re = r;
084
085        // add options
086        id.setToolTipText(Bundle.getMessage("ToolTipID"));
087
088        addrSel.setEnabled(false);
089        addrSel.setLocked(false);
090
091        if ((InstanceManager.getNullableDefault(jmri.ThrottleManager.class) != null)
092                && !InstanceManager.throttleManagerInstance().addressTypeUnique()) {
093            // This goes through to find common protocols between the command station and the decoder
094            // and will set the selection box list to match those that are common.
095            jmri.ThrottleManager tm = InstanceManager.throttleManagerInstance();
096            List<LocoAddress.Protocol> protocolTypes = new ArrayList<>(Arrays.asList(tm.getAddressProtocolTypes()));
097
098            if (!protocolTypes.contains(LocoAddress.Protocol.DCC_LONG) && !protocolTypes.contains(LocoAddress.Protocol.DCC_SHORT)) {
099                //Multi-protocol systems so far are not worried about dcc long vs dcc short
100                List<DecoderFile> l = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, r.getDecoderFamily(), null, null, null, r.getDecoderModel());
101                if (log.isDebugEnabled()) {
102                    log.debug("found {} matched", l.size());
103                }
104                if (l.isEmpty()) {
105                    log.debug("Loco uses {} {} decoder, but no such decoder defined", decoderFamily, decoderModel);
106                    // fall back to use just the decoder name, not family
107                    l = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, null, null, null, null, r.getDecoderModel());
108                    if (log.isDebugEnabled()) {
109                        log.debug("found {} matches without family key", l.size());
110                    }
111                }
112                DecoderFile d;
113                if (!l.isEmpty()) {
114                    d = l.get(0);
115                    if (d != null && d.getSupportedProtocols().length > 0) {
116                        ArrayList<String> protocols = new ArrayList<>(d.getSupportedProtocols().length);
117
118                        for (LocoAddress.Protocol i : d.getSupportedProtocols()) {
119                            if (protocolTypes.contains(i)) {
120                                protocols.add(tm.getAddressTypeString(i));
121                            }
122                        }
123                        addrSel = new DccLocoAddressSelector(protocols.toArray(new String[0]));
124                        DccLocoAddress tempAddr = new DccLocoAddress(
125                                Integer.parseInt(r.getDccAddress()), r.getProtocol());
126                        addrSel.setAddress(tempAddr);
127                        addrSel.setEnabled(false);
128                        addrSel.setLocked(false);
129                        addrSel.setEnabledProtocol(true);
130                    }
131                }
132            }
133        }
134
135        JPanel selPanel = addrSel.getCombinedJPanel();
136        selPanel.setToolTipText(Bundle.getMessage("ToolTipDccAddress"));
137        decoderModel.setToolTipText(Bundle.getMessage("ToolTipDecoderModel"));
138        decoderFamily.setToolTipText(Bundle.getMessage("ToolTipDecoderFamily"));
139        decoderProgModes.setToolTipText(Bundle.getMessage("ToolTipDecoderProgModes"));
140        dateUpdated.setToolTipText(Bundle.getMessage("ToolTipDateUpdated"));
141        id.addFocusListener(new FocusListener() {
142            @Override
143            public void focusGained(FocusEvent e) {
144            }
145
146            @Override
147            public void focusLost(FocusEvent e) {
148                if (checkDuplicate()) {
149                    JmriJOptionPane.showMessageDialog(pane, Bundle.getMessage("ErrorDuplicateID"));
150                }
151            }
152        });
153
154        // New GUI to allow multiline Comment and Decoder Comment fields
155        // Set up constraints objects for convenience in GridBagLayout alignment
156        GridBagLayout gbLayout = new GridBagLayout();
157        GridBagConstraints cL = new GridBagConstraints();
158        GridBagConstraints cR = new GridBagConstraints();
159        Dimension minFieldDim = new Dimension(150, 20);
160        Dimension minScrollerDim = new Dimension(165, 42);
161        super.setLayout(gbLayout);
162
163        cL.gridx = 0;
164        cL.gridy = 0;
165        cL.ipadx = 3;
166        cL.anchor = GridBagConstraints.NORTHWEST;
167        cL.insets = new Insets(0, 0, 0, 15);
168        JLabel row0Label = new JLabel(Bundle.getMessage("FieldID") + ":");
169        id.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldID"));
170        gbLayout.setConstraints(row0Label, cL);
171        super.add(row0Label);
172
173        cR.gridx = 1;
174        cR.gridy = 0;
175        cR.anchor = GridBagConstraints.WEST;
176        id.setMinimumSize(minFieldDim);
177        gbLayout.setConstraints(id, cR);
178        super.add(id);
179
180        cL.gridy++;
181        JLabel row1Label = new JLabel(Bundle.getMessage("FieldRoadName") + ":");
182        roadName.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldRoadName"));
183        gbLayout.setConstraints(row1Label, cL);
184        super.add(row1Label);
185
186        cR.gridy = cL.gridy;
187        roadName.setMinimumSize(minFieldDim);
188        gbLayout.setConstraints(roadName, cR);
189        super.add(roadName);
190
191        cL.gridy++;
192        JLabel row2Label = new JLabel(Bundle.getMessage("FieldRoadNumber") + ":");
193        roadNumber.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldRoadNumber"));
194        gbLayout.setConstraints(row2Label, cL);
195        super.add(row2Label);
196
197        cR.gridy = cL.gridy;
198        roadNumber.setMinimumSize(minFieldDim);
199        gbLayout.setConstraints(roadNumber, cR);
200        super.add(roadNumber);
201
202        cL.gridy++;
203        JLabel row3Label = new JLabel(Bundle.getMessage("FieldManufacturer") + ":");
204        mfg.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldManufacturer"));
205        gbLayout.setConstraints(row3Label, cL);
206        super.add(row3Label);
207
208        cR.gridy = cL.gridy;
209        mfg.setMinimumSize(minFieldDim);
210        gbLayout.setConstraints(mfg, cR);
211        super.add(mfg);
212
213        cL.gridy++;
214        JLabel row4Label = new JLabel(Bundle.getMessage("FieldOwner") + ":");
215        owner.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldOwner"));
216        gbLayout.setConstraints(row4Label, cL);
217        super.add(row4Label);
218
219        cR.gridy = cL.gridy;
220        owner.setMinimumSize(minFieldDim);
221        gbLayout.setConstraints(owner, cR);
222        super.add(owner);
223
224        cL.gridy++;
225        JLabel row5Label = new JLabel(Bundle.getMessage("FieldModel") + ":");
226        model.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldModel"));
227        gbLayout.setConstraints(row5Label, cL);
228        super.add(row5Label);
229
230        cR.gridy = cL.gridy;
231        model.setMinimumSize(minFieldDim);
232        gbLayout.setConstraints(model, cR);
233        super.add(model);
234
235        cL.gridy++;
236        JLabel row6Label = new JLabel(Bundle.getMessage("FieldDCCAddress") + ":");
237        selPanel.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldDCCAddress"));
238        gbLayout.setConstraints(row6Label, cL);
239        super.add(row6Label);
240
241        cR.gridy = cL.gridy;
242        gbLayout.setConstraints(selPanel, cR);
243        super.add(selPanel);
244
245        cL.gridy++;
246        JLabel row7Label = new JLabel(Bundle.getMessage("FieldSpeedLimit") + ":");
247        maxSpeedSpinner.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldSpeedLimit"));
248        gbLayout.setConstraints(row7Label, cL);
249        super.add(row7Label);
250
251        cR.gridy = cL.gridy; // JSpinner is initialised in RosterEntryPane()
252        gbLayout.setConstraints(maxSpeedSpinner, cR);
253        super.add(maxSpeedSpinner);
254
255        cL.gridy++;
256        JLabel row8Label = new JLabel(Bundle.getMessage("FieldComment") + ":");
257        // ensure same font on textarea as textfield
258        // as this is not true in all GUI types.
259        comment.setFont(owner.getFont());
260        commentScroller.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldComment"));
261        gbLayout.setConstraints(row8Label, cL);
262        super.add(row8Label);
263
264        cR.gridy = cL.gridy;
265        commentScroller.setMinimumSize(minScrollerDim);
266        gbLayout.setConstraints(commentScroller, cR);
267        super.add(commentScroller);
268
269        cL.gridy++;
270        JLabel row9Label = new JLabel(Bundle.getMessage("FieldDecoderFamily") + ":");
271        decoderFamily.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldDecoderFamily"));
272        gbLayout.setConstraints(row9Label, cL);
273        super.add(row9Label);
274
275        cR.gridy = cL.gridy;
276        decoderFamily.setMinimumSize(minFieldDim);
277        gbLayout.setConstraints(decoderFamily, cR);
278        super.add(decoderFamily);
279
280        cL.gridy++;
281        JLabel row10Label = new JLabel(Bundle.getMessage("FieldDecoderModel") + ":");
282        decoderModel.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldDecoderModel"));
283        gbLayout.setConstraints(row10Label, cL);
284        super.add(row10Label);
285
286        cR.gridy = cL.gridy;
287        decoderModel.setMinimumSize(minFieldDim);
288        gbLayout.setConstraints(decoderModel, cR);
289        super.add(decoderModel);
290
291        cL.gridy++;
292        JLabel row11Label = new JLabel(Bundle.getMessage("FieldDecoderModes") + ":");
293        decoderProgModes.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldDecoderModes"));
294        gbLayout.setConstraints(row11Label, cL);
295        super.add(row11Label);
296
297        cR.gridy = cL.gridy;
298        decoderProgModes.setMinimumSize(minFieldDim);
299        gbLayout.setConstraints(decoderProgModes, cR);
300        super.add(decoderProgModes);
301
302        cL.gridy++;
303        JLabel row12Label = new JLabel(Bundle.getMessage("FieldDecoderComment") + ":");
304        // ensure same font on textarea as textfield
305        // as this is not true in all GUI types.
306        decoderComment.setFont(owner.getFont());
307        decoderCommentScroller.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldDecoderComment"));
308        gbLayout.setConstraints(row12Label, cL);
309        super.add(row12Label);
310
311        cR.gridy = cL.gridy;
312        decoderCommentScroller.setMinimumSize(minScrollerDim);
313        gbLayout.setConstraints(decoderCommentScroller, cR);
314        super.add(decoderCommentScroller);
315
316        cL.gridy++;
317        JLabel row13Label = new JLabel(Bundle.getMessage("FieldDateUpdated") + ":");
318        dateUpdated.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldDateUpdated"));
319        gbLayout.setConstraints(row13Label, cL);
320        super.add(row13Label);
321
322        cR.gridy = cL.gridy;
323        dateUpdated.setMinimumSize(minFieldDim);
324        gbLayout.setConstraints(dateUpdated, cR);
325        super.add(dateUpdated);
326    }
327
328    double maxSet;
329
330    /**
331     * Do the GUI contents agree with a RosterEntry?
332     *
333     * @param r the entry to compare
334     * @return true if entry in GUI does not match r; false otherwise
335     */
336    public boolean guiChanged(RosterEntry r) {
337        if (!r.getRoadName().equals(roadName.getText())) {
338            return true;
339        }
340        if (!r.getRoadNumber().equals(roadNumber.getText())) {
341            return true;
342        }
343        if (!r.getMfg().equals(mfg.getText())) {
344            return true;
345        }
346        if (!r.getOwner().equals(owner.getText())) {
347            return true;
348        }
349        if (!r.getModel().equals(model.getText())) {
350            return true;
351        }
352        if (!r.getComment().equals(comment.getText())) {
353            return true;
354        }
355        if (!r.getDecoderFamily().equals(decoderFamily.getText())) {
356            return true;
357        }
358        if (!r.getDecoderModel().equals(decoderModel.getText())) {
359            return true;
360        }
361        if (!r.getProgrammingModes().equals(decoderProgModes.getText())) {
362            return true;
363        }
364        if (!r.getDecoderComment().equals(decoderComment.getText())) {
365            return true;
366        }
367        if (!r.getId().equals(id.getText())) {
368            return true;
369        }
370        maxSet = (Double) maxSpeedSpinner.getValue();
371        if (r.getMaxSpeedPCT() != (int) Math.round(100 * maxSet)) {
372            log.debug("check: {}|{}", r.getMaxSpeedPCT(), (int) Math.round(100 * maxSet));
373            return true;
374        }
375        DccLocoAddress a = addrSel.getAddress();
376        if (a == null) {
377            return !r.getDccAddress().isEmpty();
378        } else {
379            if (r.getProtocol() != a.getProtocol()) {
380                return true;
381            }
382            return !r.getDccAddress().equals("" + a.getNumber());
383        }
384    }
385
386    /**
387     *
388     * @return true if the value in the id JTextField is a duplicate of some
389     *         other RosterEntry in the roster
390     */
391    public boolean checkDuplicate() {
392        // check it's not a duplicate
393        List<RosterEntry> l = Roster.getDefault().matchingList(null, null, null, null, null, null, id.getText());
394        boolean oops = false;
395        for (RosterEntry rosterEntry : l) {
396            if (re != rosterEntry) {
397                oops = true;
398                break;
399            }
400        }
401        return oops;
402    }
403
404    /**
405     * Fill a RosterEntry object from GUI contents.
406     *
407     * @param r the roster entry to display
408     */
409    public void update(RosterEntry r) {
410        r.setId(id.getText());
411        r.setRoadName(roadName.getText());
412        r.setRoadNumber(roadNumber.getText());
413        r.setMfg(mfg.getText());
414        r.setOwner(owner.getText());
415        r.setModel(model.getText());
416        DccLocoAddress a = addrSel.getAddress();
417        if (a != null) {
418            r.setDccAddress("" + a.getNumber());
419            r.setProtocol(a.getProtocol());
420        }
421        r.setComment(comment.getText());
422
423        maxSet = (Double) maxSpeedSpinner.getValue();
424        log.debug("maxSet saved: {}", maxSet);
425        r.setMaxSpeedPCT((int) Math.round(100 * maxSet));
426        log.debug("maxSet read from config: {}", r.getMaxSpeedPCT());
427        r.setDecoderFamily(decoderFamily.getText());
428        r.setDecoderModel(decoderModel.getText());
429        r.setDecoderComment(decoderComment.getText());
430    }
431
432    /**
433     * Fill GUI from roster contents.
434     *
435     * @param r the roster entry to display
436     */
437    public void updateGUI(RosterEntry r) {
438        roadName.setText(r.getRoadName());
439        roadNumber.setText(r.getRoadNumber());
440        mfg.setText(r.getMfg());
441        owner.setText(r.getOwner());
442        model.setText(r.getModel());
443        comment.setText(r.getComment());
444        decoderModel.setText(r.getDecoderModel());
445        decoderFamily.setText(r.getDecoderFamily());
446        decoderProgModes.setText(r.getProgrammingModes());
447        decoderComment.setText(r.getDecoderComment());
448        dateUpdated.setText((r.getDateModified() != null)
449                ? DateFormat.getDateTimeInstance().format(r.getDateModified())
450                : r.getDateUpdated());
451        // retrieve MaxSpeed from r
452        double maxSpeedSet = r.getMaxSpeedPCT() / 100d; // why resets to 100?
453        log.debug("Max Speed set to: {}", maxSpeedSet);
454        maxSpeedSpinner.setValue(maxSpeedSet);
455        log.debug("Max Speed in spinner: {}", maxSpeedSpinner.getValue());
456    }
457
458    public void setDccAddress(String a) {
459        DccLocoAddress addr = addrSel.getAddress();
460        LocoAddress.Protocol protocol = addr.getProtocol();
461        try {
462            addrSel.setAddress(new DccLocoAddress(Integer.parseInt(a), protocol));
463        } catch (NumberFormatException e) {
464            log.error("Can't set DccAddress to {}", a);
465        }
466    }
467
468    public void setDccAddressLong(boolean m) {
469        DccLocoAddress addr = addrSel.getAddress();
470        int n = 0;
471        if (addr != null) {
472            //If the protocol is already set to something other than DCC, then do not try to configure it as DCC long or short.
473            if (addr.getProtocol() != LocoAddress.Protocol.DCC_LONG
474                    && addr.getProtocol() != LocoAddress.Protocol.DCC_SHORT
475                    && addr.getProtocol() != LocoAddress.Protocol.DCC) {
476                return;
477            }
478            n = addr.getNumber();
479        }
480        addrSel.setAddress(new DccLocoAddress(n, m));
481    }
482
483    public void dispose() {
484        log.debug("dispose");
485    }
486
487    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterEntryPane.class);
488
489}