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