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