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