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}