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