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        popupMenu.show(e.getComponent(), e.getX(), e.getY());
866    }
867
868    public void copyUserName(int row, int column) {
869        T nBean = getBySystemName(sysNameList.get(row));
870        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
871        StringSelection name = new StringSelection(nBean.getUserName());
872        clipboard.setContents(name, null);
873    }
874
875    public void copySystemName(int row, int column) {
876        String systemName = sysNameList.get(row);
877        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
878        StringSelection name = new StringSelection(systemName);
879        clipboard.setContents(name, null);
880    }
881
882    /**
883     * Change the bean User Name in a dialog.
884     *
885     * @param row table model row number of bean
886     * @param column always passed in as 0, not used
887     */
888    public void renameBean(int row, int column) {
889        T nBean = getBySystemName(sysNameList.get(row));
890        String oldName = (nBean.getUserName() == null ? "" : nBean.getUserName());
891        String newName = JmriJOptionPane.showInputDialog(null,
892                Bundle.getMessage("RenameFrom", getBeanType(), "\"" +oldName+"\""), oldName);
893        if (newName == null || newName.equals(nBean.getUserName())) {
894            // name not changed
895            return;
896        } else {
897            T nB = getByUserName(newName);
898            if (nB != null) {
899                log.error("User name is not unique {}", newName);
900                String msg = Bundle.getMessage("WarningUserName", "" + newName);
901                JmriJOptionPane.showMessageDialog(null, msg,
902                        Bundle.getMessage("WarningTitle"),
903                        JmriJOptionPane.ERROR_MESSAGE);
904                return;
905            }
906        }
907
908        if (!allowBlockNameChange("Rename", nBean, newName)) {
909            return;  // NOI18N
910        }
911
912        try {
913            nBean.setUserName(newName);
914        } catch (NamedBean.BadSystemNameException | NamedBean.BadUserNameException ex) {
915            JmriJOptionPane.showMessageDialog(null, ex.getLocalizedMessage(),
916                    Bundle.getMessage("ErrorTitle"), // NOI18N
917                    JmriJOptionPane.ERROR_MESSAGE);
918            return;
919        }
920
921        fireTableRowsUpdated(row, row);
922        if (!newName.isEmpty()) {
923            if (oldName == null || oldName.isEmpty()) {
924                if (!nbMan.inUse(sysNameList.get(row), nBean)) {
925                    return;
926                }
927                String msg = Bundle.getMessage("UpdateToUserName", getBeanType(), newName, sysNameList.get(row));
928                int optionPane = JmriJOptionPane.showConfirmDialog(null,
929                        msg, Bundle.getMessage("UpdateToUserNameTitle"),
930                        JmriJOptionPane.YES_NO_OPTION);
931                if (optionPane == JmriJOptionPane.YES_OPTION) {
932                    //This will update the bean reference from the systemName to the userName
933                    try {
934                        nbMan.updateBeanFromSystemToUser(nBean);
935                    } catch (JmriException ex) {
936                        //We should never get an exception here as we already check that the username is not valid
937                        log.error("Impossible exception renaming Bean", ex);
938                    }
939                }
940            } else {
941                nbMan.renameBean(oldName, newName, nBean);
942            }
943
944        } else {
945            //This will update the bean reference from the old userName to the SystemName
946            nbMan.updateBeanFromUserToSystem(nBean);
947        }
948    }
949
950    public void removeName(int modelRow, int column) {
951        T nBean = getBySystemName(sysNameList.get(modelRow));
952        if (!allowBlockNameChange("Remove", nBean, "")) { // NOI18N
953            return;
954        }
955        String msg = Bundle.getMessage("UpdateToSystemName", getBeanType());
956        int optionPane = JmriJOptionPane.showConfirmDialog(null,
957                msg, Bundle.getMessage("UpdateToSystemNameTitle"),
958                JmriJOptionPane.YES_NO_OPTION);
959        if (optionPane == JmriJOptionPane.YES_OPTION) {
960            nbMan.updateBeanFromUserToSystem(nBean);
961        }
962        nBean.setUserName(null);
963        fireTableRowsUpdated(modelRow, modelRow);
964    }
965
966    /**
967     * Determine whether it is safe to rename/remove a Block user name.
968     * <p>The user name is used by the LayoutBlock to link to the block and
969     * by Layout Editor track components to link to the layout block.
970     *
971     * @param changeType This will be Remove or Rename.
972     * @param bean The affected bean.  Only the Block bean is of interest.
973     * @param newName For Remove this will be empty, for Rename it will be the new user name.
974     * @return true to continue with the user name change.
975     */
976    boolean allowBlockNameChange(String changeType, T bean, String newName) {
977        if (!(bean instanceof jmri.Block)) {
978            return true;
979        }
980        // If there is no layout block or the block name is empty, Block rename and remove are ok without notification.
981        String oldName = bean.getUserName();
982        if (oldName == null) return true;
983        LayoutBlock layoutBlock = jmri.InstanceManager.getDefault(LayoutBlockManager.class).getByUserName(oldName);
984        if (layoutBlock == null) return true;
985
986        // Remove is not allowed if there is a layout block
987        if (changeType.equals("Remove")) {
988            log.warn("Cannot remove user name for block {}", oldName);  // NOI18N
989                JmriJOptionPane.showMessageDialog(null,
990                        Bundle.getMessage("BlockRemoveUserNameWarning", oldName),  // NOI18N
991                        Bundle.getMessage("WarningTitle"),  // NOI18N
992                        JmriJOptionPane.WARNING_MESSAGE);
993            return false;
994        }
995
996        // Confirmation dialog
997        int optionPane = JmriJOptionPane.showConfirmDialog(null,
998                Bundle.getMessage("BlockChangeUserName", oldName, newName),  // NOI18N
999                Bundle.getMessage("QuestionTitle"),  // NOI18N
1000                JmriJOptionPane.YES_NO_OPTION);
1001        return optionPane == JmriJOptionPane.YES_OPTION;
1002    }
1003
1004    public void moveBean(int row, int column) {
1005        final T t = getBySystemName(sysNameList.get(row));
1006        String currentName = t.getUserName();
1007        T oldNameBean = getBySystemName(sysNameList.get(row));
1008
1009        if ((currentName == null) || currentName.isEmpty()) {
1010            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("MoveDialogErrorMessage"));
1011            return;
1012        }
1013
1014        JComboBox<String> box = new JComboBox<>();
1015        getManager().getNamedBeanSet().forEach((T b) -> {
1016            //Only add items that do not have a username assigned.
1017            String userName = b.getUserName();
1018            if (userName == null || userName.isEmpty()) {
1019                box.addItem(b.getSystemName());
1020            }
1021        });
1022
1023        int retval = JmriJOptionPane.showOptionDialog(null,
1024                Bundle.getMessage("MoveDialog", getBeanType(), currentName, oldNameBean.getSystemName()),
1025                Bundle.getMessage("MoveDialogTitle"),
1026                JmriJOptionPane.YES_NO_OPTION, JmriJOptionPane.INFORMATION_MESSAGE, null,
1027                new Object[]{Bundle.getMessage("ButtonCancel"), Bundle.getMessage("ButtonOK"), box}, null);
1028        log.debug("Dialog value {} selected {}:{}", retval, box.getSelectedIndex(), box.getSelectedItem());
1029        if (retval != 1) {
1030            return;
1031        }
1032        String entry = (String) box.getSelectedItem();
1033        assert entry != null;
1034        T newNameBean = getBySystemName(entry);
1035        if (oldNameBean != newNameBean) {
1036            oldNameBean.setUserName(null);
1037            newNameBean.setUserName(currentName);
1038            InstanceManager.getDefault(NamedBeanHandleManager.class).moveBean(oldNameBean, newNameBean, currentName);
1039            if (nbMan.inUse(newNameBean.getSystemName(), newNameBean)) {
1040                String msg = Bundle.getMessage("UpdateToUserName", getBeanType(), currentName, sysNameList.get(row));
1041                int optionPane = JmriJOptionPane.showConfirmDialog(null, msg, Bundle.getMessage("UpdateToUserNameTitle"), JmriJOptionPane.YES_NO_OPTION);
1042                if (optionPane == JmriJOptionPane.YES_OPTION) {
1043                    try {
1044                        nbMan.updateBeanFromSystemToUser(newNameBean);
1045                    } catch (JmriException ex) {
1046                        //We should never get an exception here as we already check that the username is not valid
1047                        log.error("Impossible exception moving Bean", ex);
1048                    }
1049                }
1050            }
1051            fireTableRowsUpdated(row, row);
1052            InstanceManager.getDefault(UserPreferencesManager.class).
1053                    showInfoMessage(Bundle.getMessage("ReminderTitle"),
1054                            Bundle.getMessage("UpdateComplete", getBeanType()),
1055                            getMasterClassName(), "remindSaveReLoad");
1056        }
1057    }
1058
1059    public void editComment(int row, int column) {
1060        T nBean = getBySystemName(sysNameList.get(row));
1061        JTextArea commentField = new JTextArea(5, 50);
1062        JScrollPane commentFieldScroller = new JScrollPane(commentField);
1063        commentField.setText(nBean.getComment());
1064        Object[] editCommentOption = {Bundle.getMessage("ButtonCancel"), Bundle.getMessage("ButtonUpdate")};
1065        int retval = JmriJOptionPane.showOptionDialog(null,
1066                commentFieldScroller, Bundle.getMessage("EditComment"),
1067                JmriJOptionPane.YES_NO_OPTION, JmriJOptionPane.INFORMATION_MESSAGE, null,
1068                editCommentOption, editCommentOption[1]);
1069        if (retval != 1) {
1070            return;
1071        }
1072        nBean.setComment(commentField.getText());
1073   }
1074
1075    /**
1076     * Display the comment text for the current row as a tool tip.
1077     *
1078     * Most of the bean tables use the standard model with comments in column 3.
1079     *
1080     * @param table The current table.
1081     * @param modelRow The current row.
1082     * @param modelCol The current column.
1083     * @return a formatted tool tip or null if there is none.
1084     */
1085    public String getCellToolTip(JTable table, int modelRow, int modelCol) {
1086        String tip = null;
1087        T nBean = getBySystemName(sysNameList.get(modelRow));
1088        if (nBean != null) {
1089            tip = formatToolTip(nBean.getRecommendedToolTip());
1090        }
1091        return tip;
1092    }
1093
1094    /**
1095     * Get a ToolTip for a Table Column Header.
1096     * @param columnModelIndex the model column number.
1097     * @return ToolTip, else null.
1098     */
1099    @OverridingMethodsMustInvokeSuper
1100    protected String getHeaderTooltip(int columnModelIndex) {
1101        return null;
1102    }
1103
1104    /**
1105     * Format a tool tip string. Multi line tooltips are supported.
1106     * @param tooltip The tooltip string to be formatted
1107     * @return a html formatted string or null if the comment is empty.
1108     */
1109    protected String formatToolTip(String tooltip) {
1110        String tip = null;
1111        if (tooltip != null && !tooltip.isEmpty()) {
1112            tip = "<html>" + tooltip.replaceAll(System.getProperty("line.separator"), "<br>") + "</html>";
1113        }
1114        return tip;
1115    }
1116
1117    /**
1118     * Show the Table Column Menu.
1119     * @param e Instigating event ( e.g. from Mouse click )
1120     * @param table table to get columns from
1121     */
1122    protected void showTableHeaderPopup(JmriMouseEvent e, JTable table) {
1123        JPopupMenu popupMenu = new JPopupMenu();
1124        XTableColumnModel tcm = (XTableColumnModel) table.getColumnModel();
1125        for (int i = 0; i < tcm.getColumnCount(false); i++) {
1126            TableColumn tc = tcm.getColumnByModelIndex(i);
1127            String columnName = table.getModel().getColumnName(i);
1128            if (columnName != null && !columnName.isEmpty()) {
1129                StayOpenCheckBoxItem menuItem = new StayOpenCheckBoxItem(table.getModel().getColumnName(i), tcm.isColumnVisible(tc));
1130                menuItem.addActionListener(new HeaderActionListener(tc, tcm));
1131                TableModel mod = table.getModel();
1132                if (mod instanceof BeanTableDataModel<?>) {
1133                    menuItem.setToolTipText(((BeanTableDataModel<?>)mod).getHeaderTooltip(i));
1134                }
1135                popupMenu.add(menuItem);
1136            }
1137
1138        }
1139        popupMenu.show(e.getComponent(), e.getX(), e.getY());
1140    }
1141
1142    protected void addMouseListenerToHeader(JTable table) {
1143        JmriMouseListener mouseHeaderListener = new TableHeaderListener(table);
1144        table.getTableHeader().addMouseListener(JmriMouseListener.adapt(mouseHeaderListener));
1145    }
1146
1147    /**
1148     * Persist the state of the table after first setting the table to the last
1149     * persisted state.
1150     *
1151     * @param table the table to persist
1152     * @throws NullPointerException if the name of the table is null
1153     */
1154    public void persistTable(@Nonnull JTable table) throws NullPointerException {
1155        InstanceManager.getOptionalDefault(JTablePersistenceManager.class).ifPresent((manager) -> {
1156            setColumnIdentities(table);
1157            manager.resetState(table); // throws NPE if table name is null
1158            manager.persist(table);
1159        });
1160    }
1161
1162    /**
1163     * Stop persisting the state of the table.
1164     *
1165     * @param table the table to stop persisting
1166     * @throws NullPointerException if the name of the table is null
1167     */
1168    public void stopPersistingTable(@Nonnull JTable table) throws NullPointerException {
1169        InstanceManager.getOptionalDefault(JTablePersistenceManager.class).ifPresent((manager) -> {
1170            manager.stopPersisting(table); // throws NPE if table name is null
1171        });
1172    }
1173
1174    /**
1175     * Set identities for any columns that need an identity.
1176     *
1177     * It is recommended that all columns get a constant identity to
1178     * prevent identities from being subject to changes due to translation.
1179     * <p>
1180     * The default implementation sets column identities to the String
1181     * {@code Column#} where {@code #} is the model index for the column.
1182     * Note that if the TableColumnModel is a {@link jmri.util.swing.XTableColumnModel},
1183     * the index includes hidden columns.
1184     *
1185     * @param table the table to set identities for.
1186     */
1187    protected void setColumnIdentities(JTable table) {
1188        Objects.requireNonNull(table.getModel(), "Table must have data model");
1189        Objects.requireNonNull(table.getColumnModel(), "Table must have column model");
1190        Enumeration<TableColumn> columns;
1191        if (table.getColumnModel() instanceof XTableColumnModel) {
1192            columns = ((XTableColumnModel) table.getColumnModel()).getColumns(false);
1193        } else {
1194            columns = table.getColumnModel().getColumns();
1195        }
1196        int i = 0;
1197        while (columns.hasMoreElements()) {
1198            TableColumn column = columns.nextElement();
1199            if (column.getIdentifier() == null || column.getIdentifier().toString().isEmpty()) {
1200                column.setIdentifier(String.format("Column%d", i));
1201            }
1202            i += 1;
1203        }
1204    }
1205
1206    protected class BeanTableTooltipHeaderRenderer extends DefaultTableCellRenderer  {
1207        private final TableCellRenderer _existingRenderer;
1208
1209        protected BeanTableTooltipHeaderRenderer(TableCellRenderer existingRenderer) {
1210            _existingRenderer = existingRenderer;
1211        }
1212
1213        @Override
1214        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
1215            
1216            Component rendererComponent = _existingRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
1217            TableModel mod = table.getModel();
1218            if ( rendererComponent instanceof JLabel && mod instanceof BeanTableDataModel<?> ) { // Set the cell ToolTip
1219                int modelIndex = table.getColumnModel().getColumn(column).getModelIndex();
1220                String tooltip = ((BeanTableDataModel<?>)mod).getHeaderTooltip(modelIndex);
1221                ((JLabel)rendererComponent).setToolTipText(tooltip);
1222            }
1223            return rendererComponent;
1224        }
1225    }
1226
1227    /**
1228     * Listener class which processes Column Menu button clicks.
1229     * Does not allow the last column to be hidden,
1230     * otherwise there would be no table header to recover the column menu / columns from.
1231     */
1232    static class HeaderActionListener implements ActionListener {
1233
1234        private final TableColumn tc;
1235        private final XTableColumnModel tcm;
1236
1237        HeaderActionListener(TableColumn tc, XTableColumnModel tcm) {
1238            this.tc = tc;
1239            this.tcm = tcm;
1240        }
1241
1242        @Override
1243        public void actionPerformed(ActionEvent e) {
1244            JCheckBoxMenuItem check = (JCheckBoxMenuItem) e.getSource();
1245            //Do not allow the last column to be hidden
1246            if (!check.isSelected() && tcm.getColumnCount(true) == 1) {
1247                return;
1248            }
1249            tcm.setColumnVisible(tc, check.isSelected());
1250        }
1251    }
1252
1253    class DeleteBeanWorker  {
1254
1255        public DeleteBeanWorker(final T bean) {
1256
1257            StringBuilder message = new StringBuilder();
1258            try {
1259                getManager().deleteBean(bean, "CanDelete");  // NOI18N
1260            } catch (PropertyVetoException e) {
1261                if (e.getPropertyChangeEvent().getPropertyName().equals("DoNotDelete")) { // NOI18N
1262                    log.warn("Should not delete {}, {}", bean.getDisplayName((DisplayOptions.USERNAME_SYSTEMNAME)), e.getMessage());
1263                    message.append(Bundle.getMessage("VetoDeleteBean", bean.getBeanType(), bean.getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME), e.getMessage()));
1264                    JmriJOptionPane.showMessageDialog(null, message.toString(),
1265                            Bundle.getMessage("WarningTitle"),
1266                            JmriJOptionPane.ERROR_MESSAGE);
1267                    return;
1268                }
1269                message.append(e.getMessage());
1270            }
1271            int count = bean.getListenerRefs().size();
1272            log.debug("Delete with {}", count);
1273            if (getDisplayDeleteMsg() == 0x02 && message.toString().isEmpty()) {
1274                doDelete(bean);
1275            } else {
1276                JPanel container = new JPanel();
1277                container.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
1278                container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS));
1279                if (count > 0) { // warn of listeners attached before delete
1280
1281                    JLabel question = new JLabel(Bundle.getMessage("DeletePrompt", bean.getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME)));
1282                    question.setAlignmentX(Component.CENTER_ALIGNMENT);
1283                    container.add(question);
1284
1285                    ArrayList<String> listenerRefs = bean.getListenerRefs();
1286                    if (!listenerRefs.isEmpty()) {
1287                        ArrayList<String> listeners = new ArrayList<>();
1288                        for (String listenerRef : listenerRefs) {
1289                            if (!listeners.contains(listenerRef)) {
1290                                listeners.add(listenerRef);
1291                            }
1292                        }
1293
1294                        message.append("<br>");
1295                        message.append(Bundle.getMessage("ReminderInUse", count));
1296                        message.append("<ul>");
1297                        for (String listener : listeners) {
1298                            message.append("<li>");
1299                            message.append(listener);
1300                            message.append("</li>");
1301                        }
1302                        message.append("</ul>");
1303
1304                        JEditorPane pane = new JEditorPane();
1305                        pane.setContentType("text/html");
1306                        pane.setText("<html>" + message.toString() + "</html>");
1307                        pane.setEditable(false);
1308                        JScrollPane jScrollPane = new JScrollPane(pane);
1309                        container.add(jScrollPane);
1310                    }
1311                } else {
1312                    String msg = MessageFormat.format(
1313                            Bundle.getMessage("DeletePrompt"), bean.getSystemName());
1314                    JLabel question = new JLabel(msg);
1315                    question.setAlignmentX(Component.CENTER_ALIGNMENT);
1316                    container.add(question);
1317                }
1318
1319                final JCheckBox remember = new JCheckBox(Bundle.getMessage("MessageRememberSetting"));
1320                remember.setFont(remember.getFont().deriveFont(10f));
1321                remember.setAlignmentX(Component.CENTER_ALIGNMENT);
1322
1323                container.add(remember);
1324                container.setAlignmentX(Component.CENTER_ALIGNMENT);
1325                container.setAlignmentY(Component.CENTER_ALIGNMENT);
1326                String[] options = new String[]{JmriJOptionPane.YES_STRING, JmriJOptionPane.NO_STRING};
1327                int result = JmriJOptionPane.showOptionDialog(null, container, Bundle.getMessage("WarningTitle"), 
1328                    JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.WARNING_MESSAGE, null, 
1329                    options, JmriJOptionPane.NO_STRING);
1330
1331                if ( result == 0 ){ // first item in Array is Yes
1332                    if (remember.isSelected()) {
1333                        setDisplayDeleteMsg(0x02);
1334                    }
1335                    doDelete(bean);
1336                }
1337
1338            }
1339        }
1340    }
1341
1342    /**
1343     * Listener to trigger display of table cell menu.
1344     * Delete / Rename / Move etc.
1345     */
1346    class PopupListener extends JmriMouseAdapter {
1347
1348        /**
1349         * {@inheritDoc}
1350         */
1351        @Override
1352        public void mousePressed(JmriMouseEvent e) {
1353            if (e.isPopupTrigger()) {
1354                showPopup(e);
1355            }
1356        }
1357
1358        /**
1359         * {@inheritDoc}
1360         */
1361        @Override
1362        public void mouseReleased(JmriMouseEvent e) {
1363            if (e.isPopupTrigger()) {
1364                showPopup(e);
1365            }
1366        }
1367    }
1368
1369    /**
1370     * Listener to trigger display of table header column menu.
1371     */
1372    class TableHeaderListener extends JmriMouseAdapter {
1373
1374        private final JTable table;
1375
1376        TableHeaderListener(JTable tbl) {
1377            super();
1378            table = tbl;
1379        }
1380
1381        /**
1382         * {@inheritDoc}
1383         */
1384        @Override
1385        public void mousePressed(JmriMouseEvent e) {
1386            if (e.isPopupTrigger()) {
1387                showTableHeaderPopup(e, table);
1388            }
1389        }
1390
1391        /**
1392         * {@inheritDoc}
1393         */
1394        @Override
1395        public void mouseReleased(JmriMouseEvent e) {
1396            if (e.isPopupTrigger()) {
1397                showTableHeaderPopup(e, table);
1398            }
1399        }
1400
1401        /**
1402         * {@inheritDoc}
1403         */
1404        @Override
1405        public void mouseClicked(JmriMouseEvent e) {
1406            if (e.isPopupTrigger()) {
1407                showTableHeaderPopup(e, table);
1408            }
1409        }
1410    }
1411
1412    private class BtComboboxEditor extends jmri.jmrit.symbolicprog.ValueEditor {
1413
1414        BtComboboxEditor(){
1415            super();
1416        }
1417
1418        @Override
1419        public Component getTableCellEditorComponent(JTable table, Object value,
1420            boolean isSelected,
1421            int row, int column) {
1422            if (value instanceof JComboBox) {
1423                ((JComboBox<?>) value).addActionListener((ActionEvent e1) -> table.getCellEditor().stopCellEditing());
1424            }
1425
1426            if (value instanceof JComponent ) {
1427
1428                int modelcol =  table.convertColumnIndexToModel(column);
1429                int modelrow = table.convertRowIndexToModel(row);
1430
1431                // if cell is not editable, jcombobox not applicable for hardware type
1432                boolean editable = table.getModel().isCellEditable(modelrow, modelcol);
1433
1434                ((JComponent) value).setEnabled(editable);
1435
1436            }
1437
1438            return super.getTableCellEditorComponent(table, value, isSelected, row, column);
1439        }
1440
1441
1442    }
1443
1444    private class BtValueRenderer implements TableCellRenderer {
1445
1446        BtValueRenderer() {
1447            super();
1448        }
1449
1450        @Override
1451        public Component getTableCellRendererComponent(JTable table, Object value,
1452            boolean isSelected, boolean hasFocus, int row, int column) {
1453
1454            if (value instanceof Component) {
1455                return (Component) value;
1456            } else if (value instanceof String) {
1457                return new JLabel((String) value);
1458            } else {
1459                JPanel f = new JPanel();
1460                f.setBackground(isSelected ? table.getSelectionBackground() : table.getBackground() );
1461                return f;
1462            }
1463        }
1464    }
1465
1466    /**
1467     * Set the filter to select which beans to include in the table.
1468     * @param filter the filter
1469     */
1470    public synchronized void setFilter(Predicate<? super T> filter) {
1471        this.filter = filter;
1472        updateNameList();
1473    }
1474
1475    /**
1476     * Get the filter to select which beans to include in the table.
1477     * @return the filter
1478     */
1479    public synchronized Predicate<? super T> getFilter() {
1480        return filter;
1481    }
1482
1483    static class DateRenderer extends DefaultTableCellRenderer {
1484
1485        private final DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM);
1486
1487        @Override
1488        public Component getTableCellRendererComponent( JTable table, Object value,
1489            boolean isSelected, boolean hasFocus, int row, int column) {
1490            JLabel c = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
1491            if ( value instanceof Date) {
1492                c.setText(dateFormat.format(value));
1493            }
1494            return c;
1495        }
1496    }
1497
1498    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BeanTableDataModel.class);
1499
1500}