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}