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