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