001package jmri.jmrit.beantable.light; 002 003import java.awt.*; 004import java.awt.event.MouseAdapter; 005import java.awt.event.MouseEvent; 006import java.awt.image.BufferedImage; 007import java.io.File; 008import java.io.IOException; 009 010import javax.annotation.Nonnull; 011import javax.imageio.ImageIO; 012import javax.swing.*; 013import javax.swing.table.TableCellEditor; 014import javax.swing.table.TableCellRenderer; 015 016import jmri.*; 017import jmri.jmrit.beantable.BeanTableDataModel; 018import static jmri.jmrit.beantable.LightTableAction.getDescriptionText; 019 020/** 021 * Data model for a Light Table. 022 * Code originally within LightTableAction. 023 * 024 * @author Dave Duchamp Copyright (C) 2004 025 * @author Egbert Broerse Copyright (C) 2017 026 * @author Steve Young Copyright (C) 2021 027 */ 028public class LightTableDataModel extends BeanTableDataModel<Light> { 029 030 static public final int ENABLECOL = BeanTableDataModel.NUMCOLUMN; 031 static public final int INTENSITYCOL = ENABLECOL + 1; 032 static public final int EDITCOL = INTENSITYCOL + 1; 033 static public final int CONTROLCOL = EDITCOL + 1; 034 035 // for icon state col 036 protected boolean _graphicState = false; // updated from prefs 037 038 public LightTableDataModel(){ 039 super(); 040 initTable(); 041 } 042 043 public LightTableDataModel(Manager<Light> mgr){ 044 super(); 045 setManager(mgr); 046 initTable(); 047 } 048 049 private void initTable() { 050 051 _graphicState = InstanceManager.getDefault(jmri.util.gui.GuiLafPreferencesManager.class).isGraphicTableState(); 052 053 } 054 055 private Manager<Light> lightManager; 056 057 /** 058 * {@inheritDoc} 059 */ 060 @Nonnull 061 @Override 062 public Manager<Light> getManager() { 063 if (lightManager == null) { 064 lightManager = InstanceManager.getDefault(LightManager.class); 065 } 066 return lightManager; 067 } 068 069 /** 070 * {@inheritDoc} 071 */ 072 @Override 073 protected final void setManager(@Nonnull Manager<Light> manager) { 074 if (!(manager instanceof LightManager)) { 075 return; 076 } 077 getManager().removePropertyChangeListener(this); 078 if (sysNameList != null) { 079 for (int i = 0; i < sysNameList.size(); i++) { 080 // if object has been deleted, it's not here; ignore it 081 NamedBean b = getBySystemName(sysNameList.get(i)); 082 if (b != null) { 083 b.removePropertyChangeListener(this); 084 } 085 } 086 } 087 lightManager = manager; 088 getManager().addPropertyChangeListener(this); 089 updateNameList(); 090 } 091 092 /** 093 * {@inheritDoc} 094 */ 095 @Override 096 public int getColumnCount() { 097 return CONTROLCOL + 1 + getPropertyColumnCount(); 098 } 099 100 /** 101 * {@inheritDoc} 102 */ 103 @Override 104 public String getColumnName(int col) { 105 switch (col) { 106 case EDITCOL: 107 return ""; // no heading on "Edit" 108 case INTENSITYCOL: 109 return Bundle.getMessage("ColumnHeadIntensity"); 110 case ENABLECOL: 111 return Bundle.getMessage("ColumnHeadEnabled"); 112 case CONTROLCOL: 113 return Bundle.getMessage("LightControllerTitlePlural"); 114 default: 115 return super.getColumnName(col); 116 } 117 } 118 119 /** 120 * {@inheritDoc} 121 */ 122 @Override 123 public Class<?> getColumnClass(int col) { 124 switch (col) { 125 case EDITCOL: 126 return JButton.class; 127 case INTENSITYCOL: 128 return Double.class; 129 case ENABLECOL: 130 return Boolean.class; 131 case CONTROLCOL: 132 return String.class; 133 case VALUECOL: // may use an image to show light state 134 return ( _graphicState ? JLabel.class : JButton.class ); 135 default: 136 return super.getColumnClass(col); 137 } 138 } 139 140 /** 141 * {@inheritDoc} 142 */ 143 @Override 144 public int getPreferredWidth(int col) { 145 switch (col) { 146 case USERNAMECOL: // override default value for UserName column 147 case CONTROLCOL: 148 return new JTextField(16).getPreferredSize().width; 149 case EDITCOL: 150 return new JButton(Bundle.getMessage("ButtonEdit")).getPreferredSize().width+4; 151 case INTENSITYCOL: 152 case ENABLECOL: 153 return new JTextField(6).getPreferredSize().width; 154 default: 155 return super.getPreferredWidth(col); 156 } 157 } 158 159 /** 160 * {@inheritDoc} 161 */ 162 @Override 163 public boolean isCellEditable(int row, int col) { 164 switch (col) { 165 case INTENSITYCOL: 166 return getValueAt(row, SYSNAMECOL) instanceof VariableLight; 167 case EDITCOL: 168 case ENABLECOL: 169 return true; 170 case CONTROLCOL: 171 return false; 172 default: 173 return super.isCellEditable(row, col); 174 } 175 } 176 177 /** 178 * {@inheritDoc} 179 */ 180 @Override 181 public String getValue(String name) { 182 Light l = lightManager.getBySystemName(name); 183 return (l==null ? ("Failed to find " + name) : l.describeState(l.getState())); 184 } 185 186 /** 187 * {@inheritDoc} 188 */ 189 @Override 190 public Object getValueAt(int row, int col) { 191 Light l = (Light) super.getValueAt(row, SYSNAMECOL); 192 if (l == null){ 193 return null; 194 } 195 switch (col) { 196 case EDITCOL: 197 return Bundle.getMessage("ButtonEdit"); 198 case INTENSITYCOL: 199 if (l instanceof VariableLight) { 200 return ((VariableLight)l).getTargetIntensity(); 201 } else { 202 return 0.0; 203 } 204 case ENABLECOL: 205 return l.getEnabled(); 206 case CONTROLCOL: 207 StringBuilder sb = new StringBuilder(); 208 for (LightControl lc : l.getLightControlList()) { 209 sb.append(getDescriptionText(lc, lc.getControlType())); 210 sb.append(" "); 211 } 212 return sb.toString(); 213 default: 214 return super.getValueAt(row, col); 215 } 216 } 217 218 /** 219 * {@inheritDoc} 220 */ 221 @Override 222 public void setValueAt(Object value, int row, int col) { 223 Light l = (Light) getValueAt(row, SYSNAMECOL); 224 if (l == null){ 225 return; 226 } 227 switch (col) { 228 case EDITCOL: 229 // Use separate Runnable so window is created on top 230 javax.swing.SwingUtilities.invokeLater(() -> { 231 editButton(l); 232 }); 233 break; 234 case INTENSITYCOL: 235 // alternate 236 try { 237 if (l instanceof VariableLight) { 238 double intensity = Math.max(0, Math.min(1.0, (Double) value)); 239 ((VariableLight)l).setTargetIntensity(intensity); 240 } else { 241 double intensity = ((Double) value); 242 l.setCommandedState( intensity > 0.5 ? Light.ON : Light.OFF); 243 } 244 } catch (IllegalArgumentException e1) { 245 jmri.util.swing.JmriJOptionPane.showMessageDialog(null, e1.getMessage()); 246 } 247 break; 248 case ENABLECOL: 249 l.setEnabled(!l.getEnabled()); 250 break; 251 case VALUECOL: 252 clickOn(l); 253 fireTableRowsUpdated(row, row); 254 break; 255 default: 256 super.setValueAt(value, row, col); 257 break; 258 } 259 } 260 261 private void editButton(Light bean){ 262 jmri.jmrit.beantable.beanedit.LightEditAction beanEdit = new jmri.jmrit.beantable.beanedit.LightEditAction(); 263 beanEdit.setBean(bean); 264 beanEdit.actionPerformed(null); 265 } 266 267 /** 268 * Delete the bean after all the checking has been done. 269 * <p> 270 * Deactivate the light, then use the superclass to delete it. 271 */ 272 @Override 273 protected void doDelete(Light bean) { 274 bean.deactivateLight(); 275 super.doDelete(bean); 276 } 277 278 // all properties update for now 279 @Override 280 protected boolean matchPropertyName(java.beans.PropertyChangeEvent e) { 281 return true; 282 } 283 284 /** 285 * {@inheritDoc} 286 */ 287 @Override 288 public Light getBySystemName(@Nonnull String name) { 289 return lightManager.getBySystemName(name); 290 } 291 292 /** 293 * {@inheritDoc} 294 */ 295 @Override 296 public Light getByUserName(@Nonnull String name) { 297 return InstanceManager.getDefault(LightManager.class).getByUserName(name); 298 } 299 300 /** 301 * {@inheritDoc} 302 */ 303 @Override 304 protected String getMasterClassName() { 305 return jmri.jmrit.beantable.LightTableAction.class.getName(); 306 } 307 308 /** 309 * {@inheritDoc} 310 */ 311 @Override 312 public void clickOn(Light t) { 313 t.setState( t.getState()==Light.OFF ? Light.ON : Light.OFF ); 314 } 315 316 /** 317 * {@inheritDoc} 318 */ 319 @Override 320 public JButton configureButton() { 321 return new JButton(" " + Bundle.getMessage("StateOff") + " "); 322 } 323 324 /** 325 * Customize the light table Value (State) column to show an 326 * appropriate graphic for the light state if _graphicState = true, 327 * or (default) just show the localized state text when the 328 * TableDataModel is being called from ListedTableAction. 329 * 330 * @param table a JTable of Lights 331 */ 332 @Override 333 protected void configValueColumn(JTable table) { 334 // have the value column hold a JPanel (icon) 335 //setColumnToHoldButton(table, VALUECOL, new JLabel("123456")); // for small round icon, but cannot be converted to JButton 336 // add extras, override BeanTableDataModel 337 log.debug("Light configValueColumn (I am {})", this); 338 if (_graphicState) { // load icons, only once 339 table.setDefaultEditor(JLabel.class, new ImageIconRenderer()); // editor 340 table.setDefaultRenderer(JLabel.class, new ImageIconRenderer()); // item class copied from SwitchboardEditor panel 341 } else { 342 super.configValueColumn(table); // classic text style state indication 343 } 344 } 345 346 /** 347 * Visualize state in table as a graphic, customized for Lights (2 348 * states + ... for transitioning). Renderer and Editor are 349 * identical, as the cell contents are not actually edited, only 350 * used to toggle state using {@link #clickOn(Light)}. 351 * 352 */ 353 static class ImageIconRenderer extends AbstractCellEditor implements TableCellEditor, TableCellRenderer { 354 355 protected JLabel label; 356 protected String rootPath = "resources/icons/misc/switchboard/"; // also used in display.switchboardEditor 357 protected char beanTypeChar = 'L'; // for Light 358 protected String onIconPath = rootPath + beanTypeChar + "-on-s.png"; 359 protected String offIconPath = rootPath + beanTypeChar + "-off-s.png"; 360 protected BufferedImage onImage; 361 protected BufferedImage offImage; 362 protected ImageIcon onIcon; 363 protected ImageIcon offIcon; 364 protected int iconHeight = -1; 365 366 @Override 367 public Component getTableCellRendererComponent( 368 JTable table, Object value, boolean isSelected, 369 boolean hasFocus, int row, int column) { 370 log.debug("Renderer Item = {}, State = {}", row, value); 371 if (iconHeight < 0) { // load resources only first time, either for renderer or editor 372 loadIcons(); 373 log.debug("icons loaded"); 374 } 375 return updateLabel((String) value, row); 376 } 377 378 @Override 379 public Component getTableCellEditorComponent( 380 JTable table, Object value, boolean isSelected, 381 int row, int column) { 382 log.debug("Renderer Item = {}, State = {}", row, value); 383 if (iconHeight < 0) { // load resources only first time, either for renderer or editor 384 loadIcons(); 385 log.debug("icons loaded"); 386 } 387 return updateLabel((String) value, row); 388 } 389 390 public JLabel updateLabel(String value, int row) { 391 if (iconHeight > 0) { // if necessary, increase row height; 392 log.debug("TODO adjust table row height for Lights?"); 393 //table.setRowHeight(row, Math.max(table.getRowHeight(), iconHeight - 5)); 394 } 395 if (value.equals(Bundle.getMessage("StateOff")) && offIcon != null) { 396 label = new JLabel(offIcon); 397 label.setVerticalAlignment(JLabel.BOTTOM); 398 log.debug("offIcon set"); 399 } else if (value.equals(Bundle.getMessage("StateOn")) && onIcon != null) { 400 label = new JLabel(onIcon); 401 label.setVerticalAlignment(JLabel.BOTTOM); 402 log.debug("onIcon set"); 403 } else if (value.equals(Bundle.getMessage("BeanStateInconsistent"))) { 404 label = new JLabel("X", JLabel.CENTER); // centered text alignment 405 label.setForeground(Color.red); 406 log.debug("Light state inconsistent"); 407 iconHeight = 0; 408 } else if (value.equals(Bundle.getMessage("LightStateIntermediate"))) { 409 label = new JLabel("...", JLabel.CENTER); // centered text alignment 410 log.debug("Light state in transition"); 411 iconHeight = 0; 412 } else { // failed to load icon 413 label = new JLabel(value, JLabel.CENTER); // centered text alignment 414 log.warn("Error reading icons for LightTable"); 415 iconHeight = 0; 416 } 417 label.setToolTipText(value); 418 label.addMouseListener(new MouseAdapter() { 419 @Override 420 public final void mousePressed(MouseEvent evt) { 421 log.debug("Clicked on icon in row {}", row); 422 stopCellEditing(); 423 } 424 }); 425 return label; 426 } 427 428 @Override 429 public Object getCellEditorValue() { 430 log.debug("getCellEditorValue, me = {})", this); 431 return this.toString(); 432 } 433 434 /** 435 * Read and buffer graphics. Only called once for this table. 436 * 437 * @see #getTableCellEditorComponent(JTable, Object, boolean, 438 * int, int) 439 */ 440 protected void loadIcons() { 441 try { 442 onImage = ImageIO.read(new File(onIconPath)); 443 offImage = ImageIO.read(new File(offIconPath)); 444 } catch (IOException ex) { 445 log.error("error reading image from {} or {}", onIconPath, offIconPath, ex); 446 } 447 log.debug("Success reading images"); 448 int imageWidth = onImage.getWidth(); 449 int imageHeight = onImage.getHeight(); 450 // scale icons 50% to fit in table rows 451 Image smallOnImage = onImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT); 452 Image smallOffImage = offImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT); 453 onIcon = new ImageIcon(smallOnImage); 454 offIcon = new ImageIcon(smallOffImage); 455 iconHeight = onIcon.getIconHeight(); 456 } 457 458 } 459 460 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LightTableDataModel.class); 461 462}