001package jmri.jmrit.beantable.sensor;
002
003import jmri.util.gui.GuiLafPreferencesManager;
004import java.awt.Color;
005import java.awt.Component;
006import java.awt.Image;
007import java.awt.Rectangle;
008import java.awt.event.MouseAdapter;
009import java.awt.event.MouseEvent;
010import java.awt.image.BufferedImage;
011import java.beans.PropertyChangeEvent;
012import java.io.File;
013import java.io.IOException;
014import java.util.Enumeration;
015
016import javax.annotation.Nonnull;
017import javax.imageio.ImageIO;
018import javax.swing.*;
019import javax.swing.table.TableCellEditor;
020import javax.swing.table.TableCellRenderer;
021import javax.swing.table.TableColumn;
022import jmri.InstanceManager;
023import jmri.JmriException;
024import jmri.Manager;
025import jmri.NamedBean;
026import jmri.Sensor;
027import jmri.SensorManager;
028import jmri.managers.ProxySensorManager;
029import jmri.jmrit.beantable.BeanTableDataModel;
030import jmri.util.swing.XTableColumnModel;
031import jmri.util.swing.JmriJOptionPane;
032
033/**
034 * Data model for a SensorTable.
035 *
036 * @author Bob Jacobsen Copyright (C) 2003, 2009
037 * @author Egbert Broerse Copyright (C) 2017
038 */
039public class SensorTableDataModel extends BeanTableDataModel<Sensor> {
040
041    public static final int INVERTCOL = BeanTableDataModel.NUMCOLUMN;
042    public static final int EDITCOL = INVERTCOL + 1;
043    public static final int USEGLOBALDELAY = EDITCOL + 1;
044    public static final int ACTIVEDELAY = USEGLOBALDELAY + 1;
045    public static final int INACTIVEDELAY = ACTIVEDELAY + 1;
046    public static final int PULLUPCOL = INACTIVEDELAY + 1;
047    public static final int FORGETCOL = PULLUPCOL + 1;
048    public static final int QUERYCOL = FORGETCOL + 1;
049
050    private Manager<Sensor> senManager = null;
051    protected boolean _graphicState = false; // icon state col updated from prefs
052
053    /**
054     * Create a new Sensor Table Data Model.
055     * The default Manager for the bean type will be a Proxy Manager.
056     */
057    public SensorTableDataModel() {
058        super();
059        _graphicState = InstanceManager.getDefault(GuiLafPreferencesManager.class).isGraphicTableState();
060    }
061
062    /**
063     * Create a new Sensor Table Data Model.
064     * The default Manager for the bean type will be a Proxy Manager unless
065     * one is specified here.
066     * @param manager Bean Manager.
067     */
068    public SensorTableDataModel(Manager<Sensor> manager) {
069        super();
070        setManager(manager); // updates name list
071        // load graphic state column display preference
072        _graphicState = InstanceManager.getDefault(GuiLafPreferencesManager.class).isGraphicTableState();
073    }
074
075    /**
076     * {@inheritDoc}
077     */
078    @Override
079    public String getValue(String name) {
080        Sensor sen = getManager().getBySystemName(name);
081        if (sen == null) {
082            return "Failed to get sensor " + name;
083        }
084        return sen.describeState(sen.getKnownState());
085    }
086
087    /**
088     * {@inheritDoc}
089     */
090    @Override
091    protected final void setManager(@Nonnull Manager<Sensor> manager) {
092        if (!(manager instanceof SensorManager)) {
093            return;
094        }
095        getManager().removePropertyChangeListener(this);
096        if (sysNameList != null) {
097            for (int i = 0; i < sysNameList.size(); i++) {
098                // if object has been deleted, it's not here; ignore it
099                NamedBean b = getBySystemName(sysNameList.get(i));
100                if (b != null) {
101                    b.removePropertyChangeListener(this);
102                }
103            }
104        }
105        senManager = manager;
106        getManager().addPropertyChangeListener(this);
107        updateNameList();
108    }
109
110    /**
111     * {@inheritDoc}
112     */
113    @Override
114    protected Manager<Sensor> getManager() {
115        if (senManager == null) {
116            senManager = InstanceManager.sensorManagerInstance();
117        }
118        return senManager;
119    }
120
121    /**
122     * {@inheritDoc}
123     */
124    @Override
125    protected Sensor getBySystemName(@Nonnull String name) {
126        return getManager().getBySystemName(name);
127    }
128
129    /**
130     * {@inheritDoc}
131     */
132    @Override
133    protected Sensor getByUserName(@Nonnull String name) {
134        return InstanceManager.getDefault(SensorManager.class).getByUserName(name);
135    }
136
137    /**
138     * {@inheritDoc}
139     */
140    @Override
141    protected String getMasterClassName() {
142        return getClassName();
143    }
144
145    /**
146     * {@inheritDoc}
147     */
148    @Override
149    protected void clickOn(Sensor t) {
150        try {
151            t.setKnownState(t.getKnownState() == Sensor.INACTIVE ? Sensor.ACTIVE : Sensor.INACTIVE );
152        } catch (JmriException e) {
153            log.warn("Error setting state", e);
154        }
155    }
156
157    /**
158     * {@inheritDoc}
159     */
160    @Override
161    public int getColumnCount() {
162        return QUERYCOL + getPropertyColumnCount() + 1;
163    }
164
165    /**
166     * {@inheritDoc}
167     */
168    @Override
169    public String getColumnName(int col) {
170        switch (col) {
171            case INVERTCOL:
172                return Bundle.getMessage("Inverted");
173            case EDITCOL:
174                return "";
175            case USEGLOBALDELAY:
176                return Bundle.getMessage("SensorUseGlobalDebounce");
177            case ACTIVEDELAY:
178                return Bundle.getMessage("SensorActiveDebounce");
179            case INACTIVEDELAY:
180                return Bundle.getMessage("SensorInActiveDebounce");
181            case PULLUPCOL:
182                return Bundle.getMessage("SensorPullUp");
183            case FORGETCOL:
184                return Bundle.getMessage("StateForgetHeader");
185            case QUERYCOL:
186                return Bundle.getMessage("StateQueryHeader");
187            default:
188                return super.getColumnName(col);
189        }
190    }
191
192    /**
193     * {@inheritDoc}
194     */
195    @Override
196    public Class<?> getColumnClass(int col) {
197        switch (col) {
198            case INVERTCOL:
199            case USEGLOBALDELAY:
200                return Boolean.class;
201            case ACTIVEDELAY:
202            case INACTIVEDELAY:
203                return Long.class; // if long.class (lowercase) is returned here, cell is NOT editable.
204            case PULLUPCOL:
205                return JComboBox.class;
206            case EDITCOL:
207            case FORGETCOL:
208            case QUERYCOL:
209                return JButton.class;
210            case VALUECOL:
211                if (_graphicState) {
212                    return JLabel.class; // use an image to show sensor state
213                } else {
214                    return super.getColumnClass(col);
215                }
216            default:
217                return super.getColumnClass(col);
218        }
219    }
220
221    /**
222     * {@inheritDoc}
223     */
224    @Override
225    public int getPreferredWidth(int col) {
226        switch (col) {
227            case INVERTCOL:
228                return new JTextField(4).getPreferredSize().width;
229            case USEGLOBALDELAY:
230            case ACTIVEDELAY:
231            case INACTIVEDELAY:
232            case PULLUPCOL:
233                return new JTextField(8).getPreferredSize().width;
234            case EDITCOL:
235                return new JButton(Bundle.getMessage("ButtonEdit")).getPreferredSize().width+4;
236            case FORGETCOL:
237                return new JButton(Bundle.getMessage("StateForgetButton"))
238                        .getPreferredSize().width+4;
239            case QUERYCOL:
240                return new JButton(Bundle.getMessage("StateQueryButton"))
241                        .getPreferredSize().width+4;
242            default:
243                return super.getPreferredWidth(col);
244        }
245    }
246
247    /**
248     * {@inheritDoc}
249     */
250    @Override
251    public boolean isCellEditable(int row, int col) {
252        String name = sysNameList.get(row);
253        Sensor sen = getManager().getBySystemName(name);
254        if (sen == null) {
255            return false;
256        }
257        switch (col) {
258            case EDITCOL:
259            case USEGLOBALDELAY:
260            case FORGETCOL:
261            case QUERYCOL:
262                return true;
263            case INVERTCOL:
264                return sen.canInvert();
265            case ACTIVEDELAY:
266            case INACTIVEDELAY:
267                return !sen.getUseDefaultTimerSettings();
268            case PULLUPCOL:
269                if ( getManager() instanceof ProxySensorManager ) {
270                    return ((ProxySensorManager)getManager()).isPullResistanceConfigurable(name);
271                }
272                return (((SensorManager) getManager()).isPullResistanceConfigurable()); // proxymanager always false
273                
274            default:
275                return super.isCellEditable(row, col);
276        }
277    }
278
279    /**
280     * {@inheritDoc}
281     */
282    @Override
283    public Object getValueAt(int row, int col) {
284        if (row >= sysNameList.size()) {
285            log.debug("row is greater than name list");
286            return "";
287        }
288        String name = sysNameList.get(row);
289        Sensor s = senManager.getBySystemName(name);
290        if (s == null) {
291            log.debug("error null sensor!");
292            return "error";
293        }
294        switch (col) {
295            case INVERTCOL:
296                return s.getInverted();
297            case USEGLOBALDELAY:
298                return s.getUseDefaultTimerSettings();
299            case ACTIVEDELAY:
300                return s.getSensorDebounceGoingActiveTimer();
301            case INACTIVEDELAY:
302                return s.getSensorDebounceGoingInActiveTimer();
303            case EDITCOL:
304                return Bundle.getMessage("ButtonEdit");
305            case PULLUPCOL:
306                PullResistanceComboBox c = new PullResistanceComboBox(Sensor.PullResistance.values());
307                c.setSelectedItem(s.getPullResistance());
308                return c;
309            case FORGETCOL:
310                return Bundle.getMessage("StateForgetButton");
311            case QUERYCOL:
312                return Bundle.getMessage("StateQueryButton");
313            default:
314                return super.getValueAt(row, col);
315        }
316    }
317    
318    /**
319     * Small class to ensure type-safety of references otherwise lost to type erasure
320     */
321    private static class PullResistanceComboBox extends JComboBox<Sensor.PullResistance> {
322        PullResistanceComboBox(Sensor.PullResistance[] values) { super(values); }
323    }
324
325    /**
326     * {@inheritDoc}
327     */
328    @Override
329    public void setValueAt(Object value, int row, int col) {
330        if (row >= sysNameList.size()) {
331            log.debug("row is greater than name list");
332            return;
333        }
334        String name = sysNameList.get(row);
335        Sensor s = senManager.getBySystemName(name);
336        if (s == null) {
337            log.debug("error null sensor!");
338            return;
339        }
340        switch (col) {
341            case INVERTCOL:
342                s.setInverted(((boolean) value));
343                break;
344            case USEGLOBALDELAY:
345                s.setUseDefaultTimerSettings(((boolean) value));
346                break;
347            case ACTIVEDELAY:
348                try {
349                    long activeDeBounce = (long) value;
350                    if (activeDeBounce < 0 || activeDeBounce > Sensor.MAX_DEBOUNCE) {
351                        JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("SensorDebounceActOutOfRange")
352                            + "\n\"" + Sensor.MAX_DEBOUNCE + "\"", Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
353                    } else {
354                        s.setSensorDebounceGoingActiveTimer(activeDeBounce);
355                    }
356                } catch (NumberFormatException exActiveDeBounce) {
357                    JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("SensorDebounceActError")
358                        + "\n\"" + value  + "\"" + exActiveDeBounce.getLocalizedMessage(), Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
359                }
360                break;
361            case INACTIVEDELAY:
362                try {
363                    long inactiveDeBounce = (long) value;
364                    if (inactiveDeBounce < 0 || inactiveDeBounce > Sensor.MAX_DEBOUNCE) {
365                        JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("SensorDebounceInActOutOfRange") 
366                            + "\n\"" + Sensor.MAX_DEBOUNCE + "\"", Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
367                    } else {
368                        s.setSensorDebounceGoingInActiveTimer(inactiveDeBounce);
369                    }
370                } catch (NumberFormatException exActiveDeBounce) {
371                    JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("SensorDebounceInActError")
372                        + "\n\"" + value + "\"" + exActiveDeBounce.getLocalizedMessage(), Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
373                }
374                break;
375            case EDITCOL:
376                javax.swing.SwingUtilities.invokeLater(() -> {
377                    editButton(s);
378                });
379                break;
380            case PULLUPCOL:
381                PullResistanceComboBox cb = (PullResistanceComboBox) value;
382                s.setPullResistance((Sensor.PullResistance) cb.getSelectedItem());
383                break;
384            case FORGETCOL:
385                try {
386                    s.setKnownState(Sensor.UNKNOWN);
387                } catch (JmriException e) {
388                    log.warn("Failed to set state to UNKNOWN: ", e);
389                }
390                break;
391            case QUERYCOL:
392                try {
393                    s.setKnownState(Sensor.UNKNOWN);
394                } catch (JmriException e) {
395                    log.warn("Failed to set state to UNKNOWN: ", e);
396                }
397                s.requestUpdateFromLayout();
398                break;
399            case VALUECOL:
400                if (_graphicState) { // respond to clicking on ImageIconRenderer CellEditor
401                    clickOn(s);
402                    fireTableRowsUpdated(row, row);
403                } else {
404                    super.setValueAt(value, row, col);
405                }
406                break;
407            default:
408                super.setValueAt(value, row, col);
409                break;
410        }
411    }
412
413    /**
414     * {@inheritDoc}
415     */
416    @Override
417    protected boolean matchPropertyName(PropertyChangeEvent e) {
418        switch (e.getPropertyName()) {
419            case Sensor.PROPERTY_SENSOR_INVERTED:
420            case Sensor.PROPERTY_GLOBAL_TIMER:
421            case Sensor.PROPERTY_ACTIVE_TIMER:
422            case Sensor.PROPERTY_INACTIVE_TIMER:
423                return true;
424            default:
425                return super.matchPropertyName(e);
426        }
427    }
428
429    /**
430     * Customize the sensor table Value (State) column to show an appropriate
431     * graphic for the sensor state if _graphicState = true, or (default) just
432     * show the localized state text when the TableDataModel is being called
433     * from ListedTableAction.
434     *
435     * @param table a JTable of Sensors
436     */
437    @Override
438    protected void configValueColumn(JTable table) {
439        // have the value column hold a JPanel (icon)
440        //setColumnToHoldButton(table, VALUECOL, new JLabel("1234")); // for small round icon, but cannot be converted to JButton
441        // add extras, override BeanTableDataModel
442        log.debug("Sensor configValueColumn (I am {})", this);
443        if (_graphicState) { // load icons, only once
444            table.setDefaultEditor(JLabel.class, new ImageIconRenderer()); // editor
445            table.setDefaultRenderer(JLabel.class, new ImageIconRenderer()); // item class copied from SwitchboardEditor panel
446        } else {
447            super.configValueColumn(table); // classic text style state indication
448        }
449    }
450
451    /**
452     * Visualize state in table as a graphic, customized for Sensors (2 states).
453     * Renderer and Editor are identical, as the cell contents are not actually
454     * edited, only used to toggle state using {@link #clickOn}.
455     * <p>
456     * A single label is reused for every cell and the icons are loaded once
457     * per JVM, so rendering a cell allocates nothing. The label ignores
458     * revalidate/repaint requests and the row height is set once in
459     * {@link #configureTable}: painting a cell must never schedule more
460     * painting, or the table repaints itself forever.
461     */
462    static class ImageIconRenderer extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
463
464        private static final String ROOT_PATH = "resources/icons/misc/switchboard/"; // also used in display.switchboardEditor
465        private static final char BEAN_TYPE_CHAR = 'S'; // for Sensor
466        private static final String ON_ICON_PATH = ROOT_PATH + BEAN_TYPE_CHAR + "-on-s.png";
467        private static final String OFF_ICON_PATH = ROOT_PATH + BEAN_TYPE_CHAR + "-off-s.png";
468        private static ImageIcon onIcon = null;
469        private static ImageIcon offIcon = null;
470        private static int iconHeight = -1;
471
472        private final StateLabel label = new StateLabel();
473        private final Color defaultForeground = label.getForeground();
474        private final String activeText = Bundle.getMessage("SensorStateActive");
475        private final String inactiveText = Bundle.getMessage("SensorStateInactive");
476        private final String unknownText = Bundle.getMessage("BeanStateUnknown");
477        private final String inconsistentText = Bundle.getMessage("BeanStateInconsistent");
478        private int row = -1; // row of the cell last rendered or edited
479
480        ImageIconRenderer() {
481            label.setHorizontalAlignment(JLabel.CENTER);
482            // must stay the only anonymous class in ImageIconRenderer, see jmri.ArchitectureTest
483            label.addMouseListener(new MouseAdapter() {
484                @Override
485                public final void mousePressed(MouseEvent evt) {
486                    log.debug("Clicked on icon in row {}", row);
487                    stopCellEditing();
488                }
489            });
490        }
491
492        /**
493         * {@inheritDoc}
494         */
495        @Override
496        public Component getTableCellRendererComponent(
497                JTable table, Object value, boolean isSelected,
498                boolean hasFocus, int row, int column) {
499            log.debug("Renderer Item = {}, State = {}", row, value);
500            return updateLabel((String) value, row);
501        }
502
503        /**
504         * {@inheritDoc}
505         */
506        @Override
507        public Component getTableCellEditorComponent(
508                JTable table, Object value, boolean isSelected,
509                int row, int column) {
510            log.debug("Editor Item = {}, State = {}", row, value);
511            return updateLabel((String) value, row);
512        }
513
514        protected JLabel updateLabel(String value, int row) {
515            this.row = row;
516            if (iconHeight < 0) { // load resources only first time, either for renderer or editor
517                loadIcons();
518                log.debug("icons loaded");
519            }
520            label.setForeground(defaultForeground); // reset a foreground left over from an INCONSISTENT cell
521            if (value.equals(inactiveText) && offIcon != null) {
522                label.setIcon(offIcon);
523                label.setText(null);
524                label.setVerticalAlignment(JLabel.BOTTOM);
525                log.debug("offIcon set");
526            } else if (value.equals(activeText) && onIcon != null) {
527                label.setIcon(onIcon);
528                label.setText(null);
529                label.setVerticalAlignment(JLabel.BOTTOM);
530                log.debug("onIcon set");
531            } else if (value.equals(inconsistentText)) {
532                label.setIcon(null);
533                label.setText("X");
534                label.setForeground(Color.red);
535                label.setVerticalAlignment(JLabel.CENTER);
536                log.debug("Sensor state inconsistent");
537            } else if (value.equals(unknownText)) {
538                label.setIcon(null);
539                label.setText("?");
540                label.setVerticalAlignment(JLabel.CENTER);
541                log.debug("Sensor state unknown");
542            } else { // failed to load icon
543                label.setIcon(null);
544                label.setText(value);
545                label.setVerticalAlignment(JLabel.CENTER);
546                log.warn("Error reading icons for SensorTable");
547            }
548            label.setToolTipText(value);
549            return label;
550        }
551
552        /**
553         * {@inheritDoc}
554         */
555        @Override
556        public Object getCellEditorValue() {
557            log.debug("getCellEditorValue, me = {})", this);
558            return this.toString();
559        }
560
561        /**
562         * Get the row height needed to fit the state icons, loading them if
563         * required. Used by {@link #configureTable} to size the rows once,
564         * outside of the render path.
565         *
566         * @return required row height in pixels, 0 if the icons could not be read
567         */
568        static int getIconRowHeight() {
569            if (iconHeight < 0) {
570                loadIcons();
571            }
572            return Math.max(iconHeight - 5, 0);
573        }
574
575        /**
576         * Read and buffer graphics. Only called once per JVM, the icons are
577         * shared by all instances of this renderer.
578         */
579        private static synchronized void loadIcons() {
580            if (iconHeight >= 0) { // another instance already loaded the icons
581                return;
582            }
583            BufferedImage onImage = null;
584            BufferedImage offImage = null;
585            try {
586                onImage = ImageIO.read(new File(ON_ICON_PATH));
587                offImage = ImageIO.read(new File(OFF_ICON_PATH));
588            } catch (IOException ex) {
589                log.error("error reading image from {} or {}", ON_ICON_PATH, OFF_ICON_PATH, ex);
590            }
591            if (onImage == null || offImage == null) { // ImageIO.read returns null for an unreadable file
592                log.error("error reading image from {} or {}", ON_ICON_PATH, OFF_ICON_PATH);
593                iconHeight = 0; // give up: render states as text, don't retry for every cell
594                return;
595            }
596            log.debug("Success reading images");
597            int imageWidth = onImage.getWidth();
598            int imageHeight = onImage.getHeight();
599            // scale icons 50% to fit in table rows
600            Image smallOnImage = onImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT);
601            Image smallOffImage = offImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT);
602            onIcon = new ImageIcon(smallOnImage);
603            offIcon = new ImageIcon(smallOffImage);
604            iconHeight = onIcon.getIconHeight();
605        }
606
607        /**
608         * Label that ignores revalidate and repaint requests.
609         * <p>
610         * The rendered component stays a child of the table's
611         * CellRendererPane after painting, so a plain JLabel schedules a new
612         * repaint of the whole table each time its icon or text changes
613         * during a paint cycle. Same overrides as DefaultTableCellRenderer,
614         * except firePropertyChange, which BasicLabelUI needs to keep its
615         * view up to date.
616         */
617        private static class StateLabel extends JLabel {
618
619            @Override
620            public void revalidate() {
621                // ignored, see class comment
622            }
623
624            @Override
625            public void repaint() {
626                // ignored, see class comment
627            }
628
629            @Override
630            public void repaint(long tm) {
631                // ignored, see class comment
632            }
633
634            @Override
635            public void repaint(int x, int y, int width, int height) {
636                // ignored, see class comment
637            }
638
639            @Override
640            public void repaint(long tm, int x, int y, int width, int height) {
641                // ignored, see class comment
642            }
643
644            @Override
645            public void repaint(Rectangle r) {
646                // ignored, see class comment
647            }
648        }
649
650    } // end of ImageIconRenderer class
651
652    /**
653     * {@inheritDoc}
654     */
655    @Override
656    public void configureTable(JTable table) {
657        super.configureTable(table);
658        if (_graphicState) {
659            // make the rows tall enough for the state icons, once and outside of the
660            // renderer; must come after super.configureTable, which sets the row
661            // height for the buttons
662            table.setRowHeight(Math.max(table.getRowHeight(), ImageIconRenderer.getIconRowHeight()));
663        }
664        XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel();
665        columnModel.getColumnByModelIndex(FORGETCOL).setHeaderValue(null);
666        columnModel.getColumnByModelIndex(QUERYCOL).setHeaderValue(null);
667    }
668
669    void editButton(Sensor s) {
670        jmri.jmrit.beantable.beanedit.SensorEditAction beanEdit = new jmri.jmrit.beantable.beanedit.SensorEditAction();
671        beanEdit.setBean(s);
672        beanEdit.actionPerformed(null);
673    }
674
675    /**
676     * Show or hide the Debounce columns.
677     * USEGLOBALDELAY, ACTIVEDELAY, INACTIVEDELAY
678     * @param show true to display, false to hide.
679     * @param table the JTable to set column visibility on.
680     */
681    public void showDebounce(boolean show, JTable table) {
682        XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel();
683        TableColumn column = columnModel.getColumnByModelIndex(USEGLOBALDELAY);
684        columnModel.setColumnVisible(column, show);
685        column = columnModel.getColumnByModelIndex(ACTIVEDELAY);
686        columnModel.setColumnVisible(column, show);
687        column = columnModel.getColumnByModelIndex(INACTIVEDELAY);
688        columnModel.setColumnVisible(column, show);
689    }
690
691    /**
692     * Show or hide the Pullup column.
693     * PULLUPCOL
694     * @param show true to display, false to hide.
695     * @param table the JTable to set column visibility on.
696     */
697    public void showPullUp(boolean show, JTable table) {
698        XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel();
699        TableColumn column = columnModel.getColumnByModelIndex(PULLUPCOL);
700        columnModel.setColumnVisible(column, show);
701    }
702
703    /**
704     * Show or hide the State - Forget and Query columns.FORGETCOL, QUERYCOL
705     * @param show true to display, false to hide.
706     * @param table the JTable to set column visibility on.
707     */
708    public void showStateForgetAndQuery(boolean show, JTable table) {
709        XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel();
710        TableColumn column = columnModel.getColumnByModelIndex(FORGETCOL);
711        columnModel.setColumnVisible(column, show);
712        column = columnModel.getColumnByModelIndex(QUERYCOL);
713        columnModel.setColumnVisible(column, show);
714    }
715
716    protected String getClassName() {
717        return jmri.jmrit.beantable.SensorTableAction.class.getName();
718    }
719
720    public String getClassDescription() {
721        return Bundle.getMessage("TitleSensorTable");
722    }
723
724    /**
725     * {@inheritDoc}
726     */
727    @Override
728    protected void setColumnIdentities(JTable table) {
729        super.setColumnIdentities(table);
730        Enumeration<TableColumn> columns;
731        if (table.getColumnModel() instanceof XTableColumnModel) {
732            columns = ((XTableColumnModel) table.getColumnModel()).getColumns(false);
733        } else {
734            columns = table.getColumnModel().getColumns();
735        }
736        while (columns.hasMoreElements()) {
737            TableColumn column = columns.nextElement();
738            switch (column.getModelIndex()) {
739                case FORGETCOL:
740                    column.setIdentifier("ForgetState");
741                    break;
742                case QUERYCOL:
743                    column.setIdentifier("QueryState");
744                    break;
745                default:
746                // use existing value
747            }
748        }
749    }
750
751    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SensorTableDataModel.class);
752
753}