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