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