001package jmri.jmrit.beantable; 002 003import java.util.*; 004import javax.annotation.Nonnull; 005import javax.swing.*; 006 007import jmri.*; 008 009/** 010 * Model for a SignalHeadTable. 011 * 012 * Code originally located within SignalHeadTableAction.java 013 * 014 * @author Bob Jacobsen Copyright (C) 2003,2006,2007, 2008, 2009 015 * @author Petr Koud'a Copyright (C) 2007 016 * @author Egbert Broerse Copyright (C) 2016 017 * @author Steve Young Copyright (C) 2023 018 */ 019public class SignalHeadTableModel extends jmri.jmrit.beantable.BeanTableDataModel<SignalHead> { 020 021 static public final int LITCOL = NUMCOLUMN; 022 static public final int HELDCOL = LITCOL + 1; 023 static public final int EDITCOL = HELDCOL + 1; 024 025 public SignalHeadTableModel(){ 026 super(); 027 } 028 029 @Override 030 public int getColumnCount() { 031 return NUMCOLUMN + 3; 032 } 033 034 @Override 035 public String getColumnName(int col) { 036 switch (col) { 037 case VALUECOL: 038 return Bundle.getMessage("SignalMastAppearance"); // override default title, correct name SignalHeadAppearance i.e. "Red" 039 case LITCOL: 040 return Bundle.getMessage("ColumnHeadLit"); 041 case HELDCOL: 042 return Bundle.getMessage("ColumnHeadHeld"); 043 case EDITCOL: 044 return ""; // no heading on "Edit" 045 default: 046 return super.getColumnName(col); 047 } 048 } 049 050 @Override 051 public Class<?> getColumnClass(int col) { 052 switch (col) { 053 case VALUECOL: 054 return RowComboBoxPanel.class; // Use a JPanel containing a custom Appearance ComboBox 055 case LITCOL: 056 case HELDCOL: 057 return Boolean.class; 058 case EDITCOL: 059 return JButton.class; 060 default: 061 return super.getColumnClass(col); 062 } 063 } 064 065 @Override 066 public int getPreferredWidth(int col) { 067 switch (col) { 068 case LITCOL: 069 case HELDCOL: 070 return new JTextField(4).getPreferredSize().width; 071 case EDITCOL: 072 return new JTextField(7).getPreferredSize().width; 073 default: 074 return super.getPreferredWidth(col); 075 } 076 } 077 078 @Override 079 public boolean isCellEditable(int row, int col) { 080 switch (col) { 081 case LITCOL: 082 case HELDCOL: 083 case EDITCOL: 084 return true; 085 default: 086 return super.isCellEditable(row, col); 087 } 088 } 089 090 @Override 091 public Object getValueAt(int row, int col) { 092 // some error checking 093 if (row >= sysNameList.size()) { 094 log.debug("row is greater than name list"); 095 return "error"; 096 } 097 String name = sysNameList.get(row); 098 SignalHead s = InstanceManager.getDefault(SignalHeadManager.class).getBySystemName(name); 099 if (s == null) { 100 return Boolean.FALSE; // if due to race condition, the device is going away 101 } 102 switch (col) { 103 case LITCOL: 104 return s.getLit(); 105 case HELDCOL: 106 return s.getHeld(); 107 case EDITCOL: 108 return Bundle.getMessage("ButtonEdit"); 109 case VALUECOL: 110 String appearance = s.getAppearanceName(); 111 if ( !appearance.isEmpty()) { 112 return appearance; 113 } else { 114 //Appearance (head) not set 115 log.debug("No Appearance returned for head in row {}", row); 116 return Bundle.getMessage("BeanStateUnknown"); // use place holder string in table 117 } 118 default: 119 return super.getValueAt(row, col); 120 } 121 } 122 123 @Override 124 public void setValueAt(Object value, int row, int col) { 125 String name = sysNameList.get(row); 126 SignalHead s = InstanceManager.getDefault(SignalHeadManager.class).getBySystemName(name); 127 if (s == null) { 128 return; // device is going away anyway 129 } 130 switch (col) { 131 case VALUECOL: 132 if (value != null) { 133 //row = table.convertRowIndexToModel(row); // find the right row in model instead of table (not needed here) 134 log.debug("SignalHead setValueAt (rowConverted={}; value={})", row, value); 135 // convert from String (selected item) to int 136 int newState = 99; 137 String[] stateNameList = s.getValidStateNames(); // Array of valid appearance names 138 int[] validStateList = s.getValidStates(); // Array of valid appearance numbers 139 for (int i = 0; i < stateNameList.length; i++) { 140 if (value.equals(stateNameList[i])) { 141 newState = validStateList[i]; 142 break; 143 } 144 } 145 if (newState == 99) { 146 if (stateNameList.length == 0) { 147 newState = SignalHead.DARK; 148 log.warn("New signal state not found so setting to Dark {}", s.getDisplayName()); 149 } else { 150 newState = validStateList[0]; 151 log.warn("New signal state not found so setting to the first available {}", s.getDisplayName()); 152 } 153 } 154 log.debug("Signal Head set from: {} to: {} [{}]", s.getAppearanceName(), value, newState); 155 s.setAppearance(newState); 156 fireTableRowsUpdated(row, row); 157 } break; 158 case LITCOL: 159 s.setLit((Boolean) value); 160 break; 161 case HELDCOL: 162 s.setHeld((Boolean) value); 163 break; 164 case EDITCOL: 165 // button clicked - edit 166 editSignal(s); 167 break; 168 default: 169 super.setValueAt(value, row, col); 170 break; 171 } 172 } 173 174 @Override 175 public String getValue(String name) { 176 SignalHead s = InstanceManager.getDefault(SignalHeadManager.class).getBySystemName(name); 177 if (s == null) { 178 return "<lost>"; // if due to race condition, the device is going away 179 } 180 String val = null; 181 try { 182 val = s.getAppearanceName(); 183 } catch (java.lang.ArrayIndexOutOfBoundsException e) { 184 log.error("Could not get Appearance Name for {}", s.getDisplayName(), e); 185 } 186 if (val != null) { 187 return val; 188 } else { 189 return "Unexpected null value"; 190 } 191 } 192 193 @Override 194 public SignalHeadManager getManager() { 195 return InstanceManager.getDefault(SignalHeadManager.class); 196 } 197 198 @Override 199 public SignalHead getBySystemName(@Nonnull String name) { 200 return InstanceManager.getDefault(SignalHeadManager.class).getBySystemName(name); 201 } 202 203 @Override 204 public SignalHead getByUserName(@Nonnull String name) { 205 return InstanceManager.getDefault(SignalHeadManager.class).getByUserName(name); 206 } 207 208 @Override 209 protected String getMasterClassName() { 210 return SignalHeadTableAction.class.getName(); 211 } 212 213 @Override 214 public void clickOn(SignalHead t) { 215 } 216 217 /** 218 * Set column width. 219 * 220 * @return a button to fit inside the VALUE column 221 */ 222 @Override 223 public JButton configureButton() { 224 // pick a large size 225 JButton b = new JButton(Bundle.getMessage("SignalHeadStateYellow")); // about the longest Appearance string 226 b.putClientProperty("JComponent.sizeVariant", "small"); 227 b.putClientProperty("JButton.buttonType", "square"); 228 return b; 229 } 230 231 @Override 232 public boolean matchPropertyName(java.beans.PropertyChangeEvent e) { 233 if (e.getPropertyName().contains("Lit") || e.getPropertyName().contains("Held") || e.getPropertyName().contains("ValidStatesChanged")) { 234 return true; 235 } else { 236 return super.matchPropertyName(e); 237 } 238 } 239 240 @Override 241 protected String getBeanType() { 242 return Bundle.getMessage("BeanNameSignalHead"); 243 } 244 245 /** 246 * Respond to change from bean. Prevent Appearance change when 247 * Signal Head is set to Hold or Unlit. 248 * 249 * @param e A property change of any bean 250 */ 251 @Override 252 // Might be useful to show only a Dark option in the comboBox if head is Held 253 // At present, does not work/change when head Lit/Held checkboxes are (de)activated 254 public void propertyChange(java.beans.PropertyChangeEvent e) { 255 if (!e.getPropertyName().contains("Lit") || e.getPropertyName().contains("Held") || e.getPropertyName().contains("ValidStatesChanged")) { 256 if (e.getSource() instanceof NamedBean) { 257 String name = ((NamedBean) e.getSource()).getSystemName(); 258 if (log.isDebugEnabled()) { 259 log.debug("Update cell {}, {} for {}", sysNameList.indexOf(name), VALUECOL, name); 260 } 261 // since we can add columns, the entire row is marked as updated 262 int row = sysNameList.indexOf(name); 263 this.fireTableRowsUpdated(row, row); 264 clearAppearanceVector(row); // activate this method below 265 } 266 } 267 super.propertyChange(e); 268 } 269 270 /** 271 * Customize the SignalHead Value (Appearance) column to show an 272 * appropriate ComboBox of available Appearances when the 273 * TableDataModel is being called from ListedTableAction. 274 * 275 * @param table a JTable of Signal Head 276 */ 277 @Override 278 protected void configValueColumn(JTable table) { 279 // have the value column hold a JPanel with a JComboBox for Appearances 280 setColumnToHoldButton(table, VALUECOL, configureButton()); 281 // add extras, override BeanTableDataModel 282 log.debug("Head configValueColumn (I am {})", super.toString()); 283 table.setDefaultEditor(RowComboBoxPanel.class, new AppearanceComboBoxPanel()); 284 table.setDefaultRenderer(RowComboBoxPanel.class, new AppearanceComboBoxPanel()); // use same class for the renderer 285 // Set more things? 286 } 287 288 /** 289 * A row specific Appearance combobox cell editor/renderer. 290 */ 291 class AppearanceComboBoxPanel extends RowComboBoxPanel { 292 @Override 293 protected final void eventEditorMousePressed() { 294 this.editor.add(getEditorBox(table.convertRowIndexToModel(this.currentRow))); // add editorBox to JPanel 295 this.editor.revalidate(); 296 SwingUtilities.invokeLater(this.comboBoxFocusRequester); 297 log.debug("eventEditorMousePressed in row: {})", this.currentRow); 298 } 299 300 /** 301 * Call the method in the surrounding method for the 302 * SignalHeadTable. 303 * 304 * @param row the user clicked on in the table 305 * @return an appropriate combobox for this signal head 306 */ 307 @Override 308 protected JComboBox<String> getEditorBox(int row) { 309 return getAppearanceEditorBox(row); 310 } 311 } 312 313 /** 314 * Clear the old appearance comboboxes and force them to be rebuilt. 315 * Used with the Single Output Signal Head to capture reconfiguration. 316 * 317 * @param row Index of the signal mast (in TableDataModel) to be 318 * rebuilt in the Hashtables 319 */ 320 public void clearAppearanceVector(int row) { 321 boxMap.remove(this.getValueAt(row, SYSNAMECOL)); 322 editorMap.remove(this.getValueAt(row, SYSNAMECOL)); 323 } 324 325 // Hashtables for Editors; not used for Renderer) 326 /** 327 * Provide a JComboBox element to display inside the JPanel 328 * CellEditor. When not yet present, create, store and return a new 329 * one. 330 * 331 * @param row Index number (in TableDataModel) 332 * @return A combobox containing the valid appearance names for this 333 * mast 334 */ 335 public JComboBox<String> getAppearanceEditorBox(int row) { 336 JComboBox<String> editCombo = editorMap.get(this.getValueAt(row, SYSNAMECOL)); 337 if (editCombo == null) { 338 // create a new one with correct appearances 339 editCombo = new JComboBox<>(getRowVector(row)); 340 editorMap.put(this.getValueAt(row, SYSNAMECOL), editCombo); 341 } 342 return editCombo; 343 } 344 345 final Hashtable<Object, JComboBox<String>> editorMap = new Hashtable<>(); 346 347 /** 348 * Get a list of all the valid appearances that have not been 349 * disabled. 350 * 351 * @param head the name of the signal head 352 * @return List of valid signal head appearance names 353 */ 354 public Vector<String> getValidAppearances(SignalHead head) { 355 // convert String[] validStateNames to Vector 356 String[] app = head.getValidStateNames(); 357 Vector<String> v = new Vector<>(); 358 Collections.addAll(v, app); 359 return v; 360 } 361 362 /** 363 * Holds a Hashtable of valid appearances per signal head, used by 364 * getEditorBox() 365 * 366 * @param row Index number (in TableDataModel) 367 * @return The Vector of valid appearance names for this mast to 368 * show in the JComboBox 369 */ 370 Vector<String> getRowVector(int row) { 371 Vector<String> comboappearances = boxMap.get(this.getValueAt(row, SYSNAMECOL)); 372 if (comboappearances == null) { 373 // create a new one with right appearance 374 comboappearances = getValidAppearances((SignalHead) this.getValueAt(row, SYSNAMECOL)); 375 boxMap.put(this.getValueAt(row, SYSNAMECOL), comboappearances); 376 } 377 return comboappearances; 378 } 379 380 final Hashtable<Object, Vector<String>> boxMap = new Hashtable<>(); 381 382 // end of methods to display VALUECOL ComboBox 383 384 private SignalHeadAddEditFrame editFrame = null; 385 386 private void editSignal(@Nonnull final SignalHead head) { 387 // Signal Head was found, initialize for edit 388 log.debug("editPressed started for {}", head.getSystemName()); 389 // create the Edit Signal Head Window 390 // Use separate Runnable so window is created on top 391 Runnable t = () -> makeEditSignalWindow(head); 392 javax.swing.SwingUtilities.invokeLater(t); 393 } 394 395 private void makeEditSignalWindow(@Nonnull final SignalHead head) { 396 if (editFrame == null) { 397 editFrame = new SignalHeadAddEditFrame(head){ 398 @Override 399 public void dispose() { 400 editFrame = null; 401 super.dispose(); 402 } 403 }; 404 editFrame.initComponents(); 405 } else { 406 if (head.equals(editFrame.getSignalHead())) { 407 editFrame.setVisible(true); 408 } else { 409 log.error("Attempt to edit two signal heads at the same time-{}-and-{}-", editFrame.getSignalHead(), head.getSystemName()); 410 String msg = Bundle.getMessage("WarningEdit", editFrame.getSignalHead(), head.getSystemName()); 411 jmri.util.swing.JmriJOptionPane.showMessageDialog(editFrame, msg, 412 Bundle.getMessage("WarningTitle"), jmri.util.swing.JmriJOptionPane.ERROR_MESSAGE); 413 editFrame.setVisible(true); 414 } 415 } 416 } 417 418 @Override 419 public void dispose(){ 420 if ( editFrame != null ) { 421 editFrame.dispose(); 422 editFrame = null; 423 } 424 super.dispose(); 425 } 426 427 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SignalHeadTableModel.class); 428}