001package jmri.jmrit.beantable.block; 002 003import java.awt.*; 004import java.awt.event.MouseAdapter; 005import java.awt.event.MouseEvent; 006import java.awt.image.BufferedImage; 007 008import java.beans.PropertyChangeEvent; 009 010import java.io.File; 011import java.io.IOException; 012 013import java.text.DecimalFormat; 014 015import java.util.Arrays; 016import java.util.Set; 017import java.util.Vector; 018 019import javax.annotation.Nonnull; 020import javax.imageio.ImageIO; 021import javax.swing.*; 022import javax.swing.table.TableCellEditor; 023import javax.swing.table.TableCellRenderer; 024 025import jmri.*; 026import jmri.implementation.SignalSpeedMap; 027import jmri.jmrit.beantable.*; 028import jmri.jmrit.beantable.beanedit.BlockEditAction; 029import jmri.util.gui.GuiLafPreferencesManager; 030import jmri.util.swing.JComboBoxUtil; 031import jmri.util.swing.JmriJOptionPane; 032 033/** 034 * Data model for a Block Table. 035 * Code originally within BlockTableAction. 036 * 037 * @author Bob Jacobsen Copyright (C) 2003, 2008 038 * @author Egbert Broerse Copyright (C) 2017 039 * @author Steve Young Copyright (C) 2021 040 */ 041public class BlockTableDataModel extends BeanTableDataModel<Block> { 042 043 static public final int EDITCOL = BeanTableDataModel.NUMCOLUMN; 044 static public final int DIRECTIONCOL = EDITCOL + 1; 045 static public final int LENGTHCOL = DIRECTIONCOL + 1; 046 static public final int CURVECOL = LENGTHCOL + 1; 047 static public final int STATECOL = CURVECOL + 1; 048 static public final int SENSORCOL = STATECOL + 1; 049 static public final int REPORTERCOL = SENSORCOL + 1; 050 static public final int CURRENTREPCOL = REPORTERCOL + 1; 051 static public final int PERMISCOL = CURRENTREPCOL + 1; 052 static public final int SPEEDCOL = PERMISCOL + 1; 053 054 private final boolean _graphicState = InstanceManager.getDefault(GuiLafPreferencesManager.class).isGraphicTableState(); 055 056 private final DecimalFormat twoDigit = new DecimalFormat("0.00"); 057 058 private Vector<String> speedList = new Vector<>(); 059 private String[] sensorList; 060 private String[] reporterList; 061 062 String defaultBlockSpeedText; 063 064 public BlockTableDataModel(Manager<Block> mgr){ 065 super(); 066 setManager(mgr); // for consistency with other BeanTableModels, default BlockManager always used. 067 068 defaultBlockSpeedText = (Bundle.getMessage("UseGlobal", "Global") + " " + InstanceManager.getDefault(BlockManager.class).getDefaultSpeed()); // first entry in drop down list 069 speedList.add(defaultBlockSpeedText); 070 Vector<String> _speedMap = InstanceManager.getDefault(SignalSpeedMap.class).getValidSpeedNames(); 071 for (int i = 0; i < _speedMap.size(); i++) { 072 if (!speedList.contains(_speedMap.get(i))) { 073 speedList.add(_speedMap.get(i)); 074 } 075 } 076 077 updateSensorList(); 078 updateReporterList(); 079 } 080 081 @Override 082 public String getValue(String name) { 083 if (name == null) { 084 log.warn("requested getValue(null)"); 085 return "(no name)"; 086 } 087 Block b = InstanceManager.getDefault(BlockManager.class).getBySystemName(name); 088 if (b == null) { 089 log.debug("requested getValue(\"{}\"), Block doesn't exist", name); 090 return "(no Block)"; 091 } 092 Object m = b.getValue(); 093 if (m != null) { 094 if ( m instanceof Reportable) { 095 return ((Reportable) m).toReportString(); 096 } 097 else { 098 return m.toString(); 099 } 100 } else { 101 return ""; 102 } 103 } 104 105 @Override 106 public Manager<Block> getManager() { 107 return InstanceManager.getDefault(BlockManager.class); 108 } 109 110 @Override 111 public Block getBySystemName(@Nonnull String name) { 112 return InstanceManager.getDefault(BlockManager.class).getBySystemName(name); 113 } 114 115 @Override 116 public Block getByUserName(@Nonnull String name) { 117 return InstanceManager.getDefault(BlockManager.class).getByUserName(name); 118 } 119 120 @Override 121 protected String getMasterClassName() { 122 return BlockTableAction.class.getName(); 123 } 124 125 @Override 126 public void clickOn(Block t) { 127 // don't do anything on click; not used in this class, because 128 // we override setValueAt 129 } 130 131 @Override 132 public int getColumnCount() { 133 return SPEEDCOL + 1; 134 } 135 136 @Override 137 public Object getValueAt(int row, int col) { 138 // some error checking 139 if (row >= sysNameList.size()) { 140 log.error("requested getValueAt(\"{}\"), row outside of range", row); 141 return "Error table size"; 142 } 143 Block b = getBySystemName(sysNameList.get(row)); 144 if (b == null) { 145 log.error("requested getValueAt(\"{}\"), Block doesn't exist", row); 146 return "(no Block)"; 147 } 148 switch (col) { 149 case DIRECTIONCOL: 150 return Path.decodeDirection(b.getDirection()); 151 case CURVECOL: 152 BlockCurvatureJComboBox box = new BlockCurvatureJComboBox(b.getCurvature()); 153 box.setJTableCellClientProperties(); 154 return box; 155 case LENGTHCOL: 156 return (twoDigit.format(metricUi ? b.getLengthCm() : b.getLengthIn())); 157 case PERMISCOL: 158 return b.getPermissiveWorking(); 159 case SPEEDCOL: 160 String speed = b.getBlockSpeed(); 161 if (!speedList.contains(speed)) { 162 speedList.add(speed); 163 } 164 JComboBox<String> c = new JComboBox<>(speedList); 165 c.setEditable(true); 166 c.setSelectedItem(speed); 167 JComboBoxUtil.setupComboBoxMaxRows(c); 168 return c; 169 case STATECOL: 170 return blockDescribeState(b.getState()); 171 case SENSORCOL: 172 Sensor sensor = b.getSensor(); 173 JComboBox<String> cs = new JComboBox<>(sensorList); 174 String name = ""; 175 if (sensor != null) { 176 name = sensor.getDisplayName(); 177 } 178 cs.setSelectedItem(name); 179 JComboBoxUtil.setupComboBoxMaxRows(cs); 180 return cs; 181 case REPORTERCOL: 182 Reporter reporter = b.getReporter(); 183 JComboBox<String> rs = new JComboBox<>(reporterList); 184 String repname = ""; 185 if (reporter != null) { 186 repname = reporter.getDisplayName(); 187 } 188 rs.setSelectedItem(repname); 189 JComboBoxUtil.setupComboBoxMaxRows(rs); 190 return rs; 191 case CURRENTREPCOL: 192 return b.isReportingCurrent(); 193 case EDITCOL: 194 return Bundle.getMessage("ButtonEdit"); 195 default: 196 return super.getValueAt(row, col); 197 } 198 } 199 200 // TODO : Add Block.UNDETECTED 201 // TODO : Move to Block.describeState(int) 202 private String blockDescribeState(int blockState){ 203 switch (blockState) { 204 case (Block.OCCUPIED): 205 return Bundle.getMessage("BlockOccupied"); 206 case (Block.UNOCCUPIED): 207 return Bundle.getMessage("BlockUnOccupied"); 208 case (Block.UNKNOWN): 209 return Bundle.getMessage("BlockUnknown"); 210 default: 211 return Bundle.getMessage("BlockInconsistent"); 212 } 213 } 214 215 @Override 216 public void setValueAt(Object value, int row, int col) { 217 // no setting of block state from table 218 Block b = getBySystemName(sysNameList.get(row)); 219 switch (col) { 220 case VALUECOL: 221 b.setValue(value); 222 break; 223 case LENGTHCOL: 224 float len = 0.0f; 225 try { 226 len = jmri.util.IntlUtilities.floatValue(value.toString()); 227 } catch (java.text.ParseException ex2) { 228 log.error("Error parsing length value of \"{}\"", value); 229 } // block setLength() expecting value in mm, TODO: unit testing around this. 230 b.setLength( metricUi ? len * 10.0f : len * 25.4f); 231 break; 232 case CURVECOL: 233 b.setCurvature(BlockCurvatureJComboBox.getCurvatureFromObject(value)); 234 break; 235 case PERMISCOL: 236 b.setPermissiveWorking((Boolean) value); 237 break; 238 case SPEEDCOL: 239 @SuppressWarnings("unchecked") 240 String speed = (String) ((JComboBox<String>) value).getSelectedItem(); 241 try { 242 b.setBlockSpeed(speed); 243 } catch (JmriException ex) { 244 JmriJOptionPane.showMessageDialog(null, ex.getMessage() + "\n" + speed); 245 return; 246 } 247 if (!speedList.contains(speed) && !speed.contains("Global")) { // NOI18N 248 speedList.add(speed); 249 } 250 break; 251 case REPORTERCOL: 252 @SuppressWarnings("unchecked") 253 String strReporter = (String) ((JComboBox<String>) value).getSelectedItem(); 254 Reporter r = InstanceManager.getDefault(ReporterManager.class).getReporter(strReporter); 255 b.setReporter(r); 256 break; 257 case SENSORCOL: 258 @SuppressWarnings("unchecked") 259 String strSensor = (String) ((JComboBox<String>) value).getSelectedItem(); 260 b.setSensor(strSensor); 261 break; 262 case CURRENTREPCOL: 263 b.setReportingCurrent((Boolean) value); 264 break; 265 case EDITCOL: 266 javax.swing.SwingUtilities.invokeLater(() -> { 267 editButton(b); 268 }); 269 break; 270 default: 271 super.setValueAt(value, row, col); 272 break; 273 } 274 } 275 276 @Override 277 public String getColumnName(int col) { 278 switch (col) { 279 case DIRECTIONCOL: 280 return Bundle.getMessage("BlockDirection"); 281 case VALUECOL: 282 return Bundle.getMessage("BlockValue"); 283 case CURVECOL: 284 return Bundle.getMessage("BlockCurveColName"); 285 case LENGTHCOL: 286 return Bundle.getMessage("BlockLengthColName"); 287 case PERMISCOL: 288 return Bundle.getMessage("BlockPermColName"); 289 case SPEEDCOL: 290 return Bundle.getMessage("BlockSpeedColName"); 291 case STATECOL: 292 return Bundle.getMessage("BlockState"); 293 case REPORTERCOL: 294 return Bundle.getMessage("BlockReporter"); 295 case SENSORCOL: 296 return Bundle.getMessage("BlockSensor"); 297 case CURRENTREPCOL: 298 return Bundle.getMessage("BlockReporterCurrent"); 299 case EDITCOL: 300 return Bundle.getMessage("ButtonEdit"); 301 default: 302 return super.getColumnName(col); 303 } 304 } 305 306 @Override 307 public Class<?> getColumnClass(int col) { 308 switch (col) { 309 case DIRECTIONCOL: 310 case VALUECOL: // not a button 311 case LENGTHCOL: 312 return String.class; 313 case STATECOL: // may use an image to show block state 314 if (_graphicState) { 315 return JLabel.class; 316 } else { 317 return String.class; 318 } 319 case SPEEDCOL: 320 case CURVECOL: 321 case REPORTERCOL: 322 case SENSORCOL: 323 return JComboBox.class; 324 case CURRENTREPCOL: 325 case PERMISCOL: 326 return Boolean.class; 327 case EDITCOL: 328 return JButton.class; 329 default: 330 return super.getColumnClass(col); 331 } 332 } 333 334 @Override 335 public int getPreferredWidth(int col) { 336 switch (col) { 337 case DIRECTIONCOL: 338 case LENGTHCOL: 339 case PERMISCOL: 340 case SPEEDCOL: 341 case CURRENTREPCOL: 342 case EDITCOL: 343 return new JTextField(7).getPreferredSize().width; 344 case CURVECOL: 345 case STATECOL: 346 case REPORTERCOL: 347 case SENSORCOL: 348 return new JTextField(8).getPreferredSize().width; 349 default: 350 return super.getPreferredWidth(col); 351 } 352 } 353 354 @Override 355 public void configValueColumn(JTable table) { 356 // value column isn't button, so config is null 357 } 358 359 @Override 360 public boolean isCellEditable(int row, int col) { 361 switch (col) { 362 case CURVECOL: 363 case LENGTHCOL: 364 case PERMISCOL: 365 case SPEEDCOL: 366 case REPORTERCOL: 367 case SENSORCOL: 368 case CURRENTREPCOL: 369 case EDITCOL: 370 return true; 371 case STATECOL: 372 return false; 373 default: 374 return super.isCellEditable(row, col); 375 } 376 } 377 378 @Override 379 public void configureTable(JTable table) { 380 InstanceManager.sensorManagerInstance().addPropertyChangeListener(this); 381 InstanceManager.getDefault(ReporterManager.class).addPropertyChangeListener(this); 382 configStateColumn(table); 383 super.configureTable(table); 384 } 385 386 void editButton(Block b) { 387 BlockEditAction beanEdit = new BlockEditAction(); 388 beanEdit.setBean(b); 389 beanEdit.actionPerformed(null); 390 } 391 392 /** 393 * returns true for all Block properties. 394 * @param e property event that has changed. 395 * @return true as all matched. 396 */ 397 @Override 398 protected boolean matchPropertyName(PropertyChangeEvent e) { 399 return true; 400 } 401 402 @Override 403 public JButton configureButton() { 404 log.error("configureButton should not have been called"); 405 return null; 406 } 407 408 @Override 409 public void propertyChange(PropertyChangeEvent e) { 410 if (e.getSource() instanceof SensorManager) { 411 if (e.getPropertyName().equals("length") || e.getPropertyName().equals("DisplayListName")) { // NOI18N 412 updateSensorList(); 413 } 414 } 415 if (e.getSource() instanceof ReporterManager) { 416 if (e.getPropertyName().equals("length") || e.getPropertyName().equals("DisplayListName")) { // NOI18N 417 updateReporterList(); 418 } 419 } 420 if (e.getPropertyName().equals("DefaultBlockSpeedChange")) { // NOI18N 421 updateSpeedList(); 422 } else { 423 super.propertyChange(e); 424 } 425 } 426 427 private boolean metricUi = InstanceManager.getDefault(UserPreferencesManager.class) 428 .getSimplePreferenceState(BlockTableAction.BLOCK_METRIC_PREF); 429 430 /** 431 * Set and refresh the UI to use Metric or Imperial values. 432 * @param boo true if metric, false for Imperial. 433 */ 434 public void setMetric(boolean boo){ 435 metricUi = boo; 436 fireTableDataChanged(); 437 } 438 439 private void updateSensorList() { 440 Set<Sensor> nameSet = InstanceManager.sensorManagerInstance().getNamedBeanSet(); 441 String[] displayList = new String[nameSet.size()]; 442 int i = 0; 443 for (Sensor nBean : nameSet) { 444 if (nBean != null) { 445 displayList[i++] = nBean.getDisplayName(); 446 } 447 } 448 Arrays.sort(displayList); 449 sensorList = new String[displayList.length + 1]; 450 sensorList[0] = ""; 451 i = 1; 452 for (String name : displayList) { 453 sensorList[i] = name; 454 i++; 455 } 456 } 457 458 private void updateReporterList() { 459 Set<Reporter> nameSet = InstanceManager.getDefault(ReporterManager.class).getNamedBeanSet(); 460 String[] displayList = new String[nameSet.size()]; 461 int i = 0; 462 for (Reporter nBean : nameSet) { 463 if (nBean != null) { 464 displayList[i++] = nBean.getDisplayName(); 465 } 466 } 467 Arrays.sort(displayList); 468 reporterList = new String[displayList.length + 1]; 469 reporterList[0] = ""; 470 i = 1; 471 for (String name : displayList) { 472 reporterList[i] = name; 473 i++; 474 } 475 } 476 477 private void updateSpeedList() { 478 speedList.remove(defaultBlockSpeedText); 479 defaultBlockSpeedText = (Bundle.getMessage("UseGlobal", "Global") + " " + InstanceManager.getDefault(BlockManager.class).getDefaultSpeed()); 480 speedList.add(0, defaultBlockSpeedText); 481 fireTableDataChanged(); 482 } 483 484 public void setDefaultSpeeds(JFrame _who) { 485 JComboBox<String> blockSpeedCombo = new JComboBox<>(speedList); 486 JComboBoxUtil.setupComboBoxMaxRows(blockSpeedCombo); 487 488 blockSpeedCombo.setEditable(true); 489 490 JPanel block = new JPanel(); 491 block.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("BlockSpeedLabel")))); 492 block.add(blockSpeedCombo); 493 494 blockSpeedCombo.removeItem(defaultBlockSpeedText); 495 496 blockSpeedCombo.setSelectedItem(InstanceManager.getDefault(BlockManager.class).getDefaultSpeed()); 497 498 // block of options above row of buttons; gleaned from Maintenance.makeDialog() 499 // can be accessed by Jemmy in GUI test 500 String title = Bundle.getMessage("BlockSpeedLabel"); 501 // build JPanel for comboboxes 502 JPanel speedspanel = new JPanel(); 503 speedspanel.setLayout(new BoxLayout(speedspanel, BoxLayout.PAGE_AXIS)); 504 speedspanel.add(new JLabel(Bundle.getMessage("BlockSpeedSelectDialog"))); 505 //default LEFT_ALIGNMENT 506 block.setAlignmentX(Component.LEFT_ALIGNMENT); 507 speedspanel.add(block); 508 509 int retval = JmriJOptionPane.showConfirmDialog( 510 _who, 511 speedspanel, 512 title, 513 JmriJOptionPane.OK_CANCEL_OPTION, 514 JmriJOptionPane.INFORMATION_MESSAGE); 515 log.debug("Retval = {}", retval); 516 if (retval != JmriJOptionPane.OK_OPTION) { // OK button not clicked 517 return; 518 } 519 520 String speedValue = (String) blockSpeedCombo.getSelectedItem(); 521 //We will allow the turnout manager to handle checking if the values have changed 522 try { 523 InstanceManager.getDefault(BlockManager.class).setDefaultSpeed(speedValue); 524 } catch (IllegalArgumentException ex) { 525 JmriJOptionPane.showMessageDialog(_who, ex.getMessage() + "\n" + speedValue); 526 } 527 } 528 529 @Override 530 synchronized public void dispose() { 531 InstanceManager.getDefault(SensorManager.class).removePropertyChangeListener(this); 532 InstanceManager.getDefault(ReporterManager.class).removePropertyChangeListener(this); 533 super.dispose(); 534 } 535 536 /** 537 * Customize the block table State column to show an appropriate 538 * graphic for the block occupancy state if _graphicState = true, or 539 * (default) just show the localized state text when the 540 * TableDataModel is being called from ListedTableAction. 541 * 542 * @param table a JTable of Blocks 543 */ 544 protected void configStateColumn(JTable table) { 545 // have the state column hold a JPanel (icon) 546 //setColumnToHoldButton(table, VALUECOL, new JLabel("1234")); // for small round icon, but cannot be converted to JButton 547 // add extras, override BeanTableDataModel 548 log.debug("Block configStateColumn (I am {})", super.toString()); 549 if (_graphicState) { // load icons, only once 550 //table.setDefaultEditor(JLabel.class, new ImageIconRenderer()); // there's no editor for state column in BlockTable 551 table.setDefaultRenderer(JLabel.class, new ImageIconRenderer()); // item class copied from SwitchboardEditor panel 552 // else, classic text style state indication, do nothing extra 553 } 554 } 555 556 // state column may be image so have the tooltip as text version of Block state. 557 // length column tooltip confirms inches or cm. 558 @Override 559 public String getCellToolTip(JTable table, int row, int col) { 560 switch (col) { 561 case BlockTableDataModel.STATECOL: 562 Block b = (Block) getValueAt(row, 0); 563 return blockDescribeState(b.getState()); 564 case BlockTableDataModel.LENGTHCOL: 565 return ( metricUi ? Bundle.getMessage("LengthCentimeters"): Bundle.getMessage("LengthInches")); 566 default: 567 return super.getCellToolTip(table, row, col); 568 } 569 } 570 571 /** 572 * Visualize state in table as a graphic, customized for Blocks (2 573 * states). Renderer and Editor are identical, as the cell contents 574 * are not actually edited. 575 * 576 */ 577 static class ImageIconRenderer extends AbstractCellEditor implements TableCellEditor, TableCellRenderer { 578 579 protected JLabel label; 580 protected String rootPath = "resources/icons/misc/switchboard/"; // also used in display.switchboardEditor 581 protected char beanTypeChar = 'S'; // reuse Sensor icon for block state 582 protected String onIconPath = rootPath + beanTypeChar + "-on-s.png"; 583 protected String offIconPath = rootPath + beanTypeChar + "-off-s.png"; 584 protected BufferedImage onImage; 585 protected BufferedImage offImage; 586 protected ImageIcon onIcon; 587 protected ImageIcon offIcon; 588 protected int iconHeight = -1; 589 590 @Override 591 public Component getTableCellRendererComponent( 592 JTable table, Object value, boolean isSelected, 593 boolean hasFocus, int row, int column) { 594 log.debug("Renderer Item = {}, State = {}", row, value); 595 if (iconHeight < 0) { // load resources only first time, either for renderer or editor 596 loadIcons(); 597 log.debug("icons loaded"); 598 } 599 Block b = (Block) table.getModel().getValueAt(row, 0); 600 return updateLabel(b); 601 } 602 603 @Override 604 public Component getTableCellEditorComponent( 605 JTable table, Object value, boolean isSelected, 606 int row, int column) { 607 log.debug("Renderer Item = {}, State = {}", row, value); 608 if (iconHeight < 0) { // load resources only first time, either for renderer or editor 609 loadIcons(); 610 log.debug("icons loaded"); 611 } 612 Block b = (Block) table.getModel().getValueAt(row, 0); 613 return updateLabel(b); 614 } 615 616 public JLabel updateLabel(Block b) { 617 // if (iconHeight > 0) { // if necessary, increase row height; 618 //table.setRowHeight(row, Math.max(table.getRowHeight(), iconHeight - 5)); // TODO adjust table row height for Block icons 619 // } 620 if (b.getState()==Block.UNOCCUPIED && offIcon != null) { 621 label = new JLabel(offIcon); 622 label.setVerticalAlignment(JLabel.BOTTOM); 623 } else if (b.getState()==Block.OCCUPIED && onIcon != null) { 624 label = new JLabel(onIcon); 625 label.setVerticalAlignment(JLabel.BOTTOM); 626 } else if (b.getState()==Block.INCONSISTENT) { 627 label = new JLabel("X", JLabel.CENTER); // centered text alignment 628 label.setForeground(Color.red); 629 iconHeight = 0; 630 } else { // Unknown Undetected Other 631 label = new JLabel("?", JLabel.CENTER); // centered text alignment 632 iconHeight = 0; 633 } 634 label.addMouseListener(new MouseAdapter() { 635 @Override 636 public final void mousePressed(MouseEvent evt) { 637 log.debug("Clicked on icon for block {}",b); 638 stopCellEditing(); 639 } 640 }); 641 return label; 642 } 643 644 @Override 645 public Object getCellEditorValue() { 646 log.debug("getCellEditorValue, me = {})", this.toString()); 647 return this.toString(); 648 } 649 650 /** 651 * Read and buffer graphics. Only called once for this table. 652 * 653 * @see #getTableCellEditorComponent(JTable, Object, boolean, 654 * int, int) 655 */ 656 protected void loadIcons() { 657 try { 658 onImage = ImageIO.read(new File(onIconPath)); 659 offImage = ImageIO.read(new File(offIconPath)); 660 } catch (IOException ex) { 661 log.error("error reading image from {} or {}", onIconPath, offIconPath, ex); 662 } 663 log.debug("Success reading images"); 664 int imageWidth = onImage.getWidth(); 665 int imageHeight = onImage.getHeight(); 666 // scale icons 50% to fit in table rows 667 Image smallOnImage = onImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT); 668 Image smallOffImage = offImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT); 669 onIcon = new ImageIcon(smallOnImage); 670 offIcon = new ImageIcon(smallOffImage); 671 iconHeight = onIcon.getIconHeight(); 672 } 673 674 } // end of ImageIconRenderer class 675 676 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BlockTableDataModel.class); 677 678}