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 BeanTableDataModel<E> getTableDataModel() { 118 createModel(); 119 return m; 120 } 121 122 public void setFrame(@Nonnull BeanTableFrame<E> frame) { 123 f = frame; 124 } 125 126 public BeanTableFrame<E> getFrame() { 127 return f; 128 } 129 130 /** 131 * Allow subclasses to add to the frame without having to actually subclass 132 * the BeanTableDataFrame. 133 * 134 * @param f the Frame to add to 135 */ 136 public void addToFrame(@Nonnull BeanTableFrame<E> f) { 137 } 138 139 /** 140 * Allow subclasses to add to the frame without having to actually subclass 141 * the BeanTableDataFrame. 142 * 143 * @param tti the TabbedTableItem to add to 144 */ 145 public void addToFrame(@Nonnull ListedTableFrame.TabbedTableItem<E> tti) { 146 } 147 148 /** 149 * If the subClass is being included in a greater tabbed frame, then this 150 * method is used to add the details to the tabbed frame. 151 * 152 * @param f AbstractTableTabAction for the containing frame containing these 153 * and other tabs 154 */ 155 public void addToPanel(AbstractTableTabAction<E> f) { 156 } 157 158 /** 159 * If the subClass is being included in a greater tabbed frame, then this is 160 * used to specify which manager the subclass should be using. 161 * 162 * @param man Manager for this table tab 163 */ 164 protected void setManager(@Nonnull Manager<E> man) { 165 } 166 167 /** 168 * Get the Bean Manager in use by the TableAction. 169 * @return Bean Manager, could be Proxy or normal Manager, may be null. 170 */ 171 @CheckForNull 172 protected Manager<E> getManager(){ 173 return null; 174 } 175 176 /** 177 * Allow subclasses to alter the frame's Menubar without having to actually 178 * subclass the BeanTableDataFrame. 179 * 180 * @param f the Frame to attach the menubar to 181 */ 182 public void setMenuBar(BeanTableFrame<E> f) { 183 } 184 185 public JComponent getPanel() { 186 return null; 187 } 188 189 /** 190 * Perform configuration of the JTable as required by a specific TableAction. 191 * @param table The table to configure. 192 */ 193 protected void configureTable(JTable table){ 194 } 195 196 /** 197 * Dispose of the BeanTableDataModel ( if present ), 198 * which removes the DataModel property change listeners from Beans. 199 */ 200 public void dispose() { 201 if (m != null) { 202 m.dispose(); 203 } 204 // should this also dispose of the frame f? 205 } 206 207 /** 208 * Increments trailing digits of a system/user name (string) I.E. "Geo7" 209 * returns "Geo8" Note: preserves leading zeros: "Geo007" returns "Geo008" 210 * Also, if no trailing digits, appends "1": "Geo" returns "Geo1" 211 * 212 * @param name the system or user name string 213 * @return the same name with trailing digits incremented by one 214 */ 215 protected @Nonnull String nextName(@Nonnull String name) { 216 final String[] parts = name.split("(?=\\d+$)", 2); 217 String numString = "0"; 218 if (parts.length == 2) { 219 numString = parts[1]; 220 } 221 final int numStringLength = numString.length(); 222 final int num = Integer.parseInt(numString) + 1; 223 return parts[0] + String.format("%0" + numStringLength + "d", num); 224 } 225 226 /** 227 * Specify the JavaHelp target for this specific panel. 228 * 229 * @return a fixed default string "index" pointing to to highest level in 230 * JMRI Help 231 */ 232 protected String helpTarget() { 233 return "index"; // by default, go to the top 234 } 235 236 public String getClassDescription() { 237 return "Abstract Table Action"; 238 } 239 240 public void setMessagePreferencesDetails() { 241 HashMap<Integer, String> options = new HashMap<>(3); 242 options.put(0x00, Bundle.getMessage("DeleteAsk")); 243 options.put(0x01, Bundle.getMessage("DeleteNever")); 244 options.put(0x02, Bundle.getMessage("DeleteAlways")); 245 jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class).setMessageItemDetails(getClassName(), 246 "deleteInUse", Bundle.getMessage("DeleteItemInUse"), options, 0x00); 247 } 248 249 protected abstract String getClassName(); 250 251 /** 252 * Test if to include an Add New Button. 253 * @return true to include, else false. 254 */ 255 public boolean includeAddButton() { 256 return includeAddButton; 257 } 258 259 protected boolean includeAddButton = true; 260 261 /** 262 * Used with the Tabbed instances of table action, so that the print option 263 * is handled via that on the appropriate tab. 264 * 265 * @param mode table print mode 266 * @param headerFormat messageFormat for header 267 * @param footerFormat messageFormat for footer 268 */ 269 public void print(JTable.PrintMode mode, MessageFormat headerFormat, MessageFormat footerFormat) { 270 log.error("Printing not handled for {} tables.", m.getBeanType()); 271 } 272 273 protected abstract void addPressed(ActionEvent e); 274 275 /** 276 * Configure the combo box listing managers. 277 * Can be placed on Add New pane to select a connection for the new item. 278 * 279 * @param comboBox the combo box to configure 280 * @param manager the current manager 281 * @param managerClass the implemented manager class for the current 282 * manager; this is the class used by 283 * {@link InstanceManager#getDefault(Class)} to get the 284 * default manager, which may or may not be the current 285 * manager 286 */ 287 protected void configureManagerComboBox(ManagerComboBox<E> comboBox, Manager<E> manager, 288 Class<? extends Manager<E>> managerClass) { 289 Manager<E> defaultManager = InstanceManager.getDefault(managerClass); 290 // populate comboBox 291 if (defaultManager instanceof ProxyManager) { 292 comboBox.setManagers(defaultManager); 293 } else { 294 comboBox.setManagers(manager); 295 } 296 // set current selection 297 if (manager instanceof ProxyManager) { 298 UserPreferencesManager upm = InstanceManager.getDefault(UserPreferencesManager.class); 299 String systemSelectionCombo = this.getClass().getName() + ".SystemSelected"; 300 String userPref = upm.getComboBoxLastSelection(systemSelectionCombo); 301 if ( userPref != null) { 302 SystemConnectionMemo memo = SystemConnectionMemoManager.getDefault() 303 .getSystemConnectionMemoForUserName(userPref); 304 if (memo!=null) { 305 comboBox.setSelectedItem(memo.get(managerClass)); 306 } else { 307 ProxyManager<E> proxy = (ProxyManager<E>) manager; 308 comboBox.setSelectedItem(proxy.getDefaultManager()); 309 } 310 } else { 311 ProxyManager<E> proxy = (ProxyManager<E>) manager; 312 comboBox.setSelectedItem(proxy.getDefaultManager()); 313 } 314 } else { 315 comboBox.setSelectedItem(manager); 316 } 317 } 318 319 /** 320 * Remove the Add panel prefixBox listener before disposal. 321 * The listener is created when the Add panel is defined. It persists after the 322 * the Add panel has been disposed. When the next Add is created, AbstractTableAction 323 * sets the default connection as the current selection. This triggers validation before 324 * the new Add panel is created. 325 * <p> 326 * The listener is removed by the controlling table action before disposing of the Add 327 * panel after Close or Create. 328 * @param prefixBox The prefix combobox that might contain the listener. 329 */ 330 protected void removePrefixBoxListener(ManagerComboBox<E> prefixBox) { 331 Arrays.asList(prefixBox.getActionListeners()).forEach((l) -> { 332 prefixBox.removeActionListener(l); 333 }); 334 } 335 336 /** 337 * Display a warning to user about invalid entry. Needed as entry validation 338 * does not disable the Create button when full system name eg "LT1" is entered. 339 * 340 * @param curAddress address as entered in Add new... pane address field 341 * @param ex the exception that occurred 342 */ 343 protected void displayHwError(String curAddress, Exception ex) { 344 log.warn("Invalid Entry: {}",ex.getMessage()); 345 jmri.InstanceManager.getDefault(jmri.UserPreferencesManager .class). 346 showErrorMessage(Bundle.getMessage("ErrorTitle"), 347 Bundle.getMessage("ErrorConvertHW", curAddress),"" + ex,"", 348 true,false); 349 } 350 351 protected static class TableItem<E extends NamedBean> implements TableColumnModelListener { // E comes from the parent 352 353 BeanTableDataModel<E> dataModel; 354 JTable dataTable; 355 final AbstractTableAction<E> tableAction; 356 BeanTableFrame<E> beanTableFrame; 357 358 void setTableFrame(BeanTableFrame<E> frame){ 359 beanTableFrame = frame; 360 } 361 362 final TriStateJCheckBox propertyVisible = 363 new TriStateJCheckBox(Bundle.getMessage("ShowSystemSpecificProperties")); 364 365 public TableItem(@Nonnull AbstractTableAction<E> tableAction) { 366 this.tableAction = tableAction; 367 } 368 369 @SuppressWarnings("unchecked") 370 public AbstractTableAction<E> getAAClass() { 371 return tableAction; 372 } 373 374 public JTable getDataTable() { 375 return dataTable; 376 } 377 378 void includePropertyCheckBox() { 379 380 if (dataModel==null) { 381 log.error("datamodel for dataTable {} should not be null", dataTable); 382 return; 383 } 384 385 if (dataModel.getPropertyColumnCount() > 0) { 386 propertyVisible.setToolTipText(Bundle.getMessage 387 ("ShowSystemSpecificPropertiesToolTip")); 388 addToBottomBox(propertyVisible); 389 propertyVisible.addActionListener((ActionEvent e) -> 390 dataModel.setPropertyColumnsVisible(dataTable, propertyVisible.isSelected())); 391 } 392 fireColumnsUpdated(); // init bottom buttons 393 dataTable.getColumnModel().addColumnModelListener(this); 394 395 } 396 397 void includeAddButton(boolean includeAddButton){ 398 399 if (includeAddButton) { 400 JButton addButton = new JButton(Bundle.getMessage("ButtonAdd")); 401 addToBottomBox(addButton ); 402 addButton.addActionListener(tableAction::addPressed); 403 } 404 } 405 406 protected void addToBottomBox(JComponent comp) { 407 if (beanTableFrame != null ) { 408 beanTableFrame.addToBottomBox(comp, this.getClass().getName()); 409 } 410 } 411 412 /** 413 * Notify the subclasses that column visibility has been updated, 414 * or the table has finished loading. 415 * 416 * Sends notification to the tableAction with boolean array of column visibility. 417 * 418 */ 419 private void fireColumnsUpdated(){ 420 TableColumnModel model = dataTable.getColumnModel(); 421 if (model instanceof XTableColumnModel) { 422 Enumeration<TableColumn> e = ((XTableColumnModel) model).getColumns(false); 423 int numCols = ((XTableColumnModel) model).getColumnCount(false); 424 // XTableColumnModel has been spotted to return a fleeting different 425 // column count to actual model, generally if manager is changed at startup 426 // so we do a sanity check to make sure the models are in synch. 427 if (numCols != dataModel.getColumnCount()){ 428 log.debug("Difference with Xtable cols: {} Model cols: {}",numCols,dataModel.getColumnCount()); 429 return; 430 } 431 boolean[] colsVisible = new boolean[numCols]; 432 while (e.hasMoreElements()) { 433 TableColumn column = e.nextElement(); 434 boolean visible = ((XTableColumnModel) model).isColumnVisible(column); 435 colsVisible[column.getModelIndex()] = visible; 436 } 437 tableAction.columnsVisibleUpdated(colsVisible); 438 setPropertyVisibleCheckbox(colsVisible); 439 } 440 } 441 442 /** 443 * Updates the custom bean property columns checkbox. 444 * @param colsVisible array of column visibility 445 */ 446 private void setPropertyVisibleCheckbox(boolean[] colsVisible){ 447 int numberofCustomCols = dataModel.getPropertyColumnCount(); 448 if (numberofCustomCols>0){ 449 boolean[] customColVisibility = new boolean[numberofCustomCols]; 450 for ( int i=0; i<numberofCustomCols; i++){ 451 customColVisibility[i]=colsVisible[colsVisible.length-i-1]; 452 } 453 propertyVisible.setState(customColVisibility); 454 } 455 } 456 457 /** 458 * {@inheritDoc} 459 * A column is now visible. fireColumnsUpdated() 460 */ 461 @Override 462 public void columnAdded(TableColumnModelEvent e) { 463 fireColumnsUpdated(); 464 } 465 466 /** 467 * {@inheritDoc} 468 * A column is now hidden. fireColumnsUpdated() 469 */ 470 @Override 471 public void columnRemoved(TableColumnModelEvent e) { 472 fireColumnsUpdated(); 473 } 474 475 /** 476 * {@inheritDoc} 477 * Unused. 478 */ 479 @Override 480 public void columnMoved(TableColumnModelEvent e) {} 481 482 /** 483 * {@inheritDoc} 484 * Unused. 485 */ 486 @Override 487 public void columnSelectionChanged(ListSelectionEvent e) {} 488 489 /** 490 * {@inheritDoc} 491 * Unused. 492 */ 493 @Override 494 public void columnMarginChanged(ChangeEvent e) {} 495 496 protected void dispose() { 497 if (dataTable !=null ) { 498 dataTable.getColumnModel().removeColumnModelListener(this); 499 } 500 if (dataModel != null) { 501 dataModel.stopPersistingTable(dataTable); 502 dataModel.dispose(); 503 } 504 dataModel = null; 505 dataTable = null; 506 } 507 508 } 509 510 511 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractTableAction.class); 512 513}