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