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