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