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