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}