001package jmri.jmrit.beantable.sensor; 002 003import jmri.util.gui.GuiLafPreferencesManager; 004import java.awt.Color; 005import java.awt.Component; 006import java.awt.Image; 007import java.awt.event.MouseAdapter; 008import java.awt.event.MouseEvent; 009import java.awt.image.BufferedImage; 010import java.beans.PropertyChangeEvent; 011import java.io.File; 012import java.io.IOException; 013import java.util.Enumeration; 014 015import javax.annotation.Nonnull; 016import javax.imageio.ImageIO; 017import javax.swing.*; 018import javax.swing.table.TableCellEditor; 019import javax.swing.table.TableCellRenderer; 020import javax.swing.table.TableColumn; 021import jmri.InstanceManager; 022import jmri.JmriException; 023import jmri.Manager; 024import jmri.NamedBean; 025import jmri.Sensor; 026import jmri.SensorManager; 027import jmri.managers.ProxySensorManager; 028import jmri.jmrit.beantable.BeanTableDataModel; 029import jmri.util.swing.XTableColumnModel; 030import jmri.util.swing.JmriJOptionPane; 031 032/** 033 * Data model for a SensorTable. 034 * 035 * @author Bob Jacobsen Copyright (C) 2003, 2009 036 * @author Egbert Broerse Copyright (C) 2017 037 */ 038public class SensorTableDataModel extends BeanTableDataModel<Sensor> { 039 040 static public final int INVERTCOL = BeanTableDataModel.NUMCOLUMN; 041 static public final int EDITCOL = INVERTCOL + 1; 042 static public final int USEGLOBALDELAY = EDITCOL + 1; 043 static public final int ACTIVEDELAY = USEGLOBALDELAY + 1; 044 static public final int INACTIVEDELAY = ACTIVEDELAY + 1; 045 static public final int PULLUPCOL = INACTIVEDELAY + 1; 046 static public final int FORGETCOL = PULLUPCOL + 1; 047 static public final int QUERYCOL = FORGETCOL + 1; 048 049 private Manager<Sensor> senManager = null; 050 protected boolean _graphicState = false; // icon state col updated from prefs 051 052 /** 053 * Create a new Sensor Table Data Model. 054 * The default Manager for the bean type will be a Proxy Manager. 055 */ 056 public SensorTableDataModel() { 057 super(); 058 _graphicState = InstanceManager.getDefault(GuiLafPreferencesManager.class).isGraphicTableState(); 059 } 060 061 /** 062 * Create a new Sensor Table Data Model. 063 * The default Manager for the bean type will be a Proxy Manager unless 064 * one is specified here. 065 * @param manager Bean Manager. 066 */ 067 public SensorTableDataModel(Manager<Sensor> manager) { 068 super(); 069 setManager(manager); // updates name list 070 // load graphic state column display preference 071 _graphicState = InstanceManager.getDefault(GuiLafPreferencesManager.class).isGraphicTableState(); 072 } 073 074 /** 075 * {@inheritDoc} 076 */ 077 @Override 078 public String getValue(String name) { 079 Sensor sen = getManager().getBySystemName(name); 080 if (sen == null) { 081 return "Failed to get sensor " + name; 082 } 083 return sen.describeState(sen.getKnownState()); 084 } 085 086 /** 087 * {@inheritDoc} 088 */ 089 @Override 090 protected final void setManager(@Nonnull Manager<Sensor> manager) { 091 if (!(manager instanceof SensorManager)) { 092 return; 093 } 094 getManager().removePropertyChangeListener(this); 095 if (sysNameList != null) { 096 for (int i = 0; i < sysNameList.size(); i++) { 097 // if object has been deleted, it's not here; ignore it 098 NamedBean b = getBySystemName(sysNameList.get(i)); 099 if (b != null) { 100 b.removePropertyChangeListener(this); 101 } 102 } 103 } 104 senManager = manager; 105 getManager().addPropertyChangeListener(this); 106 updateNameList(); 107 } 108 109 /** 110 * {@inheritDoc} 111 */ 112 @Override 113 protected Manager<Sensor> getManager() { 114 if (senManager == null) { 115 senManager = InstanceManager.sensorManagerInstance(); 116 } 117 return senManager; 118 } 119 120 /** 121 * {@inheritDoc} 122 */ 123 @Override 124 protected Sensor getBySystemName(@Nonnull String name) { 125 return getManager().getBySystemName(name); 126 } 127 128 /** 129 * {@inheritDoc} 130 */ 131 @Override 132 protected Sensor getByUserName(@Nonnull String name) { 133 return InstanceManager.getDefault(SensorManager.class).getByUserName(name); 134 } 135 136 /** 137 * {@inheritDoc} 138 */ 139 @Override 140 protected String getMasterClassName() { 141 return getClassName(); 142 } 143 144 /** 145 * {@inheritDoc} 146 */ 147 @Override 148 protected void clickOn(Sensor t) { 149 try { 150 t.setKnownState(t.getKnownState() == Sensor.INACTIVE ? Sensor.ACTIVE : Sensor.INACTIVE ); 151 } catch (JmriException e) { 152 log.warn("Error setting state", e); 153 } 154 } 155 156 /** 157 * {@inheritDoc} 158 */ 159 @Override 160 public int getColumnCount() { 161 return QUERYCOL + getPropertyColumnCount() + 1; 162 } 163 164 /** 165 * {@inheritDoc} 166 */ 167 @Override 168 public String getColumnName(int col) { 169 switch (col) { 170 case INVERTCOL: 171 return Bundle.getMessage("Inverted"); 172 case EDITCOL: 173 return ""; 174 case USEGLOBALDELAY: 175 return Bundle.getMessage("SensorUseGlobalDebounce"); 176 case ACTIVEDELAY: 177 return Bundle.getMessage("SensorActiveDebounce"); 178 case INACTIVEDELAY: 179 return Bundle.getMessage("SensorInActiveDebounce"); 180 case PULLUPCOL: 181 return Bundle.getMessage("SensorPullUp"); 182 case FORGETCOL: 183 return Bundle.getMessage("StateForgetHeader"); 184 case QUERYCOL: 185 return Bundle.getMessage("StateQueryHeader"); 186 default: 187 return super.getColumnName(col); 188 } 189 } 190 191 /** 192 * {@inheritDoc} 193 */ 194 @Override 195 public Class<?> getColumnClass(int col) { 196 switch (col) { 197 case INVERTCOL: 198 case USEGLOBALDELAY: 199 return Boolean.class; 200 case ACTIVEDELAY: 201 case INACTIVEDELAY: 202 return Long.class; // if long.class (lowercase) is returned here, cell is NOT editable. 203 case PULLUPCOL: 204 return JComboBox.class; 205 case EDITCOL: 206 case FORGETCOL: 207 case QUERYCOL: 208 return JButton.class; 209 case VALUECOL: 210 if (_graphicState) { 211 return JLabel.class; // use an image to show sensor state 212 } else { 213 return super.getColumnClass(col); 214 } 215 default: 216 return super.getColumnClass(col); 217 } 218 } 219 220 /** 221 * {@inheritDoc} 222 */ 223 @Override 224 public int getPreferredWidth(int col) { 225 switch (col) { 226 case INVERTCOL: 227 return new JTextField(4).getPreferredSize().width; 228 case USEGLOBALDELAY: 229 case ACTIVEDELAY: 230 case INACTIVEDELAY: 231 case PULLUPCOL: 232 return new JTextField(8).getPreferredSize().width; 233 case EDITCOL: 234 return new JButton(Bundle.getMessage("ButtonEdit")).getPreferredSize().width+4; 235 case FORGETCOL: 236 return new JButton(Bundle.getMessage("StateForgetButton")) 237 .getPreferredSize().width+4; 238 case QUERYCOL: 239 return new JButton(Bundle.getMessage("StateQueryButton")) 240 .getPreferredSize().width+4; 241 default: 242 return super.getPreferredWidth(col); 243 } 244 } 245 246 /** 247 * {@inheritDoc} 248 */ 249 @Override 250 public boolean isCellEditable(int row, int col) { 251 String name = sysNameList.get(row); 252 Sensor sen = getManager().getBySystemName(name); 253 if (sen == null) { 254 return false; 255 } 256 switch (col) { 257 case EDITCOL: 258 case USEGLOBALDELAY: 259 case FORGETCOL: 260 case QUERYCOL: 261 return true; 262 case INVERTCOL: 263 return sen.canInvert(); 264 case ACTIVEDELAY: 265 case INACTIVEDELAY: 266 return !sen.getUseDefaultTimerSettings(); 267 case PULLUPCOL: 268 if ( getManager() instanceof ProxySensorManager ) { 269 return ((ProxySensorManager)getManager()).isPullResistanceConfigurable(name); 270 } 271 return (((SensorManager) getManager()).isPullResistanceConfigurable()); // proxymanager always false 272 273 default: 274 return super.isCellEditable(row, col); 275 } 276 } 277 278 /** 279 * {@inheritDoc} 280 */ 281 @Override 282 public Object getValueAt(int row, int col) { 283 if (row >= sysNameList.size()) { 284 log.debug("row is greater than name list"); 285 return ""; 286 } 287 String name = sysNameList.get(row); 288 Sensor s = senManager.getBySystemName(name); 289 if (s == null) { 290 log.debug("error null sensor!"); 291 return "error"; 292 } 293 switch (col) { 294 case INVERTCOL: 295 return s.getInverted(); 296 case USEGLOBALDELAY: 297 return s.getUseDefaultTimerSettings(); 298 case ACTIVEDELAY: 299 return s.getSensorDebounceGoingActiveTimer(); 300 case INACTIVEDELAY: 301 return s.getSensorDebounceGoingInActiveTimer(); 302 case EDITCOL: 303 return Bundle.getMessage("ButtonEdit"); 304 case PULLUPCOL: 305 PullResistanceComboBox c = new PullResistanceComboBox(Sensor.PullResistance.values()); 306 c.setSelectedItem(s.getPullResistance()); 307 return c; 308 case FORGETCOL: 309 return Bundle.getMessage("StateForgetButton"); 310 case QUERYCOL: 311 return Bundle.getMessage("StateQueryButton"); 312 default: 313 return super.getValueAt(row, col); 314 } 315 } 316 317 /** 318 * Small class to ensure type-safety of references otherwise lost to type erasure 319 */ 320 static private class PullResistanceComboBox extends JComboBox<Sensor.PullResistance> { 321 PullResistanceComboBox(Sensor.PullResistance[] values) { super(values); } 322 } 323 324 /** 325 * {@inheritDoc} 326 */ 327 @Override 328 public void setValueAt(Object value, int row, int col) { 329 if (row >= sysNameList.size()) { 330 log.debug("row is greater than name list"); 331 return; 332 } 333 String name = sysNameList.get(row); 334 Sensor s = senManager.getBySystemName(name); 335 if (s == null) { 336 log.debug("error null sensor!"); 337 return; 338 } 339 switch (col) { 340 case INVERTCOL: 341 s.setInverted(((boolean) value)); 342 break; 343 case USEGLOBALDELAY: 344 s.setUseDefaultTimerSettings(((boolean) value)); 345 break; 346 case ACTIVEDELAY: 347 try { 348 long activeDeBounce = (long) value; 349 if (activeDeBounce < 0 || activeDeBounce > Sensor.MAX_DEBOUNCE) { 350 JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("SensorDebounceActOutOfRange") 351 + "\n\"" + Sensor.MAX_DEBOUNCE + "\"", Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE); 352 } else { 353 s.setSensorDebounceGoingActiveTimer(activeDeBounce); 354 } 355 } catch (NumberFormatException exActiveDeBounce) { 356 JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("SensorDebounceActError") 357 + "\n\"" + value + "\"" + exActiveDeBounce.getLocalizedMessage(), Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE); 358 } 359 break; 360 case INACTIVEDELAY: 361 try { 362 long inactiveDeBounce = (long) value; 363 if (inactiveDeBounce < 0 || inactiveDeBounce > Sensor.MAX_DEBOUNCE) { 364 JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("SensorDebounceInActOutOfRange") 365 + "\n\"" + Sensor.MAX_DEBOUNCE + "\"", Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE); 366 } else { 367 s.setSensorDebounceGoingInActiveTimer(inactiveDeBounce); 368 } 369 } catch (NumberFormatException exActiveDeBounce) { 370 JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("SensorDebounceInActError") 371 + "\n\"" + value + "\"" + exActiveDeBounce.getLocalizedMessage(), Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE); 372 } 373 break; 374 case EDITCOL: 375 javax.swing.SwingUtilities.invokeLater(() -> { 376 editButton(s); 377 }); 378 break; 379 case PULLUPCOL: 380 PullResistanceComboBox cb = (PullResistanceComboBox) value; 381 s.setPullResistance((Sensor.PullResistance) cb.getSelectedItem()); 382 break; 383 case FORGETCOL: 384 try { 385 s.setKnownState(Sensor.UNKNOWN); 386 } catch (JmriException e) { 387 log.warn("Failed to set state to UNKNOWN: ", e); 388 } 389 break; 390 case QUERYCOL: 391 try { 392 s.setKnownState(Sensor.UNKNOWN); 393 } catch (JmriException e) { 394 log.warn("Failed to set state to UNKNOWN: ", e); 395 } 396 s.requestUpdateFromLayout(); 397 break; 398 case VALUECOL: 399 if (_graphicState) { // respond to clicking on ImageIconRenderer CellEditor 400 clickOn(s); 401 fireTableRowsUpdated(row, row); 402 } else { 403 super.setValueAt(value, row, col); 404 } 405 break; 406 default: 407 super.setValueAt(value, row, col); 408 break; 409 } 410 } 411 412 /** 413 * {@inheritDoc} 414 */ 415 @Override 416 protected boolean matchPropertyName(PropertyChangeEvent e) { 417 switch (e.getPropertyName()) { 418 case "inverted": 419 case "GlobalTimer": 420 case "ActiveTimer": 421 case "InActiveTimer": 422 return true; 423 default: 424 return super.matchPropertyName(e); 425 } 426 } 427 428 /** 429 * Customize the sensor table Value (State) column to show an appropriate 430 * graphic for the sensor state if _graphicState = true, or (default) just 431 * show the localized state text when the TableDataModel is being called 432 * from ListedTableAction. 433 * 434 * @param table a JTable of Sensors 435 */ 436 @Override 437 protected void configValueColumn(JTable table) { 438 // have the value column hold a JPanel (icon) 439 //setColumnToHoldButton(table, VALUECOL, new JLabel("1234")); // for small round icon, but cannot be converted to JButton 440 // add extras, override BeanTableDataModel 441 log.debug("Sensor configValueColumn (I am {})", this); 442 if (_graphicState) { // load icons, only once 443 table.setDefaultEditor(JLabel.class, new ImageIconRenderer()); // editor 444 table.setDefaultRenderer(JLabel.class, new ImageIconRenderer()); // item class copied from SwitchboardEditor panel 445 } else { 446 super.configValueColumn(table); // classic text style state indication 447 } 448 } 449 450 /** 451 * Visualize state in table as a graphic, customized for Sensors (2 states). 452 * Renderer and Editor are identical, as the cell contents are not actually 453 * edited, only used to toggle state using {@link #clickOn}. 454 */ 455 static class ImageIconRenderer extends AbstractCellEditor implements TableCellEditor, TableCellRenderer { 456 457 protected JLabel label; 458 protected String rootPath = "resources/icons/misc/switchboard/"; // also used in display.switchboardEditor 459 protected char beanTypeChar = 'S'; // for Sensor 460 protected String onIconPath = rootPath + beanTypeChar + "-on-s.png"; 461 protected String offIconPath = rootPath + beanTypeChar + "-off-s.png"; 462 protected BufferedImage onImage; 463 protected BufferedImage offImage; 464 protected ImageIcon onIcon; 465 protected ImageIcon offIcon; 466 protected int iconHeight = -1; 467 468 /** 469 * {@inheritDoc} 470 */ 471 @Override 472 public Component getTableCellRendererComponent( 473 JTable table, Object value, boolean isSelected, 474 boolean hasFocus, int row, int column) { 475 log.debug("Renderer Item = {}, State = {}", row, value); 476 if (iconHeight < 0) { // load resources only first time, either for renderer or editor 477 loadIcons(); 478 log.debug("icons loaded"); 479 } 480 return updateLabel((String) value, row, table); 481 } 482 483 /** 484 * {@inheritDoc} 485 */ 486 @Override 487 public Component getTableCellEditorComponent( 488 JTable table, Object value, boolean isSelected, 489 int row, int column) { 490 log.debug("Renderer Item = {}, State = {}", row, value); 491 if (iconHeight < 0) { // load resources only first time, either for renderer or editor 492 loadIcons(); 493 log.debug("icons loaded"); 494 } 495 return updateLabel((String) value, row, table); 496 } 497 498 public JLabel updateLabel(String value, int row, JTable table) { 499 if (iconHeight > 0) { // if necessary, increase row height; 500 table.setRowHeight(row, Math.max(table.getRowHeight(), iconHeight - 5)); // adjust table row height for Sensor icon 501 } 502 if (value.equals(Bundle.getMessage("SensorStateInactive")) && offIcon != null) { 503 label = new JLabel(offIcon); 504 label.setVerticalAlignment(JLabel.BOTTOM); 505 log.debug("offIcon set"); 506 } else if (value.equals(Bundle.getMessage("SensorStateActive")) && onIcon != null) { 507 label = new JLabel(onIcon); 508 label.setVerticalAlignment(JLabel.BOTTOM); 509 log.debug("onIcon set"); 510 } else if (value.equals(Bundle.getMessage("BeanStateInconsistent"))) { 511 label = new JLabel("X", JLabel.CENTER); // centered text alignment 512 label.setForeground(Color.red); 513 log.debug("Sensor state inconsistent"); 514 iconHeight = 0; 515 } else if (value.equals(Bundle.getMessage("BeanStateUnknown"))) { 516 label = new JLabel("?", JLabel.CENTER); // centered text alignment 517 log.debug("Sensor state unknown"); 518 iconHeight = 0; 519 } else { // failed to load icon 520 label = new JLabel(value, JLabel.CENTER); // centered text alignment 521 log.warn("Error reading icons for SensorTable"); 522 iconHeight = 0; 523 } 524 label.setToolTipText(value); 525 label.addMouseListener(new MouseAdapter() { 526 @Override 527 public final void mousePressed(MouseEvent evt) { 528 log.debug("Clicked on icon in row {}", row); 529 stopCellEditing(); 530 } 531 }); 532 return label; 533 } 534 535 /** 536 * {@inheritDoc} 537 */ 538 @Override 539 public Object getCellEditorValue() { 540 log.debug("getCellEditorValue, me = {})", this); 541 return this.toString(); 542 } 543 544 /** 545 * Read and buffer graphics. Only called once for this table. 546 * 547 * @see #getTableCellEditorComponent(JTable, Object, boolean, int, int) 548 */ 549 protected void loadIcons() { 550 try { 551 onImage = ImageIO.read(new File(onIconPath)); 552 offImage = ImageIO.read(new File(offIconPath)); 553 } catch (IOException ex) { 554 log.error("error reading image from {} or {}", onIconPath, offIconPath, ex); 555 } 556 log.debug("Success reading images"); 557 int imageWidth = onImage.getWidth(); 558 int imageHeight = onImage.getHeight(); 559 // scale icons 50% to fit in table rows 560 Image smallOnImage = onImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT); 561 Image smallOffImage = offImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT); 562 onIcon = new ImageIcon(smallOnImage); 563 offIcon = new ImageIcon(smallOffImage); 564 iconHeight = onIcon.getIconHeight(); 565 } 566 567 } // end of ImageIconRenderer class 568 569 /** 570 * {@inheritDoc} 571 */ 572 @Override 573 public void configureTable(JTable table) { 574 super.configureTable(table); 575 XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel(); 576 columnModel.getColumnByModelIndex(FORGETCOL).setHeaderValue(null); 577 columnModel.getColumnByModelIndex(QUERYCOL).setHeaderValue(null); 578 } 579 580 void editButton(Sensor s) { 581 jmri.jmrit.beantable.beanedit.SensorEditAction beanEdit = new jmri.jmrit.beantable.beanedit.SensorEditAction(); 582 beanEdit.setBean(s); 583 beanEdit.actionPerformed(null); 584 } 585 586 /** 587 * Show or hide the Debounce columns. 588 * USEGLOBALDELAY, ACTIVEDELAY, INACTIVEDELAY 589 * @param show true to display, false to hide. 590 * @param table the JTable to set column visibility on. 591 */ 592 public void showDebounce(boolean show, JTable table) { 593 XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel(); 594 TableColumn column = columnModel.getColumnByModelIndex(USEGLOBALDELAY); 595 columnModel.setColumnVisible(column, show); 596 column = columnModel.getColumnByModelIndex(ACTIVEDELAY); 597 columnModel.setColumnVisible(column, show); 598 column = columnModel.getColumnByModelIndex(INACTIVEDELAY); 599 columnModel.setColumnVisible(column, show); 600 } 601 602 /** 603 * Show or hide the Pullup column. 604 * PULLUPCOL 605 * @param show true to display, false to hide. 606 * @param table the JTable to set column visibility on. 607 */ 608 public void showPullUp(boolean show, JTable table) { 609 XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel(); 610 TableColumn column = columnModel.getColumnByModelIndex(PULLUPCOL); 611 columnModel.setColumnVisible(column, show); 612 } 613 614 /** 615 * Show or hide the State - Forget and Query columns.FORGETCOL, QUERYCOL 616 * @param show true to display, false to hide. 617 * @param table the JTable to set column visibility on. 618 */ 619 public void showStateForgetAndQuery(boolean show, JTable table) { 620 XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel(); 621 TableColumn column = columnModel.getColumnByModelIndex(FORGETCOL); 622 columnModel.setColumnVisible(column, show); 623 column = columnModel.getColumnByModelIndex(QUERYCOL); 624 columnModel.setColumnVisible(column, show); 625 } 626 627 protected String getClassName() { 628 return jmri.jmrit.beantable.SensorTableAction.class.getName(); 629 } 630 631 public String getClassDescription() { 632 return Bundle.getMessage("TitleSensorTable"); 633 } 634 635 /** 636 * {@inheritDoc} 637 */ 638 @Override 639 protected void setColumnIdentities(JTable table) { 640 super.setColumnIdentities(table); 641 Enumeration<TableColumn> columns; 642 if (table.getColumnModel() instanceof XTableColumnModel) { 643 columns = ((XTableColumnModel) table.getColumnModel()).getColumns(false); 644 } else { 645 columns = table.getColumnModel().getColumns(); 646 } 647 while (columns.hasMoreElements()) { 648 TableColumn column = columns.nextElement(); 649 switch (column.getModelIndex()) { 650 case FORGETCOL: 651 column.setIdentifier("ForgetState"); 652 break; 653 case QUERYCOL: 654 column.setIdentifier("QueryState"); 655 break; 656 default: 657 // use existing value 658 } 659 } 660 } 661 662 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SensorTableDataModel.class); 663 664}