001package jmri.jmrit.beantable.turnout; 002 003import java.awt.Color; 004import java.awt.Component; 005import java.awt.Image; 006import java.awt.Rectangle; 007import java.awt.event.ActionEvent; 008import java.awt.event.ActionListener; 009import java.awt.event.MouseAdapter; 010import java.awt.event.MouseEvent; 011import java.awt.image.BufferedImage; 012import java.io.File; 013import java.io.IOException; 014 015import javax.annotation.CheckForNull; 016import javax.annotation.Nonnull; 017import javax.imageio.ImageIO; 018import javax.swing.*; 019import javax.swing.table.TableCellEditor; 020import javax.swing.table.TableCellRenderer; 021import javax.swing.table.TableColumn; 022import javax.swing.table.TableModel; 023 024import jmri.*; 025import jmri.implementation.SignalSpeedMap; 026import jmri.jmrit.beantable.*; 027import jmri.util.swing.*; 028 029/** 030 * Data model for a Turnout Table. 031 * Code originally within TurnoutTableAction. 032 * 033 * @author Bob Jacobsen Copyright (C) 2003, 2004, 2007 034 * @author Egbert Broerse Copyright (C) 2017 035 * @author Steve Young Copyright (C) 2021 036 */ 037public class TurnoutTableDataModel extends BeanTableDataModel<Turnout>{ 038 039 public static final int INVERTCOL = BeanTableDataModel.NUMCOLUMN; 040 public static final int LOCKCOL = INVERTCOL + 1; 041 public static final int EDITCOL = LOCKCOL + 1; 042 public static final int KNOWNCOL = EDITCOL + 1; 043 public static final int MODECOL = KNOWNCOL + 1; 044 public static final int SENSOR1COL = MODECOL + 1; 045 public static final int SENSOR2COL = SENSOR1COL + 1; 046 public static final int OPSONOFFCOL = SENSOR2COL + 1; 047 public static final int OPSEDITCOL = OPSONOFFCOL + 1; 048 public static final int LOCKOPRCOL = OPSEDITCOL + 1; 049 public static final int LOCKDECCOL = LOCKOPRCOL + 1; 050 public static final int STRAIGHTCOL = LOCKDECCOL + 1; 051 public static final int DIVERGCOL = STRAIGHTCOL + 1; 052 public static final int FORGETCOL = DIVERGCOL + 1; 053 public static final int QUERYCOL = FORGETCOL + 1; 054 055 private boolean _graphicState; 056 private TurnoutManager turnoutManager; 057 058 059 String closedText; 060 String thrownText; 061 public String defaultThrownSpeedText; 062 public String defaultClosedSpeedText; 063 // I18N TODO but note storing in xml independent from Locale 064 String useBlockSpeed; 065 String bothText = "Both"; 066 String cabOnlyText = "Cab only"; 067 String pushbutText = "Pushbutton only"; 068 String noneText = "None"; 069 070 public final java.util.Vector<String> speedListClosed = new java.util.Vector<>(); 071 public final java.util.Vector<String> speedListThrown = new java.util.Vector<>(); 072 073 074 public TurnoutTableDataModel(){ 075 super(); 076 initTable(); 077 } 078 079 public TurnoutTableDataModel(Manager<Turnout> mgr){ 080 super(); 081 setManager(mgr); 082 initTable(); 083 } 084 085 private void initTable() { 086 087 // load graphic state column display preference 088 _graphicState = InstanceManager.getDefault(jmri.util.gui.GuiLafPreferencesManager.class).isGraphicTableState(); 089 090 closedText = turnoutManager.getClosedText(); 091 thrownText = turnoutManager.getThrownText(); 092 093 //This following must contain the word Global for a correct match in the abstract turnout 094 defaultThrownSpeedText = (Bundle.getMessage("UseGlobal", "Global") + " " + turnoutManager.getDefaultThrownSpeed()); 095 defaultClosedSpeedText = (Bundle.getMessage("UseGlobal", "Global") + " " + turnoutManager.getDefaultClosedSpeed()); 096 097 //This following must contain the word Block for a correct match in the abstract turnout 098 useBlockSpeed = Bundle.getMessage("UseGlobal", "Block Speed"); 099 100 speedListClosed.add(defaultClosedSpeedText); 101 speedListThrown.add(defaultThrownSpeedText); 102 speedListClosed.add(useBlockSpeed); 103 speedListThrown.add(useBlockSpeed); 104 java.util.Vector<String> _speedMap = jmri.InstanceManager.getDefault(SignalSpeedMap.class).getValidSpeedNames(); 105 for (String s : _speedMap) { 106 if (!speedListClosed.contains(s)) { 107 speedListClosed.add(s); 108 } 109 if (!speedListThrown.contains(s)) { 110 speedListThrown.add(s); 111 } 112 } 113 114 } 115 116 /** 117 * {@inheritDoc} 118 */ 119 @Override 120 public int getColumnCount() { 121 return QUERYCOL + getPropertyColumnCount() + 1; 122 } 123 124 /** 125 * {@inheritDoc} 126 */ 127 @Override 128 public String getColumnName(int col) { 129 switch (col) { 130 case INVERTCOL: 131 return Bundle.getMessage("Inverted"); 132 case LOCKCOL: 133 return Bundle.getMessage("Locked"); 134 case KNOWNCOL: 135 return Bundle.getMessage("Feedback"); 136 case MODECOL: 137 return Bundle.getMessage("ModeLabel"); 138 case SENSOR1COL: 139 return Bundle.getMessage("BlockSensor") + " 1"; 140 case SENSOR2COL: 141 return Bundle.getMessage("BlockSensor") + " 2"; 142 case OPSONOFFCOL: 143 return Bundle.getMessage("TurnoutAutomationMenu"); 144 case OPSEDITCOL: 145 return ""; 146 case LOCKOPRCOL: 147 return Bundle.getMessage("LockMode"); 148 case LOCKDECCOL: 149 return Bundle.getMessage("Decoder"); 150 case DIVERGCOL: 151 return Bundle.getMessage("ThrownSpeed"); 152 case STRAIGHTCOL: 153 return Bundle.getMessage("ClosedSpeed"); 154 case FORGETCOL: 155 return Bundle.getMessage("StateForgetHeader"); 156 case QUERYCOL: 157 return Bundle.getMessage("StateQueryHeader"); 158 case EDITCOL: 159 return ""; 160 default: 161 return super.getColumnName(col); 162 } 163 } 164 165 /** 166 * {@inheritDoc} 167 */ 168 @Override 169 protected String getHeaderTooltip(int col) { 170 switch (col) { 171 case SENSOR1COL: 172 return Bundle.getMessage("Sensor1Tip", turnoutManager.getThrownText()); 173 case SENSOR2COL: 174 return Bundle.getMessage("Sensor2Tip", turnoutManager.getClosedText()); 175 case OPSONOFFCOL: 176 return Bundle.getMessage("TurnoutAutomationTip"); 177 case KNOWNCOL: 178 return Bundle.getMessage("FeedbackTip"); 179 case MODECOL: 180 return Bundle.getMessage("FeedbackModeTip"); 181 default: 182 return super.getHeaderTooltip(col); 183 } 184 } 185 186 /** 187 * {@inheritDoc} 188 */ 189 @Override 190 public Class<?> getColumnClass(int col) { 191 switch (col) { 192 case INVERTCOL: 193 case LOCKCOL: 194 return Boolean.class; 195 case KNOWNCOL: 196 return String.class; 197 case MODECOL: 198 case SENSOR1COL: 199 case SENSOR2COL: 200 case OPSONOFFCOL: 201 case LOCKOPRCOL: 202 case LOCKDECCOL: 203 case DIVERGCOL: 204 case STRAIGHTCOL: 205 return JComboBox.class; 206 case OPSEDITCOL: 207 case EDITCOL: 208 case FORGETCOL: 209 case QUERYCOL: 210 return JButton.class; 211 case VALUECOL: // may use an image to show turnout state 212 return ( _graphicState ? JLabel.class : JButton.class ); 213 default: 214 return super.getColumnClass(col); 215 } 216 } 217 218 /** 219 * {@inheritDoc} 220 */ 221 @Override 222 public int getPreferredWidth(int col) { 223 switch (col) { 224 case INVERTCOL: 225 case LOCKCOL: 226 return new JTextField(6).getPreferredSize().width; 227 case LOCKOPRCOL: 228 case LOCKDECCOL: 229 case KNOWNCOL: 230 case MODECOL: 231 return new JTextField(10).getPreferredSize().width; 232 case SENSOR1COL: 233 case SENSOR2COL: 234 return new JTextField(5).getPreferredSize().width; 235 case OPSEDITCOL: 236 return new JButton(Bundle.getMessage("EditTurnoutOperation")).getPreferredSize().width; 237 case EDITCOL: 238 return new JButton(Bundle.getMessage("ButtonEdit")).getPreferredSize().width+4; 239 case OPSONOFFCOL: 240 return new JTextField(Bundle.getMessage("TurnoutAutomationMenu")).getPreferredSize().width; 241 case DIVERGCOL: 242 case STRAIGHTCOL: 243 return new JTextField(14).getPreferredSize().width; 244 case FORGETCOL: 245 return new JButton(Bundle.getMessage("StateForgetButton")).getPreferredSize().width; 246 case QUERYCOL: 247 return new JButton(Bundle.getMessage("StateQueryButton")).getPreferredSize().width; 248 default: 249 return super.getPreferredWidth(col); 250 } 251 } 252 253 /** 254 * {@inheritDoc} 255 */ 256 @Override 257 public boolean isCellEditable(int row, int col) { 258 Turnout t = turnoutManager.getBySystemName(sysNameList.get(row)); 259 if (t == null){ 260 return false; 261 } 262 switch (col) { 263 case INVERTCOL: 264 return t.canInvert(); 265 case LOCKCOL: 266 // checkbox disabled unless current configuration allows locking 267 return t.canLock(Turnout.CABLOCKOUT | Turnout.PUSHBUTTONLOCKOUT); 268 case OPSEDITCOL: 269 return t.getTurnoutOperation() != null; 270 case KNOWNCOL: 271 return false; 272 case MODECOL: 273 case SENSOR1COL: 274 case SENSOR2COL: 275 case OPSONOFFCOL: 276 case LOCKOPRCOL: // editable always so user can configure it, even if current configuration prevents locking now 277 case LOCKDECCOL: // editable always so user can configure it, even if current configuration prevents locking now 278 case DIVERGCOL: 279 case STRAIGHTCOL: 280 case EDITCOL: 281 case FORGETCOL: 282 case QUERYCOL: 283 return true; 284 default: 285 return super.isCellEditable(row, col); 286 } 287 } 288 289 /** 290 * {@inheritDoc} 291 */ 292 @Override 293 public Object getValueAt(int row, int col) { 294 // some error checking 295 if (row >= sysNameList.size()) { 296 log.warn("row is greater than name list"); 297 return "error"; 298 } 299 String name = sysNameList.get(row); 300 TurnoutManager manager = turnoutManager; 301 Turnout t = manager.getBySystemName(name); 302 if (t == null) { 303 log.debug("error null turnout!"); 304 return "error"; 305 } 306 if (col == INVERTCOL) { 307 return t.getInverted(); 308 } else if (col == LOCKCOL) { 309 return t.getLocked(Turnout.CABLOCKOUT | Turnout.PUSHBUTTONLOCKOUT); 310 } else if (col == KNOWNCOL) { 311 return t.describeState(t.getKnownState()); 312 } else if (col == MODECOL) { 313 JComboBox<String> c = new JComboBox<>(t.getValidFeedbackNames()); 314 c.setSelectedItem(t.getFeedbackModeName()); 315 return c; 316 } else if (col == SENSOR1COL) { 317 return t.getFirstSensor(); 318 } else if (col == SENSOR2COL) { 319 return t.getSecondSensor(); 320 } else if (col == OPSONOFFCOL) { 321 return makeAutomationBox(t); 322 } else if (col == OPSEDITCOL) { 323 return Bundle.getMessage("EditTurnoutOperation"); 324 } else if (col == EDITCOL) { 325 return Bundle.getMessage("ButtonEdit"); 326 } else if (col == LOCKDECCOL) { 327 JComboBox<String> c; 328 if ((t.getPossibleLockModes() & Turnout.PUSHBUTTONLOCKOUT) != 0) { 329 c = new JComboBox<>(t.getValidDecoderNames()); 330 } else { 331 c = new JComboBox<>(new String[]{t.getDecoderName()}); 332 } 333 334 c.setSelectedItem(t.getDecoderName()); 335 return c; 336 } else if (col == LOCKOPRCOL) { 337 338 java.util.Vector<String> lockOperations = new java.util.Vector<>(); // Vector is a JComboBox ctor; List is not 339 int modes = t.getPossibleLockModes(); 340 if ((modes & Turnout.CABLOCKOUT) != 0 && (modes & Turnout.PUSHBUTTONLOCKOUT) != 0) { 341 lockOperations.add(bothText); 342 } 343 if ((modes & Turnout.CABLOCKOUT) != 0) { 344 lockOperations.add(cabOnlyText); 345 } 346 if ((modes & Turnout.PUSHBUTTONLOCKOUT) != 0) { 347 lockOperations.add(pushbutText); 348 } 349 lockOperations.add(noneText); 350 JComboBox<String> c = new JComboBox<>(lockOperations); 351 352 if (t.canLock(Turnout.CABLOCKOUT) && t.canLock(Turnout.PUSHBUTTONLOCKOUT)) { 353 c.setSelectedItem(bothText); 354 } else if (t.canLock(Turnout.PUSHBUTTONLOCKOUT)) { 355 c.setSelectedItem(pushbutText); 356 } else if (t.canLock(Turnout.CABLOCKOUT)) { 357 c.setSelectedItem(cabOnlyText); 358 } else { 359 c.setSelectedItem(noneText); 360 } 361 return c; 362 } else if (col == STRAIGHTCOL) { 363 364 String speed = t.getStraightSpeed(); 365 if (!speedListClosed.contains(speed)) { 366 speedListClosed.add(speed); 367 } 368 JComboBox<String> c = new JComboBox<>(speedListClosed); 369 c.setEditable(true); 370 c.setSelectedItem(speed); 371 JComboBoxUtil.setupComboBoxMaxRows(c); 372 return c; 373 } else if (col == DIVERGCOL) { 374 375 String speed = t.getDivergingSpeed(); 376 if (!speedListThrown.contains(speed)) { 377 speedListThrown.add(speed); 378 } 379 JComboBox<String> c = new JComboBox<>(speedListThrown); 380 c.setEditable(true); 381 c.setSelectedItem(speed); 382 JComboBoxUtil.setupComboBoxMaxRows(c); 383 return c; 384 // } else if (col == VALUECOL && _graphicState) { // not neeeded as the 385 // graphic ImageIconRenderer uses the same super.getValueAt(row, col) as 386 // classic bean state text button 387 } else if (col == FORGETCOL) { 388 return Bundle.getMessage("StateForgetButton"); 389 } else if (col == QUERYCOL) { 390 return Bundle.getMessage("StateQueryButton"); 391 } 392 return super.getValueAt(row, col); 393 } 394 395 /** 396 * {@inheritDoc} 397 */ 398 @Override 399 public void setValueAt(Object value, int row, int col) { 400 String name = sysNameList.get(row); 401 Turnout t = turnoutManager.getBySystemName(name); 402 if (t == null) { 403 NullPointerException ex = new NullPointerException("Unexpected null turnout in turnout table"); 404 log.error("No Turnout with system name \"{}\" exists ", name , ex); // log with stack trace 405 throw ex; 406 } 407 if (col == INVERTCOL) { 408 if (t.canInvert()) { 409 t.setInverted((Boolean) value); 410 } 411 } else if (col == LOCKCOL) { 412 t.setLocked(Turnout.CABLOCKOUT | Turnout.PUSHBUTTONLOCKOUT, (Boolean) value); 413 } else if (col == MODECOL) { 414 @SuppressWarnings("unchecked") 415 String modeName = (String) ((JComboBox<String>) value).getSelectedItem(); 416 assert modeName != null; 417 t.setFeedbackMode(modeName); 418 } else if (col == SENSOR1COL) { 419 try { 420 Sensor sensor = (Sensor) value; 421 t.provideFirstFeedbackSensor(sensor != null ? sensor.getDisplayName() : null); 422 } catch (jmri.JmriException e) { 423 JmriJOptionPane.showMessageDialog(null, e.toString()); 424 } 425 } else if (col == SENSOR2COL) { 426 try { 427 Sensor sensor = (Sensor) value; 428 t.provideSecondFeedbackSensor(sensor != null ? sensor.getDisplayName() : null); 429 } catch (jmri.JmriException e) { 430 JmriJOptionPane.showMessageDialog(null, e.toString()); 431 } 432 } else if (col == OPSONOFFCOL) { 433 // do nothing as this is handled by the combo box listener 434 // column still handled here to prevent call to super.setValueAt 435 } else if (col == OPSEDITCOL) { 436 t.setInhibitOperation(false); 437 @SuppressWarnings("unchecked") // cast to JComboBox<String> required in OPSEDITCOL 438 JComboBox<String> cb = (JComboBox<String>) getValueAt(row, OPSONOFFCOL); 439 log.debug("opsSelected = {}", getValueAt(row, OPSONOFFCOL).toString()); 440 editTurnoutOperation(t, cb); 441 fireTableRowsUpdated(row, row); 442 } else if (col == EDITCOL) { 443 javax.swing.SwingUtilities.invokeLater(() -> { 444 editButton(t); 445 }); 446 } else if (col == LOCKOPRCOL) { 447 @SuppressWarnings("unchecked") 448 String lockOpName = (String) ((JComboBox<String>) value) 449 .getSelectedItem(); 450 assert lockOpName != null; 451 if (lockOpName.equals(bothText)) { 452 t.enableLockOperation(Turnout.CABLOCKOUT | Turnout.PUSHBUTTONLOCKOUT, true); 453 } 454 if (lockOpName.equals(cabOnlyText)) { 455 t.enableLockOperation(Turnout.CABLOCKOUT, true); 456 t.enableLockOperation(Turnout.PUSHBUTTONLOCKOUT, false); 457 } 458 if (lockOpName.equals(pushbutText)) { 459 t.enableLockOperation(Turnout.CABLOCKOUT, false); 460 t.enableLockOperation(Turnout.PUSHBUTTONLOCKOUT, true); 461 } 462 fireTableRowsUpdated(row, row); 463 } else if (col == LOCKDECCOL) { 464 @SuppressWarnings("unchecked") 465 String decoderName = (String) ((JComboBox<String>) value).getSelectedItem(); 466 t.setDecoderName(decoderName); 467 fireTableRowsUpdated(row, row); 468 } else if (col == STRAIGHTCOL) { 469 @SuppressWarnings("unchecked") 470 String speed = (String) ((JComboBox<String>) value).getSelectedItem(); 471 try { 472 t.setStraightSpeed(speed); 473 } catch (jmri.JmriException ex) { 474 JmriJOptionPane.showMessageDialog(null, ex.getMessage() + "\n" + speed); 475 return; 476 } 477 if ((!speedListClosed.contains(speed))) { 478 assert speed != null; 479 if (!speed.contains("Global")) { 480 speedListClosed.add(speed); 481 } 482 } 483 } else if (col == DIVERGCOL) { 484 485 @SuppressWarnings("unchecked") 486 String speed = (String) ((JComboBox<String>) value).getSelectedItem(); 487 try { 488 t.setDivergingSpeed(speed); 489 } catch (jmri.JmriException ex) { 490 JmriJOptionPane.showMessageDialog(null, ex.getMessage() + "\n" + speed); 491 return; 492 } 493 if ((!speedListThrown.contains(speed))) { 494 assert speed != null; 495 if (!speed.contains("Global")) { 496 speedListThrown.add(speed); 497 } 498 } 499 } else if (col == FORGETCOL) { 500 t.setCommandedState(Turnout.UNKNOWN); 501 } else if (col == QUERYCOL) { 502 t.setCommandedState(Turnout.UNKNOWN); 503 t.requestUpdateFromLayout(); 504 } else if (col == VALUECOL && _graphicState) { // respond to clicking on ImageIconRenderer CellEditor 505 clickOn(t); 506 fireTableRowsUpdated(row, row); 507 } else { 508 super.setValueAt(value, row, col); 509 if (row < getRowCount()) { 510 fireTableRowsUpdated(row, row); 511 } 512 } 513 } 514 515 /** 516 * {@inheritDoc} 517 */ 518 @Override 519 public String getValue(@Nonnull String name) { 520 Turnout turn = turnoutManager.getBySystemName(name); 521 if (turn != null) { 522 return turn.describeState(turn.getCommandedState()); 523 } 524 return "Turnout not found"; 525 } 526 527 /** 528 * {@inheritDoc} 529 */ 530 @Override 531 public Manager<Turnout> getManager() { 532 if (turnoutManager == null) { 533 turnoutManager = InstanceManager.getDefault(TurnoutManager.class); 534 } 535 return turnoutManager; 536 } 537 538 /** 539 * {@inheritDoc} 540 */ 541 @Override 542 protected final void setManager(@Nonnull Manager<Turnout> manager) { 543 if (!(manager instanceof TurnoutManager)) { 544 return; 545 } 546 getManager().removePropertyChangeListener(this); 547 if (sysNameList != null) { 548 for (int i = 0; i < sysNameList.size(); i++) { 549 // if object has been deleted, it's not here; ignore it 550 NamedBean b = getBySystemName(sysNameList.get(i)); 551 if (b != null) { 552 b.removePropertyChangeListener(this); 553 } 554 } 555 } 556 turnoutManager = (TurnoutManager) manager; 557 getManager().addPropertyChangeListener(this); 558 updateNameList(); 559 } 560 561 @Override 562 public Turnout getBySystemName(@Nonnull String name) { 563 return turnoutManager.getBySystemName(name); 564 } 565 566 @Override 567 public Turnout getByUserName(@Nonnull String name) { 568 return InstanceManager.getDefault(TurnoutManager.class).getByUserName(name); 569 } 570 571 @Override 572 protected String getMasterClassName() { 573 // Force message grouping 574 return jmri.jmrit.beantable.TurnoutTableAction.class.getName(); 575 } 576 577 protected String getClassName() { 578 return jmri.jmrit.beantable.TurnoutTableAction.class.getName(); 579 } 580 581 @Override 582 public void clickOn(Turnout t) { 583 t.setCommandedState( t.getCommandedState()== Turnout.CLOSED ? Turnout.THROWN : Turnout.CLOSED); 584 } 585 586 @Override 587 public void configureTable(JTable tbl) { 588 589 setColumnToHoldButton(tbl, EDITCOL, editButton()); 590 setColumnToHoldButton(tbl, OPSEDITCOL, editButton()); 591 592 //Hide the following columns by default 593 XTableColumnModel columnModel = (XTableColumnModel) tbl.getColumnModel(); 594 TableColumn column = columnModel.getColumnByModelIndex(STRAIGHTCOL); 595 columnModel.setColumnVisible(column, false); 596 column = columnModel.getColumnByModelIndex(DIVERGCOL); 597 columnModel.setColumnVisible(column, false); 598 column = columnModel.getColumnByModelIndex(KNOWNCOL); 599 columnModel.setColumnVisible(column, false); 600 column = columnModel.getColumnByModelIndex(MODECOL); 601 columnModel.setColumnVisible(column, false); 602 column = columnModel.getColumnByModelIndex(SENSOR1COL); 603 columnModel.setColumnVisible(column, false); 604 column = columnModel.getColumnByModelIndex(SENSOR2COL); 605 columnModel.setColumnVisible(column, false); 606 column = columnModel.getColumnByModelIndex(OPSONOFFCOL); 607 columnModel.setColumnVisible(column, false); 608 column = columnModel.getColumnByModelIndex(OPSEDITCOL); 609 columnModel.setColumnVisible(column, false); 610 column = columnModel.getColumnByModelIndex(LOCKOPRCOL); 611 columnModel.setColumnVisible(column, false); 612 column = columnModel.getColumnByModelIndex(LOCKDECCOL); 613 columnModel.setColumnVisible(column, false); 614 column = columnModel.getColumnByModelIndex(FORGETCOL); 615 columnModel.setColumnVisible(column, false); 616 column = columnModel.getColumnByModelIndex(QUERYCOL); 617 columnModel.setColumnVisible(column, false); 618 619 620 // and then set user prefs 621 super.configureTable(tbl); 622 623 if (_graphicState) { 624 // make the rows tall enough for the state icons, once and outside of the 625 // renderer; must come after super.configureTable, which sets the row 626 // height for the buttons 627 tbl.setRowHeight(Math.max(tbl.getRowHeight(), ImageIconRenderer.getIconRowHeight())); 628 } 629 630 columnModel.getColumnByModelIndex(FORGETCOL).setHeaderValue(null); 631 columnModel.getColumnByModelIndex(QUERYCOL).setHeaderValue(null); 632 633 } 634 635 // update table if turnout lock or feedback changes 636 @Override 637 protected boolean matchPropertyName(java.beans.PropertyChangeEvent e) { 638 switch (e.getPropertyName()) { 639 case Turnout.PROPERTY_LOCKED: 640 case Turnout.PROPERTY_INVERTED: 641 case Turnout.PROPERTY_FEEDBACK_MODE: // feedback type setting change, NOT Turnout feedback status 642 case Turnout.PROPERTY_TURNOUT_DIVERGING_SPEED: 643 case Turnout.PROPERTY_TURNOUT_STRAIGHT_SPEED: 644 case Turnout.PROPERTY_TURNOUT_FEEDBACK_FIRST_SENSOR: 645 case Turnout.PROPERTY_TURNOUT_FEEDBACK_SECOND_SENSOR: 646 case Turnout.PROPERTY_DECODER_NAME: 647 case Turnout.PROPERTY_TURNOUT_OPERATION_STATE: 648 case Turnout.PROPERTY_KNOWN_STATE: 649 case Turnout.PROPERTY_COMMANDED_STATE: 650 return true; 651 default: 652 return super.matchPropertyName(e); 653 } 654 } 655 656 @Override 657 public void propertyChange(java.beans.PropertyChangeEvent e) { 658 switch (e.getPropertyName()) { 659 case TurnoutManager.PROPERTY_DEFAULT_CLOSED_SPEED: 660 updateClosedList(); 661 break; 662 case TurnoutManager.PROPERTY_DEFAULT_THROWN_SPEED: 663 updateThrownList(); 664 break; 665 default: 666 super.propertyChange(e); 667 break; 668 } 669 } 670 671 /** 672 * Customize the turnout table Value (State) column to show an 673 * appropriate graphic for the turnout state if _graphicState = 674 * true, or (default) just show the localized state text when the 675 * TableDataModel is being called from ListedTableAction. 676 * 677 * @param table a JTable of Turnouts 678 */ 679 @Override 680 protected void configValueColumn(JTable table) { 681 // have the value column hold a JPanel (icon) 682 //setColumnToHoldButton(table, VALUECOL, new JLabel("12345678")); // for larger, wide round icon, but cannot be converted to JButton 683 // add extras, override BeanTableDataModel 684 log.debug("Turnout configValueColumn (I am {})", super.toString()); 685 if (_graphicState) { // load icons, only once 686 table.setDefaultEditor(JLabel.class, new ImageIconRenderer(closedText, thrownText)); // editor 687 table.setDefaultRenderer(JLabel.class, new ImageIconRenderer(closedText, thrownText)); // item class copied from SwitchboardEditor panel 688 } else { 689 super.configValueColumn(table); // classic text style state indication 690 } 691 } 692 693 @Override 694 public JTable makeJTable(@Nonnull String name, @Nonnull TableModel model, @CheckForNull RowSorter<? extends TableModel> sorter) { 695 if (!(model instanceof TurnoutTableDataModel)){ 696 throw new IllegalArgumentException("Model is not a TurnoutTableDataModel"); 697 } 698 return configureJTable(name, new TurnoutTableJTable((TurnoutTableDataModel)model), sorter); 699 } 700 701 @Override 702 protected void setColumnIdentities(JTable table) { 703 super.setColumnIdentities(table); 704 java.util.Enumeration<TableColumn> columns; 705 if (table.getColumnModel() instanceof XTableColumnModel) { 706 columns = ((XTableColumnModel) table.getColumnModel()).getColumns(false); 707 } else { 708 columns = table.getColumnModel().getColumns(); 709 } 710 while (columns.hasMoreElements()) { 711 TableColumn column = columns.nextElement(); 712 switch (column.getModelIndex()) { 713 case FORGETCOL: 714 column.setIdentifier("ForgetState"); 715 break; 716 case QUERYCOL: 717 column.setIdentifier("QueryState"); 718 break; 719 case SENSOR1COL: 720 column.setIdentifier("Sensor1"); 721 break; 722 case SENSOR2COL: 723 column.setIdentifier("Sensor2"); 724 break; 725 default: 726 // use existing value 727 } 728 } 729 } 730 731 /** 732 * Pop up a TurnoutOperationConfig for the turnout. 733 * 734 * @param t turnout 735 * @param box JComboBox that triggered the edit 736 */ 737 protected void editTurnoutOperation( @Nonnull Turnout t, JComboBox<String> box) { 738 if (!editingOps.getAndSet(true)) { // don't open a second edit ops pane 739 TurnoutOperation op = t.getTurnoutOperation(); 740 if (op == null) { 741 TurnoutOperation proto = InstanceManager.getDefault(TurnoutOperationManager.class).getMatchingOperationAlways(t); 742 if (proto != null) { 743 op = proto.makeNonce(t); 744 t.setTurnoutOperation(op); 745 } 746 } 747 if (op != null) { 748 if (!op.isNonce()) { 749 op = op.makeNonce(t); 750 } 751 // make and show edit dialog 752 log.debug("TurnoutOpsEditDialog starting"); 753 java.awt.Window w = JmriJOptionPane.findWindowForObject(box); 754 TurnoutOperationEditorDialog dialog = new TurnoutOperationEditorDialog(op, t, w); 755 dialog.setVisible(true); 756 } else { 757 JmriJOptionPane.showMessageDialog(box, Bundle.getMessage("TurnoutOperationErrorDialog"), 758 Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE); 759 } 760 } 761 } 762 763 /** 764 * Create a {@literal JComboBox<String>} containing all the options for 765 * turnout automation parameters for this turnout. 766 * 767 * @param t the turnout 768 * @return the JComboBox 769 */ 770 protected JComboBox<String> makeAutomationBox(Turnout t) { 771 String[] str = new String[]{"empty"}; 772 final JComboBox<String> cb = new JComboBox<>(str); 773 final Turnout myTurnout = t; 774 TurnoutTableAction.updateAutomationBox(t, cb); 775 cb.addActionListener(new ActionListener() { 776 @Override 777 public void actionPerformed(ActionEvent e) { 778 setTurnoutOperation(myTurnout, cb); 779 cb.removeActionListener(this); // avoid recursion 780 TurnoutTableAction.updateAutomationBox(myTurnout, cb); 781 cb.addActionListener(this); 782 } 783 }); 784 return cb; 785 } 786 787 /** 788 * Set the turnout's operation info based on the contents of the combo box. 789 * 790 * @param t turnout being configured 791 * @param cb JComboBox for ops for t in the TurnoutTable 792 */ 793 protected void setTurnoutOperation( @Nonnull Turnout t, JComboBox<String> cb) { 794 switch (cb.getSelectedIndex()) { 795 case 0: // Off 796 t.setInhibitOperation(true); 797 t.setTurnoutOperation(null); 798 break; 799 case 1: // Default 800 t.setInhibitOperation(false); 801 t.setTurnoutOperation(null); 802 break; 803 default: // named operation 804 t.setInhibitOperation(false); 805 t.setTurnoutOperation(InstanceManager.getDefault(TurnoutOperationManager.class). 806 getOperation(((String) java.util.Objects.requireNonNull(cb.getSelectedItem())))); 807 break; 808 } 809 } 810 811 /** 812 * Create action to edit a turnout in Edit pane. (also used in windowTest) 813 * 814 * @param t the turnout to be edited 815 */ 816 void editButton(Turnout t) { 817 jmri.jmrit.beantable.beanedit.TurnoutEditAction beanEdit = new jmri.jmrit.beantable.beanedit.TurnoutEditAction(); 818 beanEdit.setBean(t); 819 beanEdit.actionPerformed(null); 820 } 821 822 /** 823 * Create a JButton to edit a turnout's operation. 824 * 825 * @return the JButton 826 */ 827 protected JButton editButton() { 828 return new JButton(Bundle.getMessage("EditTurnoutOperation")); 829 } 830 831 private void updateClosedList() { 832 speedListClosed.remove(defaultClosedSpeedText); 833 defaultClosedSpeedText = (Bundle.getMessage("UseGlobal", "Global") + " " + turnoutManager.getDefaultClosedSpeed()); 834 speedListClosed.add(0, defaultClosedSpeedText); 835 fireTableDataChanged(); 836 } 837 838 private void updateThrownList() { 839 speedListThrown.remove(defaultThrownSpeedText); 840 defaultThrownSpeedText = (Bundle.getMessage("UseGlobal", "Global") + " " + turnoutManager.getDefaultThrownSpeed()); 841 speedListThrown.add(0, defaultThrownSpeedText); 842 fireTableDataChanged(); 843 } 844 845 public void showFeedbackChanged(boolean visible, JTable table ) { 846 XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel(); 847 TableColumn column = columnModel.getColumnByModelIndex(KNOWNCOL); 848 columnModel.setColumnVisible(column, visible); 849 column = columnModel.getColumnByModelIndex(MODECOL); 850 columnModel.setColumnVisible(column, visible); 851 column = columnModel.getColumnByModelIndex(SENSOR1COL); 852 columnModel.setColumnVisible(column, visible); 853 column = columnModel.getColumnByModelIndex(SENSOR2COL); 854 columnModel.setColumnVisible(column, visible); 855 column = columnModel.getColumnByModelIndex(OPSONOFFCOL); 856 columnModel.setColumnVisible(column, visible); 857 column = columnModel.getColumnByModelIndex(OPSEDITCOL); 858 columnModel.setColumnVisible(column, visible); 859 } 860 861 public void showLockChanged(boolean visible, JTable table) { 862 XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel(); 863 TableColumn column = ((XTableColumnModel) table.getColumnModel()).getColumnByModelIndex(LOCKDECCOL); 864 columnModel.setColumnVisible(column, visible); 865 column = columnModel.getColumnByModelIndex(LOCKOPRCOL); 866 columnModel.setColumnVisible(column, visible); 867 } 868 869 public void showTurnoutSpeedChanged(boolean visible, JTable table) { 870 XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel(); 871 TableColumn column = ((XTableColumnModel) table.getColumnModel()).getColumnByModelIndex(STRAIGHTCOL); 872 columnModel.setColumnVisible(column, visible); 873 column = columnModel.getColumnByModelIndex(DIVERGCOL); 874 columnModel.setColumnVisible(column, visible); 875 } 876 877 public void showStateForgetAndQueryChanged(boolean visible, JTable table) { 878 XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel(); 879 TableColumn column = columnModel.getColumnByModelIndex(FORGETCOL); 880 columnModel.setColumnVisible(column, visible); 881 column = columnModel.getColumnByModelIndex(QUERYCOL); 882 columnModel.setColumnVisible(column, visible); 883 } 884 885 886 /** 887 * Visualize state in table as a graphic, customized for Turnouts (4 888 * states). 889 * Renderer and Editor are identical, as the cell contents 890 * are not actually edited, only used to toggle state using 891 * {@link #clickOn(Turnout)}. 892 * <p> 893 * A single label is reused for every cell and the icons are loaded once 894 * per JVM, so rendering a cell allocates nothing. The label ignores 895 * revalidate/repaint requests and the row height is set once in 896 * {@link #configureTable}: painting a cell must never schedule more 897 * painting, or the table repaints itself forever. 898 */ 899 static class ImageIconRenderer extends AbstractCellEditor implements TableCellEditor, TableCellRenderer { 900 901 private static final String ROOT_PATH = "resources/icons/misc/switchboard/"; // also used in display.switchboardEditor 902 private static final char BEAN_TYPE_CHAR = 'T'; // for Turnout 903 private static final String ON_ICON_PATH = ROOT_PATH + BEAN_TYPE_CHAR + "-on-s.png"; 904 private static final String OFF_ICON_PATH = ROOT_PATH + BEAN_TYPE_CHAR + "-off-s.png"; 905 private static ImageIcon onIcon = null; 906 private static ImageIcon offIcon = null; 907 private static int iconHeight = -1; 908 909 private final StateLabel label = new StateLabel(); 910 private final Color defaultForeground = label.getForeground(); 911 private final String closedText; 912 private final String thrownText; 913 private final String unknownText = Bundle.getMessage("BeanStateUnknown"); 914 private final String inconsistentText = Bundle.getMessage("BeanStateInconsistent"); 915 private int row = -1; // row of the cell last rendered or edited 916 917 ImageIconRenderer(String closedText, String thrownText) { 918 this.closedText = closedText; 919 this.thrownText = thrownText; 920 label.setHorizontalAlignment(JLabel.CENTER); 921 // must stay the only anonymous class in ImageIconRenderer, see jmri.ArchitectureTest 922 label.addMouseListener(new MouseAdapter() { 923 @Override 924 public final void mousePressed(MouseEvent evt) { 925 log.debug("Clicked on icon in row {}", row); 926 stopCellEditing(); 927 } 928 }); 929 } 930 931 /** 932 * {@inheritDoc} 933 */ 934 @Override 935 public Component getTableCellRendererComponent( 936 JTable table, Object value, boolean isSelected, 937 boolean hasFocus, int row, int column) { 938 log.debug("Renderer Item = {}, State = {}", row, value); 939 return updateLabel((String) value, row); 940 } 941 942 /** 943 * {@inheritDoc} 944 */ 945 @Override 946 public Component getTableCellEditorComponent( 947 JTable table, Object value, boolean isSelected, 948 int row, int column) { 949 log.debug("Editor Item = {}, State = {}", row, value); 950 return updateLabel((String) value, row); 951 } 952 953 protected JLabel updateLabel(String value, int row) { 954 this.row = row; 955 if (iconHeight < 0) { // load resources only first time, either for renderer or editor 956 loadIcons(); 957 log.debug("icons loaded"); 958 } 959 label.setForeground(defaultForeground); // reset a foreground left over from an INCONSISTENT cell 960 if (value.equals(closedText) && onIcon != null) { 961 label.setIcon(onIcon); 962 label.setText(null); 963 label.setVerticalAlignment(JLabel.BOTTOM); 964 log.debug("onIcon set"); 965 } else if (value.equals(thrownText) && offIcon != null) { 966 label.setIcon(offIcon); 967 label.setText(null); 968 label.setVerticalAlignment(JLabel.BOTTOM); 969 log.debug("offIcon set"); 970 } else if (value.equals(inconsistentText)) { 971 label.setIcon(null); 972 label.setText("X"); 973 label.setForeground(Color.red); 974 label.setVerticalAlignment(JLabel.CENTER); 975 log.debug("Turnout state inconsistent"); 976 } else if (value.equals(unknownText)) { 977 label.setIcon(null); 978 label.setText("?"); 979 label.setVerticalAlignment(JLabel.CENTER); 980 log.debug("Turnout state unknown"); 981 } else { // failed to load icon 982 label.setIcon(null); 983 label.setText(value); 984 label.setVerticalAlignment(JLabel.CENTER); 985 log.warn("Error reading icons for TurnoutTable"); 986 } 987 label.setToolTipText(value); 988 return label; 989 } 990 991 @Override 992 public Object getCellEditorValue() { 993 log.debug("getCellEditorValue, me = {})", this.toString()); 994 return this.toString(); 995 } 996 997 /** 998 * Get the row height needed to fit the state icons, loading them if 999 * required. Used by {@link #configureTable} to size the rows once, 1000 * outside of the render path. 1001 * 1002 * @return required row height in pixels, 0 if the icons could not be read 1003 */ 1004 static int getIconRowHeight() { 1005 if (iconHeight < 0) { 1006 loadIcons(); 1007 } 1008 return Math.max(iconHeight - 5, 0); 1009 } 1010 1011 /** 1012 * Read and buffer graphics. Only called once per JVM, the icons are 1013 * shared by all instances of this renderer. 1014 */ 1015 private static synchronized void loadIcons() { 1016 if (iconHeight >= 0) { // another instance already loaded the icons 1017 return; 1018 } 1019 BufferedImage onImage = null; 1020 BufferedImage offImage = null; 1021 try { 1022 onImage = ImageIO.read(new File(ON_ICON_PATH)); 1023 offImage = ImageIO.read(new File(OFF_ICON_PATH)); 1024 } catch (IOException ex) { 1025 log.error("error reading image from {} or {}", ON_ICON_PATH, OFF_ICON_PATH, ex); 1026 } 1027 if (onImage == null || offImage == null) { // ImageIO.read returns null for an unreadable file 1028 log.error("error reading image from {} or {}", ON_ICON_PATH, OFF_ICON_PATH); 1029 iconHeight = 0; // give up: render states as text, don't retry for every cell 1030 return; 1031 } 1032 log.debug("Success reading images"); 1033 int imageWidth = onImage.getWidth(); 1034 int imageHeight = onImage.getHeight(); 1035 // scale icons 50% to fit in table rows 1036 Image smallOnImage = onImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT); 1037 Image smallOffImage = offImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT); 1038 onIcon = new ImageIcon(smallOnImage); 1039 offIcon = new ImageIcon(smallOffImage); 1040 iconHeight = onIcon.getIconHeight(); 1041 } 1042 1043 /** 1044 * Label that ignores revalidate and repaint requests. 1045 * <p> 1046 * The rendered component stays a child of the table's 1047 * CellRendererPane after painting, so a plain JLabel schedules a new 1048 * repaint of the whole table each time its icon or text changes 1049 * during a paint cycle. Same overrides as DefaultTableCellRenderer, 1050 * except firePropertyChange, which BasicLabelUI needs to keep its 1051 * view up to date. 1052 */ 1053 private static class StateLabel extends JLabel { 1054 1055 @Override 1056 public void revalidate() { 1057 // ignored, see class comment 1058 } 1059 1060 @Override 1061 public void repaint() { 1062 // ignored, see class comment 1063 } 1064 1065 @Override 1066 public void repaint(long tm) { 1067 // ignored, see class comment 1068 } 1069 1070 @Override 1071 public void repaint(int x, int y, int width, int height) { 1072 // ignored, see class comment 1073 } 1074 1075 @Override 1076 public void repaint(long tm, int x, int y, int width, int height) { 1077 // ignored, see class comment 1078 } 1079 1080 @Override 1081 public void repaint(Rectangle r) { 1082 // ignored, see class comment 1083 } 1084 } 1085 1086 } // end of ImageIconRenderer class 1087 1088 protected static java.util.concurrent.atomic.AtomicBoolean editingOps = new java.util.concurrent.atomic.AtomicBoolean(false); 1089 1090 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TurnoutTableDataModel.class); 1091 1092}