001package jmri.jmrit.beantable; 002 003import java.awt.event.ActionEvent; 004import java.text.MessageFormat; 005import java.util.*; 006 007import javax.annotation.CheckForNull; 008import javax.annotation.Nonnull; 009import javax.swing.*; 010import javax.swing.event.*; 011import javax.swing.table.*; 012 013import jmri.InstanceManager; 014import jmri.Manager; 015import jmri.NamedBean; 016import jmri.ProxyManager; 017import jmri.UserPreferencesManager; 018import jmri.SystemConnectionMemo; 019import jmri.jmrix.SystemConnectionMemoManager; 020import jmri.swing.ManagerComboBox; 021import jmri.util.swing.TriStateJCheckBox; 022import jmri.util.swing.XTableColumnModel; 023 024/** 025 * Swing action to create and register a NamedBeanTable GUI. 026 * 027 * @param <E> type of NamedBean supported in this table 028 * @author Bob Jacobsen Copyright (C) 2003 029 */ 030public abstract class AbstractTableAction<E extends NamedBean> extends AbstractAction { 031 032 public AbstractTableAction(String actionName) { 033 super(actionName); 034 } 035 036 public AbstractTableAction(String actionName, Object option) { 037 super(actionName); 038 } 039 040 protected BeanTableDataModel<E> m; 041 042 /** 043 * Create the JTable DataModel, along with the changes for the specific 044 * NamedBean type. 045 */ 046 protected abstract void createModel(); 047 048 /** 049 * Include the correct title. 050 */ 051 protected abstract void setTitle(); 052 053 protected BeanTableFrame<E> f; 054 055 @Override 056 public void actionPerformed(ActionEvent e) { 057 // create the JTable model, with changes for specific NamedBean 058 createModel(); 059 TableRowSorter<BeanTableDataModel<E>> sorter = new TableRowSorter<>(m); 060 JTable dataTable = m.makeJTable(m.getMasterClassName(), m, sorter); 061 062 // allow reordering of the columns 063 dataTable.getTableHeader().setReorderingAllowed(true); 064 065 // create the frame 066 f = new BeanTableFrame<E>(m, helpTarget(), dataTable) { 067 068 /** 069 * Include an "Add..." button 070 */ 071 @Override 072 void extras() { 073 074 addBottomButtons(this, dataTable); 075 } 076 }; 077 setMenuBar(f); // comes after the Help menu is added by f = new 078 // BeanTableFrame(etc.) in stand alone application 079 configureTable(dataTable); 080 setTitle(); 081 addToFrame(f); 082 f.pack(); 083 f.setVisible(true); 084 } 085 086 @SuppressWarnings("unchecked") // revisit Java16+ if dm instanceof BeanTableDataModel<E> 087 protected void addBottomButtons(BeanTableFrame<E> ata, JTable dataTable ){ 088 089 TableItem<E> ti = new TableItem<>(this); 090 ti.setTableFrame(ata); 091 ti.includeAddButton(includeAddButton); 092 ti.dataTable = dataTable; 093 TableModel dm = dataTable.getModel(); 094 095 if ( dm instanceof BeanTableDataModel) { 096 ti.dataModel = (BeanTableDataModel<E>)dm; 097 } 098 ti.includePropertyCheckBox(); 099 100 } 101 102 /** 103 * Notification that column visibility for the JTable has updated. 104 * <p> 105 * This is overridden by classes which have column visibility Checkboxes on bottom bar. 106 * <p> 107 * 108 * Called on table startup and whenever a column goes hidden / visible. 109 * 110 * @param colsVisible array of ALL table columns and their visibility 111 * status in order of main Table Model, NOT XTableColumnModel. 112 */ 113 protected void columnsVisibleUpdated(boolean[] colsVisible){ 114 log.debug("columns updated {}",colsVisible); 115 } 116 117 public void setFrame(@Nonnull BeanTableFrame<E> frame) { 118 f = frame; 119 } 120 121 public BeanTableFrame<E> getFrame() { 122 return f; 123 } 124 125 /** 126 * Get the relevant data model for the current table. 127 * <p> This is overridden in the tabbed-table classes 128 * to return their own local data model. 129 * <p> Unlike {@link #getTableDataModel()}, this therefore 130 * doesn't attempt to (re)-create the model. 131 */ 132 public BeanTableDataModel<E> getDataModel() { 133 return m; 134 } 135 136 final public BeanTableDataModel<E> getTableDataModel() { 137 createModel(); 138 return m; 139 } 140 141 /** 142 * Allow subclasses to add to the frame without having to actually subclass 143 * the BeanTableDataFrame. 144 * 145 * @param f the Frame to add to 146 */ 147 public void addToFrame(@Nonnull BeanTableFrame<E> f) { 148 } 149 150 /** 151 * Allow subclasses to add to the frame without having to actually subclass 152 * the BeanTableDataFrame. 153 * 154 * @param tti the TabbedTableItem to add to 155 */ 156 public void addToFrame(@Nonnull ListedTableFrame.TabbedTableItem<E> tti) { 157 } 158 159 /** 160 * If the subClass is being included in a greater tabbed frame, then this 161 * method is used to add the details to the tabbed frame. 162 * 163 * @param f AbstractTableTabAction for the containing frame containing these 164 * and other tabs 165 */ 166 public void addToPanel(AbstractTableTabAction<E> f) { 167 } 168 169 /** 170 * If the subClass is being included in a greater tabbed frame, then this is 171 * used to specify which manager the subclass should be using. 172 * 173 * @param man Manager for this table tab 174 */ 175 protected void setManager(@Nonnull Manager<E> man) { 176 } 177 178 /** 179 * Get the Bean Manager in use by the TableAction. 180 * @return Bean Manager, could be Proxy or normal Manager, may be null. 181 */ 182 @CheckForNull 183 protected Manager<E> getManager(){ 184 return null; 185 } 186 187 /** 188 * Allow subclasses to alter the frame's Menubar without having to actually 189 * subclass the BeanTableDataFrame. 190 * 191 * @param f the Frame to attach the menubar to 192 */ 193 public void setMenuBar(BeanTableFrame<E> f) { 194 } 195 196 public JComponent getPanel() { 197 return null; 198 } 199 200 /** 201 * Perform configuration of the JTable as required by a specific TableAction. 202 * @param table The table to configure. 203 */ 204 protected void configureTable(JTable table){ 205 } 206 207 /** 208 * Dispose of the BeanTableDataModel ( if present ), 209 * which removes the DataModel property change listeners from Beans. 210 */ 211 public void dispose() { 212 if (m != null) { 213 m.dispose(); 214 } 215 // should this also dispose of the frame f? 216 } 217 218 /** 219 * Increments trailing digits of a system/user name (string) I.E. "Geo7" 220 * returns "Geo8" Note: preserves leading zeros: "Geo007" returns "Geo008" 221 * Also, if no trailing digits, appends "1": "Geo" returns "Geo1" 222 * 223 * @param name the system or user name string 224 * @return the same name with trailing digits incremented by one 225 */ 226 protected @Nonnull String nextName(@Nonnull String name) { 227 final String[] parts = name.split("(?=\\d+$)", 2); 228 String numString = "0"; 229 if (parts.length == 2) { 230 numString = parts[1]; 231 } 232 final int numStringLength = numString.length(); 233 final int num = Integer.parseInt(numString) + 1; 234 return parts[0] + String.format("%0" + numStringLength + "d", num); 235 } 236 237 /** 238 * Specify the JavaHelp target for this specific panel. 239 * 240 * @return a fixed default string "index" pointing to to highest level in 241 * JMRI Help 242 */ 243 protected String helpTarget() { 244 return "index"; // by default, go to the top 245 } 246 247 public String getClassDescription() { 248 return "Abstract Table Action"; 249 } 250 251 public void setMessagePreferencesDetails() { 252 HashMap<Integer, String> options = new HashMap<>(3); 253 options.put(0x00, Bundle.getMessage("DeleteAsk")); 254 options.put(0x01, Bundle.getMessage("DeleteNever")); 255 options.put(0x02, Bundle.getMessage("DeleteAlways")); 256 jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class).setMessageItemDetails(getClassName(), 257 "deleteInUse", Bundle.getMessage("DeleteItemInUse"), options, 0x00); 258 InstanceManager.getDefault(jmri.UserPreferencesManager.class).setPreferenceItemDetails(getClassName(), "remindSaveReLoad", Bundle.getMessage("HideMoveUserReminder")); 259 } 260 261 protected abstract String getClassName(); 262 263 /** 264 * Test if to include an Add New Button. 265 * @return true to include, else false. 266 */ 267 public boolean includeAddButton() { 268 return includeAddButton; 269 } 270 271 protected boolean includeAddButton = true; 272 273 /** 274 * Used with the Tabbed instances of table action, so that the print option 275 * is handled via that on the appropriate tab. 276 * 277 * @param mode table print mode 278 * @param headerFormat messageFormat for header 279 * @param footerFormat messageFormat for footer 280 */ 281 public void print(JTable.PrintMode mode, MessageFormat headerFormat, MessageFormat footerFormat) { 282 log.error("Printing not handled for {} tables.", m.getBeanType()); 283 } 284 285 protected abstract void addPressed(ActionEvent e); 286 287 /** 288 * Configure the combo box listing managers. 289 * Can be placed on Add New pane to select a connection for the new item. 290 * 291 * @param comboBox the combo box to configure 292 * @param manager the current manager 293 * @param managerClass the implemented manager class for the current 294 * manager; this is the class used by 295 * {@link InstanceManager#getDefault(Class)} to get the 296 * default manager, which may or may not be the current 297 * manager 298 */ 299 protected void configureManagerComboBox(ManagerComboBox<E> comboBox, Manager<E> manager, 300 Class<? extends Manager<E>> managerClass) { 301 Manager<E> defaultManager = InstanceManager.getDefault(managerClass); 302 // populate comboBox 303 if (defaultManager instanceof ProxyManager) { 304 comboBox.setManagers(defaultManager); 305 } else { 306 comboBox.setManagers(manager); 307 } 308 // set current selection 309 if (manager instanceof ProxyManager) { 310 UserPreferencesManager upm = InstanceManager.getDefault(UserPreferencesManager.class); 311 String systemSelectionCombo = this.getClass().getName() + ".SystemSelected"; 312 String userPref = upm.getComboBoxLastSelection(systemSelectionCombo); 313 if ( userPref != null) { 314 SystemConnectionMemo memo = SystemConnectionMemoManager.getDefault() 315 .getSystemConnectionMemoForUserName(userPref); 316 if (memo!=null) { 317 comboBox.setSelectedItem(memo.get(managerClass)); 318 } else { 319 ProxyManager<E> proxy = (ProxyManager<E>) manager; 320 comboBox.setSelectedItem(proxy.getDefaultManager()); 321 } 322 } else { 323 ProxyManager<E> proxy = (ProxyManager<E>) manager; 324 comboBox.setSelectedItem(proxy.getDefaultManager()); 325 } 326 } else { 327 comboBox.setSelectedItem(manager); 328 } 329 } 330 331 /** 332 * Remove the Add panel prefixBox listener before disposal. 333 * The listener is created when the Add panel is defined. It persists after the 334 * the Add panel has been disposed. When the next Add is created, AbstractTableAction 335 * sets the default connection as the current selection. This triggers validation before 336 * the new Add panel is created. 337 * <p> 338 * The listener is removed by the controlling table action before disposing of the Add 339 * panel after Close or Create. 340 * @param prefixBox The prefix combobox that might contain the listener. 341 */ 342 protected void removePrefixBoxListener(ManagerComboBox<E> prefixBox) { 343 Arrays.asList(prefixBox.getActionListeners()).forEach((l) -> { 344 prefixBox.removeActionListener(l); 345 }); 346 } 347 348 /** 349 * Display a warning to user about invalid entry. Needed as entry validation 350 * does not disable the Create button when full system name eg "LT1" is entered. 351 * 352 * @param curAddress address as entered in Add new... pane address field 353 * @param ex the exception that occurred 354 */ 355 protected void displayHwError(String curAddress, Exception ex) { 356 log.warn("Invalid Entry: {}",ex.getMessage()); 357 jmri.InstanceManager.getDefault(jmri.UserPreferencesManager .class). 358 showErrorMessage(Bundle.getMessage("ErrorTitle"), 359 Bundle.getMessage("ErrorConvertHW", curAddress),"" + ex,"", 360 true,false); 361 } 362 363 protected static class TableItem<E extends NamedBean> implements TableColumnModelListener { // E comes from the parent 364 365 BeanTableDataModel<E> dataModel; 366 JTable dataTable; 367 final AbstractTableAction<E> tableAction; 368 BeanTableFrame<E> beanTableFrame; 369 370 void setTableFrame(BeanTableFrame<E> frame){ 371 beanTableFrame = frame; 372 } 373 374 final TriStateJCheckBox propertyVisible = 375 new TriStateJCheckBox(Bundle.getMessage("ShowSystemSpecificProperties")); 376 377 public TableItem(@Nonnull AbstractTableAction<E> tableAction) { 378 this.tableAction = tableAction; 379 } 380 381 @SuppressWarnings("unchecked") 382 public AbstractTableAction<E> getAAClass() { 383 return tableAction; 384 } 385 386 public JTable getDataTable() { 387 return dataTable; 388 } 389 390 void includePropertyCheckBox() { 391 392 if (dataModel==null) { 393 log.error("datamodel for dataTable {} should not be null", dataTable); 394 return; 395 } 396 397 if (dataModel.getPropertyColumnCount() > 0) { 398 propertyVisible.setToolTipText(Bundle.getMessage 399 ("ShowSystemSpecificPropertiesToolTip")); 400 addToBottomBox(propertyVisible); 401 propertyVisible.addActionListener((ActionEvent e) -> 402 dataModel.setPropertyColumnsVisible(dataTable, propertyVisible.isSelected())); 403 } 404 fireColumnsUpdated(); // init bottom buttons 405 dataTable.getColumnModel().addColumnModelListener(this); 406 407 } 408 409 void includeAddButton(boolean includeAddButton){ 410 411 if (includeAddButton) { 412 JButton addButton = new JButton(Bundle.getMessage("ButtonAdd")); 413 addToBottomBox(addButton ); 414 addButton.addActionListener(tableAction::addPressed); 415 } 416 } 417 418 protected void addToBottomBox(JComponent comp) { 419 if (beanTableFrame != null ) { 420 beanTableFrame.addToBottomBox(comp, this.getClass().getName()); 421 } 422 } 423 424 /** 425 * Notify the subclasses that column visibility has been updated, 426 * or the table has finished loading. 427 * 428 * Sends notification to the tableAction with boolean array of column visibility. 429 * 430 */ 431 private void fireColumnsUpdated(){ 432 TableColumnModel model = dataTable.getColumnModel(); 433 if (model instanceof XTableColumnModel) { 434 Enumeration<TableColumn> e = ((XTableColumnModel) model).getColumns(false); 435 int numCols = ((XTableColumnModel) model).getColumnCount(false); 436 // XTableColumnModel has been spotted to return a fleeting different 437 // column count to actual model, generally if manager is changed at startup 438 // so we do a sanity check to make sure the models are in synch. 439 if (numCols != dataModel.getColumnCount()){ 440 log.debug("Difference with Xtable cols: {} Model cols: {}",numCols,dataModel.getColumnCount()); 441 return; 442 } 443 boolean[] colsVisible = new boolean[numCols]; 444 while (e.hasMoreElements()) { 445 TableColumn column = e.nextElement(); 446 boolean visible = ((XTableColumnModel) model).isColumnVisible(column); 447 colsVisible[column.getModelIndex()] = visible; 448 } 449 tableAction.columnsVisibleUpdated(colsVisible); 450 setPropertyVisibleCheckbox(colsVisible); 451 } 452 } 453 454 /** 455 * Updates the custom bean property columns checkbox. 456 * @param colsVisible array of column visibility 457 */ 458 private void setPropertyVisibleCheckbox(boolean[] colsVisible){ 459 int numberofCustomCols = dataModel.getPropertyColumnCount(); 460 if (numberofCustomCols>0){ 461 boolean[] customColVisibility = new boolean[numberofCustomCols]; 462 for ( int i=0; i<numberofCustomCols; i++){ 463 customColVisibility[i]=colsVisible[colsVisible.length-i-1]; 464 } 465 propertyVisible.setState(customColVisibility); 466 } 467 } 468 469 /** 470 * {@inheritDoc} 471 * A column is now visible. fireColumnsUpdated() 472 */ 473 @Override 474 public void columnAdded(TableColumnModelEvent e) { 475 fireColumnsUpdated(); 476 } 477 478 /** 479 * {@inheritDoc} 480 * A column is now hidden. fireColumnsUpdated() 481 */ 482 @Override 483 public void columnRemoved(TableColumnModelEvent e) { 484 fireColumnsUpdated(); 485 } 486 487 /** 488 * {@inheritDoc} 489 * Unused. 490 */ 491 @Override 492 public void columnMoved(TableColumnModelEvent e) {} 493 494 /** 495 * {@inheritDoc} 496 * Unused. 497 */ 498 @Override 499 public void columnSelectionChanged(ListSelectionEvent e) {} 500 501 /** 502 * {@inheritDoc} 503 * Unused. 504 */ 505 @Override 506 public void columnMarginChanged(ChangeEvent e) {} 507 508 protected void dispose() { 509 if (dataTable !=null ) { 510 dataTable.getColumnModel().removeColumnModelListener(this); 511 } 512 if (dataModel != null) { 513 dataModel.stopPersistingTable(dataTable); 514 dataModel.dispose(); 515 } 516 dataModel = null; 517 dataTable = null; 518 } 519 520 } 521 522 523 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractTableAction.class); 524 525}