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