001package jmri.jmrit.beantable; 002 003import java.awt.*; 004import java.awt.datatransfer.Clipboard; 005import java.awt.datatransfer.StringSelection; 006import java.awt.event.ActionEvent; 007import java.awt.event.ActionListener; 008import java.awt.event.KeyEvent; 009import java.beans.PropertyChangeEvent; 010import java.beans.PropertyChangeListener; 011import java.beans.PropertyVetoException; 012import java.io.IOException; 013import java.text.DateFormat; 014import java.text.MessageFormat; 015import java.util.ArrayList; 016import java.util.Date; 017import java.util.Enumeration; 018import java.util.EventObject; 019import java.util.List; 020import java.util.Objects; 021import java.util.function.Predicate; 022import java.util.stream.Stream; 023 024import javax.annotation.CheckForNull; 025import javax.annotation.Nonnull; 026import javax.annotation.OverridingMethodsMustInvokeSuper; 027import javax.swing.*; 028import javax.swing.table.*; 029 030import jmri.*; 031import jmri.NamedBean.DisplayOptions; 032import jmri.jmrit.display.layoutEditor.LayoutBlock; 033import jmri.jmrit.display.layoutEditor.LayoutBlockManager; 034import jmri.swing.JTablePersistenceManager; 035import jmri.util.davidflanagan.HardcopyWriter; 036import jmri.util.swing.*; 037import jmri.util.table.ButtonEditor; 038import jmri.util.table.ButtonRenderer; 039 040/** 041 * Abstract Table data model for display of NamedBean manager contents. 042 * 043 * @author Bob Jacobsen Copyright (C) 2003 044 * @author Dennis Miller Copyright (C) 2006 045 * @param <T> the type of NamedBean supported by this model 046 */ 047abstract public class BeanTableDataModel<T extends NamedBean> extends AbstractTableModel implements PropertyChangeListener { 048 049 static public final int SYSNAMECOL = 0; 050 static public final int USERNAMECOL = 1; 051 static public final int VALUECOL = 2; 052 static public final int COMMENTCOL = 3; 053 static public final int DELETECOL = 4; 054 static public final int NUMCOLUMN = 5; 055 protected List<String> sysNameList = null; 056 private NamedBeanHandleManager nbMan; 057 private Predicate<? super T> filter; 058 059 /** 060 * Create a new Bean Table Data Model. 061 * The default Manager for the bean type may well be a Proxy Manager. 062 */ 063 public BeanTableDataModel() { 064 super(); 065 initModel(); 066 } 067 068 /** 069 * Internal routine to avoid over ride method call in constructor. 070 */ 071 private void initModel(){ 072 nbMan = InstanceManager.getDefault(NamedBeanHandleManager.class); 073 // log.error("get mgr is: {}",this.getManager()); 074 getManager().addPropertyChangeListener(this); 075 updateNameList(); 076 } 077 078 /** 079 * Get the total number of custom bean property columns. 080 * Proxy managers will return the total number of custom columns for all 081 * hardware types of that Bean type. 082 * Single hardware types will return the total just for that hardware. 083 * @return total number of custom columns within the table. 084 */ 085 protected int getPropertyColumnCount() { 086 return getManager().getKnownBeanProperties().size(); 087 } 088 089 /** 090 * Get the Named Bean Property Descriptor for a given column number. 091 * @param column table column number. 092 * @return the descriptor if available, else null. 093 */ 094 @CheckForNull 095 protected NamedBeanPropertyDescriptor<?> getPropertyColumnDescriptor(int column) { 096 List<NamedBeanPropertyDescriptor<?>> propertyColumns = getManager().getKnownBeanProperties(); 097 int totalCount = getColumnCount(); 098 int propertyCount = propertyColumns.size(); 099 int tgt = column - (totalCount - propertyCount); 100 if (tgt < 0 || tgt >= propertyCount ) { 101 return null; 102 } 103 return propertyColumns.get(tgt); 104 } 105 106 protected synchronized void updateNameList() { 107 // first, remove listeners from the individual objects 108 if (sysNameList != null) { 109 for (String s : sysNameList) { 110 // if object has been deleted, it's not here; ignore it 111 T b = getBySystemName(s); 112 if (b != null) { 113 b.removePropertyChangeListener(this); 114 } 115 } 116 } 117 Stream<T> stream = getManager().getNamedBeanSet().stream(); 118 if (filter != null) stream = stream.filter(filter); 119 sysNameList = stream.map(NamedBean::getSystemName).collect( java.util.stream.Collectors.toList() ); 120 // and add them back in 121 for (String s : sysNameList) { 122 // if object has been deleted, it's not here; ignore it 123 T b = getBySystemName(s); 124 if (b != null) { 125 b.addPropertyChangeListener(this); 126 } 127 } 128 } 129 130 /** 131 * {@inheritDoc} 132 */ 133 @Override 134 public void propertyChange(PropertyChangeEvent e) { 135 if (e.getPropertyName().equals("length")) { 136 // a new NamedBean is available in the manager 137 updateNameList(); 138 log.debug("Table changed length to {}", sysNameList.size()); 139 fireTableDataChanged(); 140 } else if (matchPropertyName(e)) { 141 // a value changed. Find it, to avoid complete redraw 142 if (e.getSource() instanceof NamedBean) { 143 String name = ((NamedBean) e.getSource()).getSystemName(); 144 int row = sysNameList.indexOf(name); 145 log.debug("Update cell {},{} for {}", row, VALUECOL, name); 146 // since we can add columns, the entire row is marked as updated 147 try { 148 fireTableRowsUpdated(row, row); 149 } catch (Exception ex) { 150 log.error("Exception updating table", ex); 151 } 152 } 153 } 154 } 155 156 /** 157 * Is this property event announcing a change this table should display? 158 * <p> 159 * Note that events will come both from the NamedBeans and also from the 160 * manager 161 * 162 * @param e the event to match 163 * @return true if the property name is of interest, false otherwise 164 */ 165 protected boolean matchPropertyName(PropertyChangeEvent e) { 166 var name = e.getPropertyName().toLowerCase(); 167 return (name.contains("state") 168 || name.contains("value") 169 || name.contains("appearance") 170 || name.contains("comment") 171 || name.contains("username") 172 || name.contains("commanded") 173 || name.contains("known")); 174 } 175 176 /** 177 * {@inheritDoc} 178 */ 179 @Override 180 public int getRowCount() { 181 return sysNameList.size(); 182 } 183 184 /** 185 * Get Column Count INCLUDING Bean Property Columns. 186 * {@inheritDoc} 187 */ 188 @Override 189 public int getColumnCount() { 190 return NUMCOLUMN + getPropertyColumnCount(); 191 } 192 193 /** 194 * {@inheritDoc} 195 */ 196 @Override 197 public String getColumnName(int col) { 198 switch (col) { 199 case SYSNAMECOL: 200 return Bundle.getMessage("ColumnSystemName"); // "System Name"; 201 case USERNAMECOL: 202 return Bundle.getMessage("ColumnUserName"); // "User Name"; 203 case VALUECOL: 204 return Bundle.getMessage("ColumnState"); // "State"; 205 case COMMENTCOL: 206 return Bundle.getMessage("ColumnComment"); // "Comment"; 207 case DELETECOL: 208 return ""; 209 default: 210 NamedBeanPropertyDescriptor<?> desc = getPropertyColumnDescriptor(col); 211 if (desc == null) { 212 return "btm unknown"; // NOI18N 213 } 214 return desc.getColumnHeaderText(); 215 } 216 } 217 218 /** 219 * {@inheritDoc} 220 */ 221 @Override 222 public Class<?> getColumnClass(int col) { 223 switch (col) { 224 case SYSNAMECOL: 225 return NamedBean.class; // can't get class of T 226 case USERNAMECOL: 227 case COMMENTCOL: 228 return String.class; 229 case VALUECOL: 230 case DELETECOL: 231 return JButton.class; 232 default: 233 NamedBeanPropertyDescriptor<?> desc = getPropertyColumnDescriptor(col); 234 if (desc == null) { 235 return null; 236 } 237 if ( desc instanceof SelectionPropertyDescriptor ){ 238 return JComboBox.class; 239 } 240 return desc.getValueClass(); 241 } 242 } 243 244 /** 245 * {@inheritDoc} 246 */ 247 @Override 248 public boolean isCellEditable(int row, int col) { 249 String uname; 250 switch (col) { 251 case VALUECOL: 252 case COMMENTCOL: 253 case DELETECOL: 254 return true; 255 case USERNAMECOL: 256 T b = getBySystemName(sysNameList.get(row)); 257 uname = b.getUserName(); 258 return ((uname == null) || uname.isEmpty()); 259 default: 260 NamedBeanPropertyDescriptor<?> desc = getPropertyColumnDescriptor(col); 261 if (desc == null) { 262 return false; 263 } 264 return desc.isEditable(getBySystemName(sysNameList.get(row))); 265 } 266 } 267 268 /** 269 * 270 * SYSNAMECOL returns the actual Bean, NOT the System Name. 271 * 272 * {@inheritDoc} 273 */ 274 @Override 275 public Object getValueAt(int row, int col) { 276 T b; 277 switch (col) { 278 case SYSNAMECOL: // slot number 279 return getBySystemName(sysNameList.get(row)); 280 case USERNAMECOL: // return user name 281 // sometimes, the TableSorter invokes this on rows that no longer exist, so we check 282 b = getBySystemName(sysNameList.get(row)); 283 return (b != null) ? b.getUserName() : null; 284 case VALUECOL: // 285 return getValue(sysNameList.get(row)); 286 case COMMENTCOL: 287 b = getBySystemName(sysNameList.get(row)); 288 return (b != null) ? b.getComment() : null; 289 case DELETECOL: // 290 return Bundle.getMessage("ButtonDelete"); 291 default: 292 NamedBeanPropertyDescriptor<?> desc = getPropertyColumnDescriptor(col); 293 if (desc == null) { 294 log.error("internal state inconsistent with table requst for getValueAt {} {}", row, col); 295 return null; 296 } 297 if ( !isCellEditable(row, col) ) { 298 return null; // do not display if not applicable to hardware type 299 } 300 b = getBySystemName(sysNameList.get(row)); 301 Object value = b.getProperty(desc.propertyKey); 302 if (desc instanceof SelectionPropertyDescriptor){ 303 JComboBox<String> c = new JComboBox<>(((SelectionPropertyDescriptor) desc).getOptions()); 304 c.setSelectedItem(( value!=null ? value.toString() : desc.defaultValue.toString() )); 305 ComboBoxToolTipRenderer renderer = new ComboBoxToolTipRenderer(); 306 c.setRenderer(renderer); 307 renderer.setTooltips(((SelectionPropertyDescriptor) desc).getOptionToolTips()); 308 return c; 309 } 310 if (value == null) { 311 return desc.defaultValue; 312 } 313 return value; 314 } 315 } 316 317 public int getPreferredWidth(int col) { 318 switch (col) { 319 case SYSNAMECOL: 320 return new JTextField(5).getPreferredSize().width; 321 case COMMENTCOL: 322 case USERNAMECOL: 323 return new JTextField(15).getPreferredSize().width; // TODO I18N using Bundle.getMessage() 324 case VALUECOL: // not actually used due to the configureTable, setColumnToHoldButton, configureButton 325 case DELETECOL: // not actually used due to the configureTable, setColumnToHoldButton, configureButton 326 return new JTextField(Bundle.getMessage("ButtonDelete")).getPreferredSize().width; 327 default: 328 NamedBeanPropertyDescriptor<?> desc = getPropertyColumnDescriptor(col); 329 if (desc == null || desc.getColumnHeaderText() == null) { 330 log.error("Unexpected column in getPreferredWidth: {} table {}", col,this); 331 return new JTextField(8).getPreferredSize().width; 332 } 333 return new JTextField(desc.getColumnHeaderText()).getPreferredSize().width; 334 } 335 } 336 337 /** 338 * Get the current Bean state value in human readable form. 339 * @param systemName System name of Bean. 340 * @return state value in localised human readable form. 341 */ 342 abstract public String getValue(String systemName); 343 344 /** 345 * Get the Table Model Bean Manager. 346 * In many cases, especially around Model startup, 347 * this will be the Proxy Manager, which is then changed to the 348 * hardware specific manager. 349 * @return current Manager in use by the Model. 350 */ 351 abstract protected Manager<T> getManager(); 352 353 /** 354 * Set the Model Bean Manager. 355 * Note that for many Models this may not work as the manager is 356 * currently obtained directly from the Action class. 357 * 358 * @param man Bean Manager that the Model should use. 359 */ 360 protected void setManager(@Nonnull Manager<T> man) { 361 } 362 363 abstract protected T getBySystemName(@Nonnull String name); 364 365 abstract protected T getByUserName(@Nonnull String name); 366 367 /** 368 * Process a click on The value cell. 369 * @param t the Bean that has been clicked. 370 */ 371 abstract protected void clickOn(T t); 372 373 public int getDisplayDeleteMsg() { 374 return InstanceManager.getDefault(UserPreferencesManager.class).getMultipleChoiceOption(getMasterClassName(), "deleteInUse"); 375 } 376 377 public void setDisplayDeleteMsg(int boo) { 378 InstanceManager.getDefault(UserPreferencesManager.class).setMultipleChoiceOption(getMasterClassName(), "deleteInUse", boo); 379 } 380 381 abstract protected String getMasterClassName(); 382 383 /** 384 * {@inheritDoc} 385 */ 386 @Override 387 public void setValueAt(Object value, int row, int col) { 388 switch (col) { 389 case USERNAMECOL: 390 // Directly changing the username should only be possible if the username was previously null or "" 391 // check to see if user name already exists 392 if (value.equals("")) { 393 value = null; 394 } else { 395 T nB = getByUserName((String) value); 396 if (nB != null) { 397 log.error("User name is not unique {}", value); 398 String msg = Bundle.getMessage("WarningUserName", "" + value); 399 JmriJOptionPane.showMessageDialog(null, msg, 400 Bundle.getMessage("WarningTitle"), 401 JmriJOptionPane.ERROR_MESSAGE); 402 return; 403 } 404 } 405 T nBean = getBySystemName(sysNameList.get(row)); 406 nBean.setUserName((String) value); 407 if (nbMan.inUse(sysNameList.get(row), nBean)) { 408 String msg = Bundle.getMessage("UpdateToUserName", getBeanType(), value, sysNameList.get(row)); 409 int optionPane = JmriJOptionPane.showConfirmDialog(null, 410 msg, Bundle.getMessage("UpdateToUserNameTitle"), 411 JmriJOptionPane.YES_NO_OPTION); 412 if (optionPane == JmriJOptionPane.YES_OPTION) { 413 //This will update the bean reference from the systemName to the userName 414 try { 415 nbMan.updateBeanFromSystemToUser(nBean); 416 } catch (JmriException ex) { 417 //We should never get an exception here as we already check that the username is not valid 418 log.error("Impossible exception setting user name", ex); 419 } 420 } 421 } 422 break; 423 case COMMENTCOL: 424 getBySystemName(sysNameList.get(row)).setComment( 425 (String) value); 426 break; 427 case VALUECOL: 428 // button fired, swap state 429 T t = getBySystemName(sysNameList.get(row)); 430 clickOn(t); 431 break; 432 case DELETECOL: 433 // button fired, delete Bean 434 deleteBean(row, col); 435 return; // manager will update rows if a delete occurs 436 default: 437 NamedBeanPropertyDescriptor<?> desc = getPropertyColumnDescriptor(col); 438 if (desc == null) { 439 log.error("btdm setvalueat {} {}",row,col); 440 break; 441 } 442 if (value instanceof JComboBox) { 443 value = ((JComboBox<?>) value).getSelectedItem(); 444 } 445 NamedBean b = getBySystemName(sysNameList.get(row)); 446 b.setProperty(desc.propertyKey, value); 447 } 448 fireTableRowsUpdated(row, row); 449 } 450 451 protected void deleteBean(int row, int col) { 452 jmri.util.ThreadingUtil.runOnGUI(() -> { 453 try { 454 var worker = new DeleteBeanWorker(getBySystemName(sysNameList.get(row))); 455 log.debug("Delete Bean {}", worker.toString()); 456 } catch (Exception e ){ 457 log.error("Exception while deleting bean", e); 458 } 459 }); 460 } 461 462 /** 463 * Delete the bean after all the checking has been done. 464 * <p> 465 * Separate so that it can be easily subclassed if other functionality is 466 * needed. 467 * 468 * @param bean NamedBean to delete 469 */ 470 protected void doDelete(T bean) { 471 try { 472 getManager().deleteBean(bean, "DoDelete"); 473 } catch (PropertyVetoException e) { 474 //At this stage the DoDelete shouldn't fail, as we have already done a can delete, which would trigger a veto 475 log.error("doDelete should not fail after canDelete. {}", e.getMessage()); 476 } 477 } 478 479 /** 480 * Configure a table to have our standard rows and columns. This is 481 * optional, in that other table formats can use this table model. But we 482 * put it here to help keep it consistent. 483 * This also persists the table user interface state. 484 * 485 * @param table {@link JTable} to configure 486 */ 487 public void configureTable(JTable table) { 488 // Property columns will be invisible at start. 489 setPropertyColumnsVisible(table, false); 490 491 table.setDefaultRenderer(JComboBox.class, new BtValueRenderer()); 492 table.setDefaultEditor(JComboBox.class, new BtComboboxEditor()); 493 table.setDefaultRenderer(Boolean.class, new EnablingCheckboxRenderer()); 494 table.setDefaultRenderer(Date.class, new DateRenderer()); 495 496 // allow reordering of the columns 497 table.getTableHeader().setReorderingAllowed(true); 498 499 // have to shut off autoResizeMode to get horizontal scroll to work (JavaSwing p 541) 500 table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); 501 502 XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel(); 503 for (int i = 0; i < columnModel.getColumnCount(false); i++) { 504 505 // resize columns as requested 506 int width = getPreferredWidth(i); 507 columnModel.getColumnByModelIndex(i).setPreferredWidth(width); 508 509 } 510 table.sizeColumnsToFit(-1); 511 512 configValueColumn(table); 513 configDeleteColumn(table); 514 515 JmriMouseListener popupListener = new PopupListener(); 516 table.addMouseListener(JmriMouseListener.adapt(popupListener)); 517 this.persistTable(table); 518 } 519 520 protected void configValueColumn(JTable table) { 521 // have the value column hold a button 522 setColumnToHoldButton(table, VALUECOL, configureButton()); 523 } 524 525 public JButton configureButton() { 526 // pick a large size 527 JButton b = new JButton(Bundle.getMessage("BeanStateInconsistent")); 528 b.putClientProperty("JComponent.sizeVariant", "small"); 529 b.putClientProperty("JButton.buttonType", "square"); 530 return b; 531 } 532 533 protected void configDeleteColumn(JTable table) { 534 // have the delete column hold a button 535 setColumnToHoldButton(table, DELETECOL, 536 new JButton(Bundle.getMessage("ButtonDelete"))); 537 } 538 539 /** 540 * Service method to setup a column so that it will hold a button for its 541 * values. 542 * 543 * @param table {@link JTable} to use 544 * @param column index for column to setup 545 * @param sample typical button, used to determine preferred size 546 */ 547 protected void setColumnToHoldButton(JTable table, int column, JButton sample) { 548 // install a button renderer & editor 549 ButtonRenderer buttonRenderer = new ButtonRenderer(); 550 table.setDefaultRenderer(JButton.class, buttonRenderer); 551 TableCellEditor buttonEditor = new ButtonEditor(new JButton()); 552 table.setDefaultEditor(JButton.class, buttonEditor); 553 // ensure the table rows, columns have enough room for buttons 554 table.setRowHeight(sample.getPreferredSize().height); 555 table.getColumnModel().getColumn(column) 556 .setPreferredWidth((sample.getPreferredSize().width) + 4); 557 } 558 559 /** 560 * Removes property change listeners from Beans. 561 */ 562 public synchronized void dispose() { 563 getManager().removePropertyChangeListener(this); 564 if (sysNameList != null) { 565 for (String s : sysNameList) { 566 T b = getBySystemName(s); 567 if (b != null) { 568 b.removePropertyChangeListener(this); 569 } 570 } 571 } 572 } 573 574 /** 575 * Method to self print or print preview the table. Printed in equally sized 576 * columns across the page with headings and vertical lines between each 577 * column. Data is word wrapped within a column. Can handle data as strings, 578 * comboboxes or booleans 579 * 580 * @param w the printer writer 581 */ 582 public void printTable(HardcopyWriter w) { 583 // determine the column size - evenly sized, with space between for lines 584 int columnSize = (w.getCharactersPerLine() - this.getColumnCount() - 1) / this.getColumnCount(); 585 586 // Draw horizontal dividing line 587 w.write(w.getCurrentLineNumber(), 0, w.getCurrentLineNumber(), 588 (columnSize + 1) * this.getColumnCount()); 589 590 // print the column header labels 591 String[] columnStrings = new String[this.getColumnCount()]; 592 // Put each column header in the array 593 for (int i = 0; i < this.getColumnCount(); i++) { 594 columnStrings[i] = this.getColumnName(i); 595 } 596 w.setFontStyle(Font.BOLD); 597 printColumns(w, columnStrings, columnSize); 598 w.setFontStyle(0); 599 w.write(w.getCurrentLineNumber(), 0, w.getCurrentLineNumber(), 600 (columnSize + 1) * this.getColumnCount()); 601 602 // now print each row of data 603 // create a base string the width of the column 604 StringBuilder spaces = new StringBuilder(); // NOI18N 605 for (int i = 0; i < columnSize; i++) { 606 spaces.append(" "); // NOI18N 607 } 608 for (int i = 0; i < this.getRowCount(); i++) { 609 for (int j = 0; j < this.getColumnCount(); j++) { 610 //check for special, non string contents 611 Object value = this.getValueAt(i, j); 612 if (value == null) { 613 columnStrings[j] = spaces.toString(); 614 } else if (value instanceof JComboBox<?>) { 615 columnStrings[j] = Objects.requireNonNull(((JComboBox<?>) value).getSelectedItem()).toString(); 616 } else { 617 // Boolean or String 618 columnStrings[j] = value.toString(); 619 } 620 } 621 printColumns(w, columnStrings, columnSize); 622 w.write(w.getCurrentLineNumber(), 0, w.getCurrentLineNumber(), 623 (columnSize + 1) * this.getColumnCount()); 624 } 625 w.close(); 626 } 627 628 protected void printColumns(HardcopyWriter w, String[] columnStrings, int columnSize) { 629 // create a base string the width of the column 630 StringBuilder spaces = new StringBuilder(); // NOI18N 631 for (int i = 0; i < columnSize; i++) { 632 spaces.append(" "); // NOI18N 633 } 634 // loop through each column 635 boolean complete = false; 636 while (!complete) { 637 StringBuilder lineString = new StringBuilder(); // NOI18N 638 complete = true; 639 for (int i = 0; i < columnStrings.length; i++) { 640 String columnString = ""; // NOI18N 641 // if the column string is too wide cut it at word boundary (valid delimiters are space, - and _) 642 // use the intial part of the text,pad it with spaces and place the remainder back in the array 643 // for further processing on next line 644 // if column string isn't too wide, pad it to column width with spaces if needed 645 if (columnStrings[i].length() > columnSize) { 646 boolean noWord = true; 647 for (int k = columnSize; k >= 1; k--) { 648 if (columnStrings[i].charAt(k - 1) == ' ' 649 || columnStrings[i].charAt(k - 1) == '-' 650 || columnStrings[i].charAt(k - 1) == '_') { 651 columnString = columnStrings[i].substring(0, k) 652 + spaces.substring(columnStrings[i].substring(0, k).length()); 653 columnStrings[i] = columnStrings[i].substring(k); 654 noWord = false; 655 complete = false; 656 break; 657 } 658 } 659 if (noWord) { 660 columnString = columnStrings[i].substring(0, columnSize); 661 columnStrings[i] = columnStrings[i].substring(columnSize); 662 complete = false; 663 } 664 665 } else { 666 columnString = columnStrings[i] + spaces.substring(columnStrings[i].length()); 667 columnStrings[i] = ""; 668 } 669 lineString.append(columnString).append(" "); // NOI18N 670 } 671 try { 672 w.write(lineString.toString()); 673 //write vertical dividing lines 674 for (int i = 0; i < w.getCharactersPerLine(); i = i + columnSize + 1) { 675 w.write(w.getCurrentLineNumber(), i, w.getCurrentLineNumber() + 1, i); 676 } 677 w.write("\n"); // NOI18N 678 } catch (IOException e) { 679 log.warn("error during printing: {}", e.getMessage()); 680 } 681 } 682 } 683 684 /** 685 * Export the contents of table to a CSV file. 686 * <p> 687 * The content is exported in column order from the table model 688 * <p> 689 * If the provided file name is null, the user will be 690 * prompted with a file dialog. 691 */ 692 @SuppressWarnings("unchecked") // have to run-time cast to JComboBox<Object> after check of JComboBox<?> 693 public void exportToCSV(java.io.File file) { 694 695 if (file == null) { 696 // prompt user for file 697 var chooser = new JFileChooser(jmri.util.FileUtil.getUserFilesPath()); 698 int retVal = chooser.showSaveDialog(null); 699 if (retVal != JFileChooser.APPROVE_OPTION) { 700 log.info("Export to CSV abandoned"); 701 return; // give up if no file selected 702 } 703 file = chooser.getSelectedFile(); 704 } 705 706 try { 707 var fileWriter = new java.io.FileWriter(file); 708 var bufferedWriter = new java.io.BufferedWriter(fileWriter); 709 var csvFile = new org.apache.commons.csv.CSVPrinter(bufferedWriter, 710 org.apache.commons.csv.CSVFormat.DEFAULT); 711 712 for (int i = 0; i < getColumnCount(); i++) { 713 csvFile.print(getColumnName(i)); 714 } 715 csvFile.println(); 716 717 for (int i = 0; i < getRowCount(); i++) { 718 for (int j = 0; j < getColumnCount(); j++) { 719 var value = getValueAt(i, j); 720 if (value instanceof JComboBox<?>) { 721 value = ((JComboBox<Object>)value).getSelectedItem().toString(); 722 } 723 csvFile.print(value); 724 } 725 csvFile.println(); 726 } 727 728 csvFile.flush(); 729 csvFile.close(); 730 731 } catch (java.io.IOException e) { 732 log.error("Failed to write file",e); 733 } 734 735 } 736 737 /** 738 * Create and configure a new table using the given model and row sorter. 739 * 740 * @param name the name of the table 741 * @param model the data model for the table 742 * @param sorter the row sorter for the table; if null, the table will not 743 * be sortable 744 * @return the table 745 * @throws NullPointerException if name or model is null 746 */ 747 public JTable makeJTable(@Nonnull String name, @Nonnull TableModel model, @CheckForNull RowSorter<? extends TableModel> sorter) { 748 Objects.requireNonNull(name, "the table name must be nonnull"); 749 Objects.requireNonNull(model, "the table model must be nonnull"); 750 JTable table = new JTable(model) { 751 752 // TODO: Create base BeanTableJTable.java, 753 // extend TurnoutTableJTable from it as next 2 classes duplicate. 754 755 @Override 756 public String getToolTipText(java.awt.event.MouseEvent e) { 757 java.awt.Point p = e.getPoint(); 758 int rowIndex = rowAtPoint(p); 759 int colIndex = columnAtPoint(p); 760 int realRowIndex = convertRowIndexToModel(rowIndex); 761 int realColumnIndex = convertColumnIndexToModel(colIndex); 762 return getCellToolTip(this, realRowIndex, realColumnIndex); 763 } 764 765 /** 766 * Disable Windows Key or Mac Meta Keys being pressed acting 767 * as a trigger for editing the focused cell. 768 * Causes unexpected behaviour, i.e. button presses. 769 * {@inheritDoc} 770 */ 771 @Override 772 public boolean editCellAt(int row, int column, EventObject e) { 773 if (e instanceof KeyEvent) { 774 if ( ((KeyEvent) e).getKeyCode() == KeyEvent.VK_WINDOWS 775 || ( (KeyEvent) e).getKeyCode() == KeyEvent.VK_META ) { 776 return false; 777 } 778 } 779 return super.editCellAt(row, column, e); 780 } 781 }; 782 return this.configureJTable(name, table, sorter); 783 } 784 785 /** 786 * Configure a new table using the given model and row sorter. 787 * 788 * @param table the table to configure 789 * @param name the table name 790 * @param sorter the row sorter for the table; if null, the table will not 791 * be sortable 792 * @return the table 793 * @throws NullPointerException if table or the table name is null 794 */ 795 protected JTable configureJTable(@Nonnull String name, @Nonnull JTable table, @CheckForNull RowSorter<? extends TableModel> sorter) { 796 Objects.requireNonNull(table, "the table must be nonnull"); 797 Objects.requireNonNull(name, "the table name must be nonnull"); 798 table.setRowSorter(sorter); 799 table.setName(name); 800 table.getTableHeader().setReorderingAllowed(true); 801 table.setColumnModel(new XTableColumnModel()); 802 table.createDefaultColumnsFromModel(); 803 addMouseListenerToHeader(table); 804 table.getTableHeader().setDefaultRenderer(new BeanTableTooltipHeaderRenderer(table.getTableHeader().getDefaultRenderer())); 805 return table; 806 } 807 808 /** 809 * Get String of the Single Bean Type. 810 * In many cases the return is Bundle localised 811 * so should not be used for matching Bean types. 812 * 813 * @return Bean Type String. 814 */ 815 protected String getBeanType(){ 816 return getManager().getBeanTypeHandled(false); 817 } 818 819 /** 820 * Updates the visibility settings of the property columns. 821 * 822 * @param table the JTable object for the current display. 823 * @param visible true to make the property columns visible, false to hide. 824 */ 825 public void setPropertyColumnsVisible(JTable table, boolean visible) { 826 XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel(); 827 for (int i = getColumnCount() - 1; i >= getColumnCount() - getPropertyColumnCount(); --i) { 828 TableColumn column = columnModel.getColumnByModelIndex(i); 829 columnModel.setColumnVisible(column, visible); 830 } 831 } 832 833 /** 834 * Is a bean allowed to have the user name cleared? 835 * @return true if clear is allowed, false otherwise 836 */ 837 protected boolean isClearUserNameAllowed() { 838 return true; 839 } 840 841 /** 842 * Display popup menu when right clicked on table cell. 843 * <p> 844 * Copy UserName 845 * Rename 846 * Remove UserName 847 * Move 848 * Edit Comment 849 * Delete 850 * @param e source event. 851 */ 852 protected void showPopup(JmriMouseEvent e) { 853 JTable source = (JTable) e.getSource(); 854 int row = source.rowAtPoint(e.getPoint()); 855 int column = source.columnAtPoint(e.getPoint()); 856 if (!source.isRowSelected(row)) { 857 source.changeSelection(row, column, false, false); 858 } 859 final int rowindex = source.convertRowIndexToModel(row); 860 861 JPopupMenu popupMenu = new JPopupMenu(); 862 JMenuItem menuItem = new JMenuItem(Bundle.getMessage("CopyName")); 863 menuItem.addActionListener((ActionEvent e1) -> copyName(rowindex, 0)); 864 popupMenu.add(menuItem); 865 866 menuItem = new JMenuItem(Bundle.getMessage("Rename")); 867 menuItem.addActionListener((ActionEvent e1) -> renameBean(rowindex, 0)); 868 popupMenu.add(menuItem); 869 870 if (isClearUserNameAllowed()) { 871 menuItem = new JMenuItem(Bundle.getMessage("ClearName")); 872 menuItem.addActionListener((ActionEvent e1) -> removeName(rowindex, 0)); 873 popupMenu.add(menuItem); 874 } 875 876 menuItem = new JMenuItem(Bundle.getMessage("MoveName")); 877 menuItem.addActionListener((ActionEvent e1) -> moveBean(rowindex, 0)); 878 if (getRowCount() == 1) { 879 menuItem.setEnabled(false); // you can't move when there is just 1 item (to other table? 880 } 881 popupMenu.add(menuItem); 882 883 menuItem = new JMenuItem(Bundle.getMessage("EditComment")); 884 menuItem.addActionListener((ActionEvent e1) -> editComment(rowindex, 0)); 885 popupMenu.add(menuItem); 886 887 menuItem = new JMenuItem(Bundle.getMessage("ButtonDelete")); 888 menuItem.addActionListener((ActionEvent e1) -> deleteBean(rowindex, 0)); 889 popupMenu.add(menuItem); 890 891 popupMenu.show(e.getComponent(), e.getX(), e.getY()); 892 } 893 894 public void copyName(int row, int column) { 895 T nBean = getBySystemName(sysNameList.get(row)); 896 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 897 StringSelection name = new StringSelection(nBean.getUserName()); 898 clipboard.setContents(name, null); 899 } 900 901 /** 902 * Change the bean User Name in a dialog. 903 * 904 * @param row table model row number of bean 905 * @param column always passed in as 0, not used 906 */ 907 public void renameBean(int row, int column) { 908 T nBean = getBySystemName(sysNameList.get(row)); 909 String oldName = (nBean.getUserName() == null ? "" : nBean.getUserName()); 910 String newName = JmriJOptionPane.showInputDialog(null, 911 Bundle.getMessage("RenameFrom", getBeanType(), "\"" +oldName+"\""), oldName); 912 if (newName == null || newName.equals(nBean.getUserName())) { 913 // name not changed 914 return; 915 } else { 916 T nB = getByUserName(newName); 917 if (nB != null) { 918 log.error("User name is not unique {}", newName); 919 String msg = Bundle.getMessage("WarningUserName", "" + newName); 920 JmriJOptionPane.showMessageDialog(null, msg, 921 Bundle.getMessage("WarningTitle"), 922 JmriJOptionPane.ERROR_MESSAGE); 923 return; 924 } 925 } 926 927 if (!allowBlockNameChange("Rename", nBean, newName)) { 928 return; // NOI18N 929 } 930 931 try { 932 nBean.setUserName(newName); 933 } catch (NamedBean.BadSystemNameException | NamedBean.BadUserNameException ex) { 934 JmriJOptionPane.showMessageDialog(null, ex.getLocalizedMessage(), 935 Bundle.getMessage("ErrorTitle"), // NOI18N 936 JmriJOptionPane.ERROR_MESSAGE); 937 return; 938 } 939 940 fireTableRowsUpdated(row, row); 941 if (!newName.isEmpty()) { 942 if (oldName == null || oldName.isEmpty()) { 943 if (!nbMan.inUse(sysNameList.get(row), nBean)) { 944 return; 945 } 946 String msg = Bundle.getMessage("UpdateToUserName", getBeanType(), newName, sysNameList.get(row)); 947 int optionPane = JmriJOptionPane.showConfirmDialog(null, 948 msg, Bundle.getMessage("UpdateToUserNameTitle"), 949 JmriJOptionPane.YES_NO_OPTION); 950 if (optionPane == JmriJOptionPane.YES_OPTION) { 951 //This will update the bean reference from the systemName to the userName 952 try { 953 nbMan.updateBeanFromSystemToUser(nBean); 954 } catch (JmriException ex) { 955 //We should never get an exception here as we already check that the username is not valid 956 log.error("Impossible exception renaming Bean", ex); 957 } 958 } 959 } else { 960 nbMan.renameBean(oldName, newName, nBean); 961 } 962 963 } else { 964 //This will update the bean reference from the old userName to the SystemName 965 nbMan.updateBeanFromUserToSystem(nBean); 966 } 967 } 968 969 public void removeName(int row, int column) { 970 T nBean = getBySystemName(sysNameList.get(row)); 971 if (!allowBlockNameChange("Remove", nBean, "")) return; // NOI18N 972 String msg = Bundle.getMessage("UpdateToSystemName", getBeanType()); 973 int optionPane = JmriJOptionPane.showConfirmDialog(null, 974 msg, Bundle.getMessage("UpdateToSystemNameTitle"), 975 JmriJOptionPane.YES_NO_OPTION); 976 if (optionPane == JmriJOptionPane.YES_OPTION) { 977 nbMan.updateBeanFromUserToSystem(nBean); 978 } 979 nBean.setUserName(null); 980 fireTableRowsUpdated(row, row); 981 } 982 983 /** 984 * Determine whether it is safe to rename/remove a Block user name. 985 * <p>The user name is used by the LayoutBlock to link to the block and 986 * by Layout Editor track components to link to the layout block. 987 * 988 * @param changeType This will be Remove or Rename. 989 * @param bean The affected bean. Only the Block bean is of interest. 990 * @param newName For Remove this will be empty, for Rename it will be the new user name. 991 * @return true to continue with the user name change. 992 */ 993 boolean allowBlockNameChange(String changeType, T bean, String newName) { 994 if (!(bean instanceof jmri.Block)) { 995 return true; 996 } 997 // If there is no layout block or the block name is empty, Block rename and remove are ok without notification. 998 String oldName = bean.getUserName(); 999 if (oldName == null) return true; 1000 LayoutBlock layoutBlock = jmri.InstanceManager.getDefault(LayoutBlockManager.class).getByUserName(oldName); 1001 if (layoutBlock == null) return true; 1002 1003 // Remove is not allowed if there is a layout block 1004 if (changeType.equals("Remove")) { 1005 log.warn("Cannot remove user name for block {}", oldName); // NOI18N 1006 JmriJOptionPane.showMessageDialog(null, 1007 Bundle.getMessage("BlockRemoveUserNameWarning", oldName), // NOI18N 1008 Bundle.getMessage("WarningTitle"), // NOI18N 1009 JmriJOptionPane.WARNING_MESSAGE); 1010 return false; 1011 } 1012 1013 // Confirmation dialog 1014 int optionPane = JmriJOptionPane.showConfirmDialog(null, 1015 Bundle.getMessage("BlockChangeUserName", oldName, newName), // NOI18N 1016 Bundle.getMessage("QuestionTitle"), // NOI18N 1017 JmriJOptionPane.YES_NO_OPTION); 1018 return optionPane == JmriJOptionPane.YES_OPTION; 1019 } 1020 1021 public void moveBean(int row, int column) { 1022 final T t = getBySystemName(sysNameList.get(row)); 1023 String currentName = t.getUserName(); 1024 T oldNameBean = getBySystemName(sysNameList.get(row)); 1025 1026 if ((currentName == null) || currentName.isEmpty()) { 1027 JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("MoveDialogErrorMessage")); 1028 return; 1029 } 1030 1031 JComboBox<String> box = new JComboBox<>(); 1032 getManager().getNamedBeanSet().forEach((T b) -> { 1033 //Only add items that do not have a username assigned. 1034 String userName = b.getUserName(); 1035 if (userName == null || userName.isEmpty()) { 1036 box.addItem(b.getSystemName()); 1037 } 1038 }); 1039 1040 int retval = JmriJOptionPane.showOptionDialog(null, 1041 Bundle.getMessage("MoveDialog", getBeanType(), currentName, oldNameBean.getSystemName()), 1042 Bundle.getMessage("MoveDialogTitle"), 1043 JmriJOptionPane.YES_NO_OPTION, JmriJOptionPane.INFORMATION_MESSAGE, null, 1044 new Object[]{Bundle.getMessage("ButtonCancel"), Bundle.getMessage("ButtonOK"), box}, null); 1045 log.debug("Dialog value {} selected {}:{}", retval, box.getSelectedIndex(), box.getSelectedItem()); 1046 if (retval != 1) { 1047 return; 1048 } 1049 String entry = (String) box.getSelectedItem(); 1050 assert entry != null; 1051 T newNameBean = getBySystemName(entry); 1052 if (oldNameBean != newNameBean) { 1053 oldNameBean.setUserName(null); 1054 newNameBean.setUserName(currentName); 1055 InstanceManager.getDefault(NamedBeanHandleManager.class).moveBean(oldNameBean, newNameBean, currentName); 1056 if (nbMan.inUse(newNameBean.getSystemName(), newNameBean)) { 1057 String msg = Bundle.getMessage("UpdateToUserName", getBeanType(), currentName, sysNameList.get(row)); 1058 int optionPane = JmriJOptionPane.showConfirmDialog(null, msg, Bundle.getMessage("UpdateToUserNameTitle"), JmriJOptionPane.YES_NO_OPTION); 1059 if (optionPane == JmriJOptionPane.YES_OPTION) { 1060 try { 1061 nbMan.updateBeanFromSystemToUser(newNameBean); 1062 } catch (JmriException ex) { 1063 //We should never get an exception here as we already check that the username is not valid 1064 log.error("Impossible exception moving Bean", ex); 1065 } 1066 } 1067 } 1068 fireTableRowsUpdated(row, row); 1069 InstanceManager.getDefault(UserPreferencesManager.class). 1070 showInfoMessage(Bundle.getMessage("ReminderTitle"), 1071 Bundle.getMessage("UpdateComplete", getBeanType()), 1072 getMasterClassName(), "remindSaveReLoad"); 1073 } 1074 } 1075 1076 public void editComment(int row, int column) { 1077 T nBean = getBySystemName(sysNameList.get(row)); 1078 JTextArea commentField = new JTextArea(5, 50); 1079 JScrollPane commentFieldScroller = new JScrollPane(commentField); 1080 commentField.setText(nBean.getComment()); 1081 Object[] editCommentOption = {Bundle.getMessage("ButtonCancel"), Bundle.getMessage("ButtonUpdate")}; 1082 int retval = JmriJOptionPane.showOptionDialog(null, 1083 commentFieldScroller, Bundle.getMessage("EditComment"), 1084 JmriJOptionPane.YES_NO_OPTION, JmriJOptionPane.INFORMATION_MESSAGE, null, 1085 editCommentOption, editCommentOption[1]); 1086 if (retval != 1) { 1087 return; 1088 } 1089 nBean.setComment(commentField.getText()); 1090 } 1091 1092 /** 1093 * Display the comment text for the current row as a tool tip. 1094 * 1095 * Most of the bean tables use the standard model with comments in column 3. 1096 * 1097 * @param table The current table. 1098 * @param row The current row. 1099 * @param col The current column. 1100 * @return a formatted tool tip or null if there is none. 1101 */ 1102 public String getCellToolTip(JTable table, int row, int col) { 1103 String tip = null; 1104 T nBean = getBySystemName(sysNameList.get(row)); 1105 if (nBean != null) { 1106 tip = formatToolTip(nBean.getRecommendedToolTip()); 1107 } 1108 return tip; 1109 } 1110 1111 /** 1112 * Get a ToolTip for a Table Column Header. 1113 * @param columnModelIndex the model column number. 1114 * @return ToolTip, else null. 1115 */ 1116 @OverridingMethodsMustInvokeSuper 1117 protected String getHeaderTooltip(int columnModelIndex) { 1118 return null; 1119 } 1120 1121 /** 1122 * Format a tool tip string. Multi line tooltips are supported. 1123 * @param tooltip The tooltip string to be formatted 1124 * @return a html formatted string or null if the comment is empty. 1125 */ 1126 protected String formatToolTip(String tooltip) { 1127 String tip = null; 1128 if (tooltip != null && !tooltip.isEmpty()) { 1129 tip = "<html>" + tooltip.replaceAll(System.getProperty("line.separator"), "<br>") + "</html>"; 1130 } 1131 return tip; 1132 } 1133 1134 /** 1135 * Show the Table Column Menu. 1136 * @param e Instigating event ( e.g. from Mouse click ) 1137 * @param table table to get columns from 1138 */ 1139 protected void showTableHeaderPopup(JmriMouseEvent e, JTable table) { 1140 JPopupMenu popupMenu = new JPopupMenu(); 1141 XTableColumnModel tcm = (XTableColumnModel) table.getColumnModel(); 1142 for (int i = 0; i < tcm.getColumnCount(false); i++) { 1143 TableColumn tc = tcm.getColumnByModelIndex(i); 1144 String columnName = table.getModel().getColumnName(i); 1145 if (columnName != null && !columnName.isEmpty()) { 1146 StayOpenCheckBoxItem menuItem = new StayOpenCheckBoxItem(table.getModel().getColumnName(i), tcm.isColumnVisible(tc)); 1147 menuItem.addActionListener(new HeaderActionListener(tc, tcm)); 1148 TableModel mod = table.getModel(); 1149 if (mod instanceof BeanTableDataModel<?>) { 1150 menuItem.setToolTipText(((BeanTableDataModel<?>)mod).getHeaderTooltip(i)); 1151 } 1152 popupMenu.add(menuItem); 1153 } 1154 1155 } 1156 popupMenu.show(e.getComponent(), e.getX(), e.getY()); 1157 } 1158 1159 protected void addMouseListenerToHeader(JTable table) { 1160 JmriMouseListener mouseHeaderListener = new TableHeaderListener(table); 1161 table.getTableHeader().addMouseListener(JmriMouseListener.adapt(mouseHeaderListener)); 1162 } 1163 1164 /** 1165 * Persist the state of the table after first setting the table to the last 1166 * persisted state. 1167 * 1168 * @param table the table to persist 1169 * @throws NullPointerException if the name of the table is null 1170 */ 1171 public void persistTable(@Nonnull JTable table) throws NullPointerException { 1172 InstanceManager.getOptionalDefault(JTablePersistenceManager.class).ifPresent((manager) -> { 1173 setColumnIdentities(table); 1174 manager.resetState(table); // throws NPE if table name is null 1175 manager.persist(table); 1176 }); 1177 } 1178 1179 /** 1180 * Stop persisting the state of the table. 1181 * 1182 * @param table the table to stop persisting 1183 * @throws NullPointerException if the name of the table is null 1184 */ 1185 public void stopPersistingTable(@Nonnull JTable table) throws NullPointerException { 1186 InstanceManager.getOptionalDefault(JTablePersistenceManager.class).ifPresent((manager) -> { 1187 manager.stopPersisting(table); // throws NPE if table name is null 1188 }); 1189 } 1190 1191 /** 1192 * Set identities for any columns that need an identity. 1193 * 1194 * It is recommended that all columns get a constant identity to 1195 * prevent identities from being subject to changes due to translation. 1196 * <p> 1197 * The default implementation sets column identities to the String 1198 * {@code Column#} where {@code #} is the model index for the column. 1199 * Note that if the TableColumnModel is a {@link jmri.util.swing.XTableColumnModel}, 1200 * the index includes hidden columns. 1201 * 1202 * @param table the table to set identities for. 1203 */ 1204 protected void setColumnIdentities(JTable table) { 1205 Objects.requireNonNull(table.getModel(), "Table must have data model"); 1206 Objects.requireNonNull(table.getColumnModel(), "Table must have column model"); 1207 Enumeration<TableColumn> columns; 1208 if (table.getColumnModel() instanceof XTableColumnModel) { 1209 columns = ((XTableColumnModel) table.getColumnModel()).getColumns(false); 1210 } else { 1211 columns = table.getColumnModel().getColumns(); 1212 } 1213 int i = 0; 1214 while (columns.hasMoreElements()) { 1215 TableColumn column = columns.nextElement(); 1216 if (column.getIdentifier() == null || column.getIdentifier().toString().isEmpty()) { 1217 column.setIdentifier(String.format("Column%d", i)); 1218 } 1219 i += 1; 1220 } 1221 } 1222 1223 protected class BeanTableTooltipHeaderRenderer extends DefaultTableCellRenderer { 1224 private final TableCellRenderer _existingRenderer; 1225 1226 protected BeanTableTooltipHeaderRenderer(TableCellRenderer existingRenderer) { 1227 _existingRenderer = existingRenderer; 1228 } 1229 1230 @Override 1231 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 1232 1233 Component rendererComponent = _existingRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 1234 TableModel mod = table.getModel(); 1235 if ( rendererComponent instanceof JLabel && mod instanceof BeanTableDataModel<?> ) { // Set the cell ToolTip 1236 int modelIndex = table.getColumnModel().getColumn(column).getModelIndex(); 1237 String tooltip = ((BeanTableDataModel<?>)mod).getHeaderTooltip(modelIndex); 1238 ((JLabel)rendererComponent).setToolTipText(tooltip); 1239 } 1240 return rendererComponent; 1241 } 1242 } 1243 1244 /** 1245 * Listener class which processes Column Menu button clicks. 1246 * Does not allow the last column to be hidden, 1247 * otherwise there would be no table header to recover the column menu / columns from. 1248 */ 1249 static class HeaderActionListener implements ActionListener { 1250 1251 private final TableColumn tc; 1252 private final XTableColumnModel tcm; 1253 1254 HeaderActionListener(TableColumn tc, XTableColumnModel tcm) { 1255 this.tc = tc; 1256 this.tcm = tcm; 1257 } 1258 1259 @Override 1260 public void actionPerformed(ActionEvent e) { 1261 JCheckBoxMenuItem check = (JCheckBoxMenuItem) e.getSource(); 1262 //Do not allow the last column to be hidden 1263 if (!check.isSelected() && tcm.getColumnCount(true) == 1) { 1264 return; 1265 } 1266 tcm.setColumnVisible(tc, check.isSelected()); 1267 } 1268 } 1269 1270 class DeleteBeanWorker { 1271 1272 public DeleteBeanWorker(final T bean) { 1273 1274 StringBuilder message = new StringBuilder(); 1275 try { 1276 getManager().deleteBean(bean, "CanDelete"); // NOI18N 1277 } catch (PropertyVetoException e) { 1278 if (e.getPropertyChangeEvent().getPropertyName().equals("DoNotDelete")) { // NOI18N 1279 log.warn("Should not delete {}, {}", bean.getDisplayName((DisplayOptions.USERNAME_SYSTEMNAME)), e.getMessage()); 1280 message.append(Bundle.getMessage("VetoDeleteBean", bean.getBeanType(), bean.getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME), e.getMessage())); 1281 JmriJOptionPane.showMessageDialog(null, message.toString(), 1282 Bundle.getMessage("WarningTitle"), 1283 JmriJOptionPane.ERROR_MESSAGE); 1284 return; 1285 } 1286 message.append(e.getMessage()); 1287 } 1288 int count = bean.getListenerRefs().size(); 1289 log.debug("Delete with {}", count); 1290 if (getDisplayDeleteMsg() == 0x02 && message.toString().isEmpty()) { 1291 doDelete(bean); 1292 } else { 1293 JPanel container = new JPanel(); 1294 container.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); 1295 container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS)); 1296 if (count > 0) { // warn of listeners attached before delete 1297 1298 JLabel question = new JLabel(Bundle.getMessage("DeletePrompt", bean.getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME))); 1299 question.setAlignmentX(Component.CENTER_ALIGNMENT); 1300 container.add(question); 1301 1302 ArrayList<String> listenerRefs = bean.getListenerRefs(); 1303 if (!listenerRefs.isEmpty()) { 1304 ArrayList<String> listeners = new ArrayList<>(); 1305 for (String listenerRef : listenerRefs) { 1306 if (!listeners.contains(listenerRef)) { 1307 listeners.add(listenerRef); 1308 } 1309 } 1310 1311 message.append("<br>"); 1312 message.append(Bundle.getMessage("ReminderInUse", count)); 1313 message.append("<ul>"); 1314 for (String listener : listeners) { 1315 message.append("<li>"); 1316 message.append(listener); 1317 message.append("</li>"); 1318 } 1319 message.append("</ul>"); 1320 1321 JEditorPane pane = new JEditorPane(); 1322 pane.setContentType("text/html"); 1323 pane.setText("<html>" + message.toString() + "</html>"); 1324 pane.setEditable(false); 1325 JScrollPane jScrollPane = new JScrollPane(pane); 1326 container.add(jScrollPane); 1327 } 1328 } else { 1329 String msg = MessageFormat.format( 1330 Bundle.getMessage("DeletePrompt"), bean.getSystemName()); 1331 JLabel question = new JLabel(msg); 1332 question.setAlignmentX(Component.CENTER_ALIGNMENT); 1333 container.add(question); 1334 } 1335 1336 final JCheckBox remember = new JCheckBox(Bundle.getMessage("MessageRememberSetting")); 1337 remember.setFont(remember.getFont().deriveFont(10f)); 1338 remember.setAlignmentX(Component.CENTER_ALIGNMENT); 1339 1340 container.add(remember); 1341 container.setAlignmentX(Component.CENTER_ALIGNMENT); 1342 container.setAlignmentY(Component.CENTER_ALIGNMENT); 1343 String[] options = new String[]{JmriJOptionPane.YES_STRING, JmriJOptionPane.NO_STRING}; 1344 int result = JmriJOptionPane.showOptionDialog(null, container, Bundle.getMessage("WarningTitle"), 1345 JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.WARNING_MESSAGE, null, 1346 options, JmriJOptionPane.NO_STRING); 1347 1348 if ( result == 0 ){ // first item in Array is Yes 1349 if (remember.isSelected()) { 1350 setDisplayDeleteMsg(0x02); 1351 } 1352 doDelete(bean); 1353 } 1354 1355 } 1356 } 1357 } 1358 1359 /** 1360 * Listener to trigger display of table cell menu. 1361 * Delete / Rename / Move etc. 1362 */ 1363 class PopupListener extends JmriMouseAdapter { 1364 1365 /** 1366 * {@inheritDoc} 1367 */ 1368 @Override 1369 public void mousePressed(JmriMouseEvent e) { 1370 if (e.isPopupTrigger()) { 1371 showPopup(e); 1372 } 1373 } 1374 1375 /** 1376 * {@inheritDoc} 1377 */ 1378 @Override 1379 public void mouseReleased(JmriMouseEvent e) { 1380 if (e.isPopupTrigger()) { 1381 showPopup(e); 1382 } 1383 } 1384 } 1385 1386 /** 1387 * Listener to trigger display of table header column menu. 1388 */ 1389 class TableHeaderListener extends JmriMouseAdapter { 1390 1391 private final JTable table; 1392 1393 TableHeaderListener(JTable tbl) { 1394 super(); 1395 table = tbl; 1396 } 1397 1398 /** 1399 * {@inheritDoc} 1400 */ 1401 @Override 1402 public void mousePressed(JmriMouseEvent e) { 1403 if (e.isPopupTrigger()) { 1404 showTableHeaderPopup(e, table); 1405 } 1406 } 1407 1408 /** 1409 * {@inheritDoc} 1410 */ 1411 @Override 1412 public void mouseReleased(JmriMouseEvent e) { 1413 if (e.isPopupTrigger()) { 1414 showTableHeaderPopup(e, table); 1415 } 1416 } 1417 1418 /** 1419 * {@inheritDoc} 1420 */ 1421 @Override 1422 public void mouseClicked(JmriMouseEvent e) { 1423 if (e.isPopupTrigger()) { 1424 showTableHeaderPopup(e, table); 1425 } 1426 } 1427 } 1428 1429 private class BtComboboxEditor extends jmri.jmrit.symbolicprog.ValueEditor { 1430 1431 BtComboboxEditor(){ 1432 super(); 1433 } 1434 1435 @Override 1436 public Component getTableCellEditorComponent(JTable table, Object value, 1437 boolean isSelected, 1438 int row, int column) { 1439 if (value instanceof JComboBox) { 1440 ((JComboBox<?>) value).addActionListener((ActionEvent e1) -> table.getCellEditor().stopCellEditing()); 1441 } 1442 1443 if (value instanceof JComponent ) { 1444 1445 int modelcol = table.convertColumnIndexToModel(column); 1446 int modelrow = table.convertRowIndexToModel(row); 1447 1448 // if cell is not editable, jcombobox not applicable for hardware type 1449 boolean editable = table.getModel().isCellEditable(modelrow, modelcol); 1450 1451 ((JComponent) value).setEnabled(editable); 1452 1453 } 1454 1455 return super.getTableCellEditorComponent(table, value, isSelected, row, column); 1456 } 1457 1458 1459 } 1460 1461 private class BtValueRenderer implements TableCellRenderer { 1462 1463 BtValueRenderer() { 1464 super(); 1465 } 1466 1467 @Override 1468 public Component getTableCellRendererComponent(JTable table, Object value, 1469 boolean isSelected, boolean hasFocus, int row, int column) { 1470 1471 if (value instanceof Component) { 1472 return (Component) value; 1473 } else if (value instanceof String) { 1474 return new JLabel((String) value); 1475 } else { 1476 JPanel f = new JPanel(); 1477 f.setBackground(isSelected ? table.getSelectionBackground() : table.getBackground() ); 1478 return f; 1479 } 1480 } 1481 } 1482 1483 /** 1484 * Set the filter to select which beans to include in the table. 1485 * @param filter the filter 1486 */ 1487 public synchronized void setFilter(Predicate<? super T> filter) { 1488 this.filter = filter; 1489 updateNameList(); 1490 } 1491 1492 /** 1493 * Get the filter to select which beans to include in the table. 1494 * @return the filter 1495 */ 1496 public synchronized Predicate<? super T> getFilter() { 1497 return filter; 1498 } 1499 1500 static class DateRenderer extends DefaultTableCellRenderer { 1501 1502 private final DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM); 1503 1504 @Override 1505 public Component getTableCellRendererComponent( JTable table, Object value, 1506 boolean isSelected, boolean hasFocus, int row, int column) { 1507 JLabel c = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); 1508 if ( value instanceof Date) { 1509 c.setText(dateFormat.format(value)); 1510 } 1511 return c; 1512 } 1513 } 1514 1515 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BeanTableDataModel.class); 1516 1517}