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
038    JTextField roadNumber = new JTextField(30);
039    JTextField mfg = new JTextField(30);
040    JTextField model = new JTextField(30);
041    JTextField owner = new JTextField(30);
042    DccLocoAddressSelector addrSel = new DccLocoAddressSelector();
043
044    JTextArea comment = new JTextArea(3, 50);
045    public String getComment() {return comment.getText();}
046    public void setComment(String text) {comment.setText(text);}
047    public Document getCommentDocument() {return comment.getDocument();}
048    
049    // JScrollPanes are defined with scroll bars on always to avoid undesirable resizing behavior
050    // Without this the field will shrink to minimum size any time the scroll bars become needed and
051    // the scroll bars are inside, not outside the field area, obscuring their contents.
052    // This way the shrinking does not happen and the scroll bars are outside the field area,
053    // leaving the contents visible
054    JScrollPane commentScroller = new JScrollPane(comment, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
055    JLabel dateUpdated = new JLabel();
056    JLabel decoderModel = new JLabel();
057    JLabel decoderFamily = new JLabel();
058    JLabel decoderProgModes = new JLabel();
059    JTextArea decoderComment = new JTextArea(3, 50);
060    JScrollPane decoderCommentScroller = new JScrollPane(decoderComment, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
061
062    Component pane;
063    RosterEntry re;
064
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        JLabel row0Label = new JLabel(Bundle.getMessage("FieldID") + ":");
171        id.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldID"));
172        gbLayout.setConstraints(row0Label, cL);
173        super.add(row0Label);
174
175        cR.gridx = 1;
176        cR.gridy = 0;
177        cR.anchor = GridBagConstraints.WEST;
178        id.setMinimumSize(minFieldDim);
179        gbLayout.setConstraints(id, cR);
180        super.add(id);
181
182        cL.gridy++;
183        JLabel row1Label = new JLabel(Bundle.getMessage("FieldRoadName") + ":");
184        roadName.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldRoadName"));
185        gbLayout.setConstraints(row1Label, cL);
186        super.add(row1Label);
187
188        cR.gridy = cL.gridy;
189        roadName.setMinimumSize(minFieldDim);
190        gbLayout.setConstraints(roadName, cR);
191        super.add(roadName);
192
193        cL.gridy++;
194        JLabel row2Label = new JLabel(Bundle.getMessage("FieldRoadNumber") + ":");
195        roadNumber.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldRoadNumber"));
196        gbLayout.setConstraints(row2Label, cL);
197        super.add(row2Label);
198
199        cR.gridy = cL.gridy;
200        roadNumber.setMinimumSize(minFieldDim);
201        gbLayout.setConstraints(roadNumber, cR);
202        super.add(roadNumber);
203
204        cL.gridy++;
205        JLabel row3Label = new JLabel(Bundle.getMessage("FieldManufacturer") + ":");
206        mfg.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldManufacturer"));
207        gbLayout.setConstraints(row3Label, cL);
208        super.add(row3Label);
209
210        cR.gridy = cL.gridy;
211        mfg.setMinimumSize(minFieldDim);
212        gbLayout.setConstraints(mfg, cR);
213        super.add(mfg);
214
215        cL.gridy++;
216        JLabel row4Label = new JLabel(Bundle.getMessage("FieldOwner") + ":");
217        owner.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldOwner"));
218        gbLayout.setConstraints(row4Label, cL);
219        super.add(row4Label);
220
221        cR.gridy = cL.gridy;
222        owner.setMinimumSize(minFieldDim);
223        gbLayout.setConstraints(owner, cR);
224        super.add(owner);
225
226        cL.gridy++;
227        JLabel row5Label = new JLabel(Bundle.getMessage("FieldModel") + ":");
228        model.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldModel"));
229        gbLayout.setConstraints(row5Label, cL);
230        super.add(row5Label);
231
232        cR.gridy = cL.gridy;
233        model.setMinimumSize(minFieldDim);
234        gbLayout.setConstraints(model, cR);
235        super.add(model);
236
237        cL.gridy++;
238        JLabel row6Label = new JLabel(Bundle.getMessage("FieldDCCAddress") + ":");
239        selPanel.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldDCCAddress"));
240        gbLayout.setConstraints(row6Label, cL);
241        super.add(row6Label);
242
243        cR.gridy = cL.gridy;
244        gbLayout.setConstraints(selPanel, cR);
245        super.add(selPanel);
246
247        cL.gridy++;
248        JLabel row7Label = new JLabel(Bundle.getMessage("FieldSpeedLimit") + ":");
249        maxSpeedSpinner.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldSpeedLimit"));
250        gbLayout.setConstraints(row7Label, cL);
251        super.add(row7Label);
252
253        cR.gridy = cL.gridy; // JSpinner is initialised in RosterEntryPane()
254        gbLayout.setConstraints(maxSpeedSpinner, cR);
255        super.add(maxSpeedSpinner);
256
257        cL.gridy++;
258        JLabel row8Label = new JLabel(Bundle.getMessage("FieldComment") + ":");
259        // ensure same font on textarea as textfield
260        // as this is not true in all GUI types.
261        comment.setFont(owner.getFont());
262        commentScroller.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldComment"));
263        gbLayout.setConstraints(row8Label, cL);
264        super.add(row8Label);
265
266        cR.gridy = cL.gridy;
267        commentScroller.setMinimumSize(minScrollerDim);
268        gbLayout.setConstraints(commentScroller, cR);
269        super.add(commentScroller);
270
271        cL.gridy++;
272        JLabel row9Label = new JLabel(Bundle.getMessage("FieldDecoderFamily") + ":");
273        decoderFamily.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldDecoderFamily"));
274        gbLayout.setConstraints(row9Label, cL);
275        super.add(row9Label);
276
277        cR.gridy = cL.gridy;
278        decoderFamily.setMinimumSize(minFieldDim);
279        gbLayout.setConstraints(decoderFamily, cR);
280        super.add(decoderFamily);
281
282        cL.gridy++;
283        JLabel row10Label = new JLabel(Bundle.getMessage("FieldDecoderModel") + ":");
284        decoderModel.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldDecoderModel"));
285        gbLayout.setConstraints(row10Label, cL);
286        super.add(row10Label);
287
288        cR.gridy = cL.gridy;
289        decoderModel.setMinimumSize(minFieldDim);
290        gbLayout.setConstraints(decoderModel, cR);
291        super.add(decoderModel);
292
293        cL.gridy++;
294        JLabel row11Label = new JLabel(Bundle.getMessage("FieldDecoderModes") + ":");
295        decoderProgModes.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldDecoderModes"));
296        gbLayout.setConstraints(row11Label, cL);
297        super.add(row11Label);
298
299        cR.gridy = cL.gridy;
300        decoderProgModes.setMinimumSize(minFieldDim);
301        gbLayout.setConstraints(decoderProgModes, cR);
302        super.add(decoderProgModes);
303
304        cL.gridy++;
305        JLabel row12Label = new JLabel(Bundle.getMessage("FieldDecoderComment") + ":");
306        // ensure same font on textarea as textfield
307        // as this is not true in all GUI types.
308        decoderComment.setFont(owner.getFont());
309        decoderCommentScroller.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldDecoderComment"));
310        gbLayout.setConstraints(row12Label, cL);
311        super.add(row12Label);
312
313        cR.gridy = cL.gridy;
314        decoderCommentScroller.setMinimumSize(minScrollerDim);
315        gbLayout.setConstraints(decoderCommentScroller, cR);
316        super.add(decoderCommentScroller);
317
318        cL.gridy++;
319        JLabel row13Label = new JLabel(Bundle.getMessage("FieldDateUpdated") + ":");
320        dateUpdated.getAccessibleContext().setAccessibleName(Bundle.getMessage("FieldDateUpdated"));
321        gbLayout.setConstraints(row13Label, cL);
322        super.add(row13Label);
323
324        cR.gridy = cL.gridy;
325        dateUpdated.setMinimumSize(minFieldDim);
326        gbLayout.setConstraints(dateUpdated, cR);
327        super.add(dateUpdated);
328    }
329
330    double maxSet;
331
332    /**
333     * Do the GUI contents agree with a RosterEntry?
334     *
335     * @param r the entry to compare
336     * @return true if entry in GUI does not match r; false otherwise
337     */
338    public boolean guiChanged(RosterEntry r) {
339        if (!r.getRoadName().equals(roadName.getText())) {
340            return true;
341        }
342        if (!r.getRoadNumber().equals(roadNumber.getText())) {
343            return true;
344        }
345        if (!r.getMfg().equals(mfg.getText())) {
346            return true;
347        }
348        if (!r.getOwner().equals(owner.getText())) {
349            return true;
350        }
351        if (!r.getModel().equals(model.getText())) {
352            return true;
353        }
354        if (!r.getComment().equals(comment.getText())) {
355            return true;
356        }
357        if (!r.getDecoderFamily().equals(decoderFamily.getText())) {
358            return true;
359        }
360        if (!r.getDecoderModel().equals(decoderModel.getText())) {
361            return true;
362        }
363        if (!r.getProgrammingModes().equals(decoderProgModes.getText())) {
364            return true;
365        }
366        if (!r.getDecoderComment().equals(decoderComment.getText())) {
367            return true;
368        }
369        if (!r.getId().equals(id.getText())) {
370            return true;
371        }
372        maxSet = (Double) maxSpeedSpinner.getValue();
373        if (r.getMaxSpeedPCT() != (int) Math.round(100 * maxSet)) {
374            log.debug("check: {}|{}", r.getMaxSpeedPCT(), (int) Math.round(100 * maxSet));
375            return true;
376        }
377        DccLocoAddress a = addrSel.getAddress();
378        if (a == null) {
379            return !r.getDccAddress().isEmpty();
380        } else {
381            if (r.getProtocol() != a.getProtocol()) {
382                return true;
383            }
384            return !r.getDccAddress().equals("" + a.getNumber());
385        }
386    }
387
388    /**
389     *
390     * @return true if the value in the id JTextField is a duplicate of some
391     *         other RosterEntry in the roster
392     */
393    public boolean checkDuplicate() {
394        // check it's not a duplicate
395        List<RosterEntry> l = Roster.getDefault().matchingList(null, null, null, null, null, null, id.getText());
396        boolean oops = false;
397        for (RosterEntry rosterEntry : l) {
398            if (re != rosterEntry) {
399                oops = true;
400                break;
401            }
402        }
403        return oops;
404    }
405
406    /**
407     * Fill a RosterEntry object from GUI contents.
408     *
409     * @param r the roster entry to display
410     */
411    public void update(RosterEntry r) {
412        r.setId(id.getText());
413        r.setRoadName(roadName.getText());
414        r.setRoadNumber(roadNumber.getText());
415        r.setMfg(mfg.getText());
416        r.setOwner(owner.getText());
417        r.setModel(model.getText());
418        DccLocoAddress a = addrSel.getAddress();
419        if (a != null) {
420            r.setDccAddress("" + a.getNumber());
421            r.setProtocol(a.getProtocol());
422        }
423        r.setComment(comment.getText());
424
425        maxSet = (Double) maxSpeedSpinner.getValue();
426        log.debug("maxSet saved: {}", maxSet);
427        r.setMaxSpeedPCT((int) Math.round(100 * maxSet));
428        log.debug("maxSet read from config: {}", r.getMaxSpeedPCT());
429        r.setDecoderFamily(decoderFamily.getText());
430        r.setDecoderModel(decoderModel.getText());
431        r.setDecoderComment(decoderComment.getText());
432    }
433
434    /**
435     * Fill GUI from roster contents.
436     *
437     * @param r the roster entry to display
438     */
439    public void updateGUI(RosterEntry r) {
440        roadName.setText(r.getRoadName());
441        roadNumber.setText(r.getRoadNumber());
442        mfg.setText(r.getMfg());
443        owner.setText(r.getOwner());
444        model.setText(r.getModel());
445        comment.setText(r.getComment());
446        decoderModel.setText(r.getDecoderModel());
447        decoderFamily.setText(r.getDecoderFamily());
448        decoderProgModes.setText(r.getProgrammingModes());
449        decoderComment.setText(r.getDecoderComment());
450        dateUpdated.setText((r.getDateModified() != null)
451                ? DateFormat.getDateTimeInstance().format(r.getDateModified())
452                : r.getDateUpdated());
453        // retrieve MaxSpeed from r
454        double maxSpeedSet = r.getMaxSpeedPCT() / 100d; // why resets to 100?
455        log.debug("Max Speed set to: {}", maxSpeedSet);
456        maxSpeedSpinner.setValue(maxSpeedSet);
457        log.debug("Max Speed in spinner: {}", maxSpeedSpinner.getValue());
458    }
459
460    public void setDccAddress(String a) {
461        DccLocoAddress addr = addrSel.getAddress();
462        LocoAddress.Protocol protocol = addr.getProtocol();
463        try {
464            addrSel.setAddress(new DccLocoAddress(Integer.parseInt(a), protocol));
465        } catch (NumberFormatException e) {
466            log.error("Can't set DccAddress to {}", a);
467        }
468    }
469
470    public void setDccAddressLong(boolean m) {
471        DccLocoAddress addr = addrSel.getAddress();
472        int n = 0;
473        if (addr != null) {
474            //If the protocol is already set to something other than DCC, then do not try to configure it as DCC long or short.
475            if (addr.getProtocol() != LocoAddress.Protocol.DCC_LONG
476                    && addr.getProtocol() != LocoAddress.Protocol.DCC_SHORT
477                    && addr.getProtocol() != LocoAddress.Protocol.DCC) {
478                return;
479            }
480            n = addr.getNumber();
481        }
482        addrSel.setAddress(new DccLocoAddress(n, m));
483    }
484
485    public void dispose() {
486        log.debug("dispose");
487    }
488
489    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterEntryPane.class);
490
491}