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