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