001package jmri.jmrit.beantable.turnout;
002
003import java.awt.event.ActionEvent;
004import java.awt.event.ActionListener;
005import java.awt.event.MouseAdapter;
006import java.awt.event.MouseEvent;
007import java.awt.image.BufferedImage;
008import java.io.File;
009import java.io.IOException;
010
011import javax.annotation.CheckForNull;
012import javax.annotation.Nonnull;
013import javax.imageio.ImageIO;
014import javax.swing.*;
015import javax.swing.table.TableCellEditor;
016import javax.swing.table.TableCellRenderer;
017import javax.swing.table.TableColumn;
018import javax.swing.table.TableModel;
019
020import jmri.*;
021import jmri.implementation.SignalSpeedMap;
022import jmri.jmrit.beantable.*;
023import jmri.util.swing.*;
024
025/**
026 * Data model for a Turnout Table.
027 * Code originally within TurnoutTableAction.
028 *
029 * @author Bob Jacobsen Copyright (C) 2003, 2004, 2007
030 * @author Egbert Broerse Copyright (C) 2017
031 * @author Steve Young Copyright (C) 2021
032 */
033public class TurnoutTableDataModel extends BeanTableDataModel<Turnout>{
034
035    public static final int INVERTCOL = BeanTableDataModel.NUMCOLUMN;
036    public static final int LOCKCOL = INVERTCOL + 1;
037    public static final int EDITCOL = LOCKCOL + 1;
038    public static final int KNOWNCOL = EDITCOL + 1;
039    public static final int MODECOL = KNOWNCOL + 1;
040    public static final int SENSOR1COL = MODECOL + 1;
041    public static final int SENSOR2COL = SENSOR1COL + 1;
042    public static final int OPSONOFFCOL = SENSOR2COL + 1;
043    public static final int OPSEDITCOL = OPSONOFFCOL + 1;
044    public static final int LOCKOPRCOL = OPSEDITCOL + 1;
045    public static final int LOCKDECCOL = LOCKOPRCOL + 1;
046    public static final int STRAIGHTCOL = LOCKDECCOL + 1;
047    public static final int DIVERGCOL = STRAIGHTCOL + 1;
048    public static final int FORGETCOL = DIVERGCOL + 1;
049    public static final int QUERYCOL = FORGETCOL + 1;
050
051    private boolean _graphicState;
052    private TurnoutManager turnoutManager;
053
054
055    String closedText;
056    String thrownText;
057    public String defaultThrownSpeedText;
058    public String defaultClosedSpeedText;
059    // I18N TODO but note storing in xml independent from Locale
060    String useBlockSpeed;
061    String bothText = "Both";
062    String cabOnlyText = "Cab only";
063    String pushbutText = "Pushbutton only";
064    String noneText = "None";
065
066    public final java.util.Vector<String> speedListClosed = new java.util.Vector<>();
067    public final java.util.Vector<String> speedListThrown = new java.util.Vector<>();
068
069
070    public TurnoutTableDataModel(){
071        super();
072        initTable();
073    }
074
075    public TurnoutTableDataModel(Manager<Turnout> mgr){
076        super();
077        setManager(mgr);
078        initTable();
079    }
080
081    private void initTable() {
082
083        // load graphic state column display preference
084        _graphicState = InstanceManager.getDefault(jmri.util.gui.GuiLafPreferencesManager.class).isGraphicTableState();
085
086        closedText = turnoutManager.getClosedText();
087        thrownText = turnoutManager.getThrownText();
088
089        //This following must contain the word Global for a correct match in the abstract turnout
090        defaultThrownSpeedText = (Bundle.getMessage("UseGlobal", "Global") + " " + turnoutManager.getDefaultThrownSpeed());
091        defaultClosedSpeedText = (Bundle.getMessage("UseGlobal", "Global") + " " + turnoutManager.getDefaultClosedSpeed());
092
093        //This following must contain the word Block for a correct match in the abstract turnout
094        useBlockSpeed = Bundle.getMessage("UseGlobal", "Block Speed");
095
096        speedListClosed.add(defaultClosedSpeedText);
097        speedListThrown.add(defaultThrownSpeedText);
098        speedListClosed.add(useBlockSpeed);
099        speedListThrown.add(useBlockSpeed);
100        java.util.Vector<String> _speedMap = jmri.InstanceManager.getDefault(SignalSpeedMap.class).getValidSpeedNames();
101        for (String s : _speedMap) {
102            if (!speedListClosed.contains(s)) {
103                speedListClosed.add(s);
104            }
105            if (!speedListThrown.contains(s)) {
106                speedListThrown.add(s);
107            }
108        }
109
110    }
111
112    /**
113     * {@inheritDoc}
114     */
115    @Override
116    public int getColumnCount() {
117        return QUERYCOL + getPropertyColumnCount() + 1;
118    }
119
120    /**
121     * {@inheritDoc}
122     */
123    @Override
124    public String getColumnName(int col) {
125        switch (col) {
126            case INVERTCOL:
127                return Bundle.getMessage("Inverted");
128            case LOCKCOL:
129                return Bundle.getMessage("Locked");
130            case KNOWNCOL:
131                return Bundle.getMessage("Feedback");
132            case MODECOL:
133                return Bundle.getMessage("ModeLabel");
134            case SENSOR1COL:
135                return Bundle.getMessage("BlockSensor") + " 1";
136            case SENSOR2COL:
137                return Bundle.getMessage("BlockSensor") + " 2";
138            case OPSONOFFCOL:
139                return Bundle.getMessage("TurnoutAutomationMenu");
140            case OPSEDITCOL:
141                return "";
142            case LOCKOPRCOL:
143                return Bundle.getMessage("LockMode");
144            case LOCKDECCOL:
145                return Bundle.getMessage("Decoder");
146            case DIVERGCOL:
147                return Bundle.getMessage("ThrownSpeed");
148            case STRAIGHTCOL:
149                return Bundle.getMessage("ClosedSpeed");
150            case FORGETCOL:
151                return Bundle.getMessage("StateForgetHeader");
152            case QUERYCOL:
153                return Bundle.getMessage("StateQueryHeader");
154            case EDITCOL:
155                return "";
156            default:
157                return super.getColumnName(col);
158        }
159    }
160
161    /**
162     * {@inheritDoc}
163     */
164    @Override
165    protected String getHeaderTooltip(int col) {
166        switch (col) {
167            case SENSOR1COL:
168                return Bundle.getMessage("Sensor1Tip", turnoutManager.getThrownText());
169            case SENSOR2COL:
170                return Bundle.getMessage("Sensor2Tip", turnoutManager.getClosedText());
171            case OPSONOFFCOL:
172                return Bundle.getMessage("TurnoutAutomationTip");
173            case KNOWNCOL:
174                return Bundle.getMessage("FeedbackTip");
175            case MODECOL:
176                return Bundle.getMessage("FeedbackModeTip");
177            default:
178                return super.getHeaderTooltip(col);
179        }
180    }
181
182    /**
183     * {@inheritDoc}
184     */
185    @Override
186    public Class<?> getColumnClass(int col) {
187        switch (col) {
188            case INVERTCOL:
189            case LOCKCOL:
190                return Boolean.class;
191            case KNOWNCOL:
192                return String.class;
193            case MODECOL:
194            case SENSOR1COL:
195            case SENSOR2COL:
196            case OPSONOFFCOL:
197            case LOCKOPRCOL:
198            case LOCKDECCOL:
199            case DIVERGCOL:
200            case STRAIGHTCOL:
201                return JComboBox.class;
202            case OPSEDITCOL:
203            case EDITCOL:
204            case FORGETCOL:
205            case QUERYCOL:
206                return JButton.class;
207            case VALUECOL: // may use an image to show turnout state
208                return ( _graphicState ? JLabel.class : JButton.class );
209            default:
210                return super.getColumnClass(col);
211        }
212    }
213
214    /**
215     * {@inheritDoc}
216     */
217    @Override
218    public int getPreferredWidth(int col) {
219        switch (col) {
220            case INVERTCOL:
221            case LOCKCOL:
222                return new JTextField(6).getPreferredSize().width;
223            case LOCKOPRCOL:
224            case LOCKDECCOL:
225            case KNOWNCOL:
226            case MODECOL:
227                return new JTextField(10).getPreferredSize().width;
228            case SENSOR1COL:
229            case SENSOR2COL:
230                return new JTextField(5).getPreferredSize().width;
231            case OPSEDITCOL:
232                return new JButton(Bundle.getMessage("EditTurnoutOperation")).getPreferredSize().width;
233            case EDITCOL:
234                return new JButton(Bundle.getMessage("ButtonEdit")).getPreferredSize().width+4;
235            case OPSONOFFCOL:
236                return new JTextField(Bundle.getMessage("TurnoutAutomationMenu")).getPreferredSize().width;
237            case DIVERGCOL:
238            case STRAIGHTCOL:
239                return new JTextField(14).getPreferredSize().width;
240            case FORGETCOL:
241                return new JButton(Bundle.getMessage("StateForgetButton")).getPreferredSize().width;
242            case QUERYCOL:
243                return new JButton(Bundle.getMessage("StateQueryButton")).getPreferredSize().width;
244            default:
245                return super.getPreferredWidth(col);
246        }
247    }
248
249    /**
250     * {@inheritDoc}
251     */
252    @Override
253    public boolean isCellEditable(int row, int col) {
254        Turnout t = turnoutManager.getBySystemName(sysNameList.get(row));
255        if (t == null){
256            return false;
257        }
258        switch (col) {
259            case INVERTCOL:
260                return t.canInvert();
261            case LOCKCOL:
262                // checkbox disabled unless current configuration allows locking
263                return t.canLock(Turnout.CABLOCKOUT | Turnout.PUSHBUTTONLOCKOUT);
264            case OPSEDITCOL:
265                return t.getTurnoutOperation() != null;
266            case KNOWNCOL:
267                return false;
268            case MODECOL:
269            case SENSOR1COL:
270            case SENSOR2COL:
271            case OPSONOFFCOL:
272            case LOCKOPRCOL: // editable always so user can configure it, even if current configuration prevents locking now
273            case LOCKDECCOL: // editable always so user can configure it, even if current configuration prevents locking now
274            case DIVERGCOL:
275            case STRAIGHTCOL:
276            case EDITCOL:
277            case FORGETCOL:
278            case QUERYCOL:
279                return true;
280            default:
281                return super.isCellEditable(row, col);
282        }
283    }
284
285    /**
286     * {@inheritDoc}
287     */
288    @Override
289    public Object getValueAt(int row, int col) {
290        // some error checking
291        if (row >= sysNameList.size()) {
292            log.warn("row is greater than name list");
293            return "error";
294        }
295        String name = sysNameList.get(row);
296        TurnoutManager manager = turnoutManager;
297        Turnout t = manager.getBySystemName(name);
298        if (t == null) {
299            log.debug("error null turnout!");
300            return "error";
301        }
302        if (col == INVERTCOL) {
303            return t.getInverted();
304        } else if (col == LOCKCOL) {
305            return t.getLocked(Turnout.CABLOCKOUT | Turnout.PUSHBUTTONLOCKOUT);
306        } else if (col == KNOWNCOL) {
307            return t.describeState(t.getKnownState());
308        } else if (col == MODECOL) {
309            JComboBox<String> c = new JComboBox<>(t.getValidFeedbackNames());
310            c.setSelectedItem(t.getFeedbackModeName());
311            return c;
312        } else if (col == SENSOR1COL) {
313            return t.getFirstSensor();
314        } else if (col == SENSOR2COL) {
315            return t.getSecondSensor();
316        } else if (col == OPSONOFFCOL) {
317            return makeAutomationBox(t);
318        } else if (col == OPSEDITCOL) {
319            return Bundle.getMessage("EditTurnoutOperation");
320        } else if (col == EDITCOL) {
321            return Bundle.getMessage("ButtonEdit");
322        } else if (col == LOCKDECCOL) {
323            JComboBox<String> c;
324            if ((t.getPossibleLockModes() & Turnout.PUSHBUTTONLOCKOUT) != 0) {
325                c = new JComboBox<>(t.getValidDecoderNames());
326            } else {
327                c = new JComboBox<>(new String[]{t.getDecoderName()});
328            }
329
330            c.setSelectedItem(t.getDecoderName());
331            return c;
332        } else if (col == LOCKOPRCOL) {
333
334            java.util.Vector<String> lockOperations = new java.util.Vector<>();  // Vector is a JComboBox ctor; List is not
335            int modes = t.getPossibleLockModes();
336            if ((modes & Turnout.CABLOCKOUT) != 0 && (modes & Turnout.PUSHBUTTONLOCKOUT) != 0) {
337                lockOperations.add(bothText);
338            }
339            if ((modes & Turnout.CABLOCKOUT) != 0) {
340                lockOperations.add(cabOnlyText);
341            }
342            if ((modes & Turnout.PUSHBUTTONLOCKOUT) != 0) {
343                lockOperations.add(pushbutText);
344            }
345            lockOperations.add(noneText);
346            JComboBox<String> c = new JComboBox<>(lockOperations);
347
348            if (t.canLock(Turnout.CABLOCKOUT) && t.canLock(Turnout.PUSHBUTTONLOCKOUT)) {
349                c.setSelectedItem(bothText);
350            } else if (t.canLock(Turnout.PUSHBUTTONLOCKOUT)) {
351                c.setSelectedItem(pushbutText);
352            } else if (t.canLock(Turnout.CABLOCKOUT)) {
353                c.setSelectedItem(cabOnlyText);
354            } else {
355                c.setSelectedItem(noneText);
356            }
357            return c;
358        } else if (col == STRAIGHTCOL) {
359
360            String speed = t.getStraightSpeed();
361            if (!speedListClosed.contains(speed)) {
362                speedListClosed.add(speed);
363            }
364            JComboBox<String> c = new JComboBox<>(speedListClosed);
365            c.setEditable(true);
366            c.setSelectedItem(speed);
367            JComboBoxUtil.setupComboBoxMaxRows(c);
368            return c;
369        } else if (col == DIVERGCOL) {
370
371            String speed = t.getDivergingSpeed();
372            if (!speedListThrown.contains(speed)) {
373                speedListThrown.add(speed);
374            }
375            JComboBox<String> c = new JComboBox<>(speedListThrown);
376            c.setEditable(true);
377            c.setSelectedItem(speed);
378            JComboBoxUtil.setupComboBoxMaxRows(c);
379            return c;
380            // } else if (col == VALUECOL && _graphicState) { // not neeeded as the
381            //  graphic ImageIconRenderer uses the same super.getValueAt(row, col) as
382            // classic bean state text button
383        } else if (col == FORGETCOL) {
384            return Bundle.getMessage("StateForgetButton");
385        } else if (col == QUERYCOL) {
386            return Bundle.getMessage("StateQueryButton");
387        }
388        return super.getValueAt(row, col);
389    }
390
391    /**
392     * {@inheritDoc}
393     */
394    @Override
395    public void setValueAt(Object value, int row, int col) {
396        String name = sysNameList.get(row);
397        Turnout t = turnoutManager.getBySystemName(name);
398        if (t == null) {
399            NullPointerException ex = new NullPointerException("Unexpected null turnout in turnout table");
400            log.error("No Turnout with system name \"{}\" exists ", name , ex); // log with stack trace
401            throw ex;
402        }
403        if (col == INVERTCOL) {
404            if (t.canInvert()) {
405                t.setInverted((Boolean) value);
406            }
407        } else if (col == LOCKCOL) {
408            t.setLocked(Turnout.CABLOCKOUT | Turnout.PUSHBUTTONLOCKOUT, (Boolean) value);
409        } else if (col == MODECOL) {
410            @SuppressWarnings("unchecked")
411            String modeName = (String) ((JComboBox<String>) value).getSelectedItem();
412            assert modeName != null;
413            t.setFeedbackMode(modeName);
414        } else if (col == SENSOR1COL) {
415            try {
416                Sensor sensor = (Sensor) value;
417                t.provideFirstFeedbackSensor(sensor != null ? sensor.getDisplayName() : null);
418            } catch (jmri.JmriException e) {
419                JmriJOptionPane.showMessageDialog(null, e.toString());
420            }
421        } else if (col == SENSOR2COL) {
422            try {
423                Sensor sensor = (Sensor) value;
424                t.provideSecondFeedbackSensor(sensor != null ? sensor.getDisplayName() : null);
425            } catch (jmri.JmriException e) {
426                JmriJOptionPane.showMessageDialog(null, e.toString());
427            }
428        } else if (col == OPSONOFFCOL) {
429            // do nothing as this is handled by the combo box listener
430            // column still handled here to prevent call to super.setValueAt
431        } else if (col == OPSEDITCOL) {
432            t.setInhibitOperation(false);
433            @SuppressWarnings("unchecked") // cast to JComboBox<String> required in OPSEDITCOL
434            JComboBox<String> cb = (JComboBox<String>) getValueAt(row, OPSONOFFCOL);
435            log.debug("opsSelected = {}", getValueAt(row, OPSONOFFCOL).toString());
436            editTurnoutOperation(t, cb);
437            fireTableRowsUpdated(row, row);
438        } else if (col == EDITCOL) {
439            javax.swing.SwingUtilities.invokeLater(() -> {
440                editButton(t);
441            });
442        } else if (col == LOCKOPRCOL) {
443            @SuppressWarnings("unchecked")
444            String lockOpName = (String) ((JComboBox<String>) value)
445                    .getSelectedItem();
446            assert lockOpName != null;
447            if (lockOpName.equals(bothText)) {
448                t.enableLockOperation(Turnout.CABLOCKOUT | Turnout.PUSHBUTTONLOCKOUT, true);
449            }
450            if (lockOpName.equals(cabOnlyText)) {
451                t.enableLockOperation(Turnout.CABLOCKOUT, true);
452                t.enableLockOperation(Turnout.PUSHBUTTONLOCKOUT, false);
453            }
454            if (lockOpName.equals(pushbutText)) {
455                t.enableLockOperation(Turnout.CABLOCKOUT, false);
456                t.enableLockOperation(Turnout.PUSHBUTTONLOCKOUT, true);
457            }
458            fireTableRowsUpdated(row, row);
459        } else if (col == LOCKDECCOL) {
460            @SuppressWarnings("unchecked")
461            String decoderName = (String) ((JComboBox<String>) value).getSelectedItem();
462            t.setDecoderName(decoderName);
463            fireTableRowsUpdated(row, row);
464        } else if (col == STRAIGHTCOL) {
465            @SuppressWarnings("unchecked")
466            String speed = (String) ((JComboBox<String>) value).getSelectedItem();
467            try {
468                t.setStraightSpeed(speed);
469            } catch (jmri.JmriException ex) {
470                JmriJOptionPane.showMessageDialog(null, ex.getMessage() + "\n" + speed);
471                return;
472            }
473            if ((!speedListClosed.contains(speed))) {
474                assert speed != null;
475                if (!speed.contains("Global")) {
476                    speedListClosed.add(speed);
477                }
478            }
479        } else if (col == DIVERGCOL) {
480
481            @SuppressWarnings("unchecked")
482            String speed = (String) ((JComboBox<String>) value).getSelectedItem();
483            try {
484                t.setDivergingSpeed(speed);
485            } catch (jmri.JmriException ex) {
486                JmriJOptionPane.showMessageDialog(null, ex.getMessage() + "\n" + speed);
487                return;
488            }
489            if ((!speedListThrown.contains(speed))) {
490                assert speed != null;
491                if (!speed.contains("Global")) {
492                    speedListThrown.add(speed);
493                }
494            }
495        } else if (col == FORGETCOL) {
496            t.setCommandedState(Turnout.UNKNOWN);
497        } else if (col == QUERYCOL) {
498            t.setCommandedState(Turnout.UNKNOWN);
499            t.requestUpdateFromLayout();
500        } else if (col == VALUECOL && _graphicState) { // respond to clicking on ImageIconRenderer CellEditor
501            clickOn(t);
502            fireTableRowsUpdated(row, row);
503        } else {
504            super.setValueAt(value, row, col);
505            if (row < getRowCount()) {
506                fireTableRowsUpdated(row, row);
507            }
508        }
509    }
510
511    /**
512     * {@inheritDoc}
513     */
514    @Override
515    public String getValue(@Nonnull String name) {
516        Turnout turn = turnoutManager.getBySystemName(name);
517        if (turn != null) {
518            return turn.describeState(turn.getCommandedState());
519        }
520        return "Turnout not found";
521    }
522
523    /**
524     * {@inheritDoc}
525     */
526    @Override
527    public Manager<Turnout> getManager() {
528        if (turnoutManager == null) {
529            turnoutManager = InstanceManager.getDefault(TurnoutManager.class);
530        }
531        return turnoutManager;
532    }
533
534    /**
535     * {@inheritDoc}
536     */
537    @Override
538    protected final void setManager(@Nonnull Manager<Turnout> manager) {
539        if (!(manager instanceof TurnoutManager)) {
540            return;
541        }
542        getManager().removePropertyChangeListener(this);
543        if (sysNameList != null) {
544            for (int i = 0; i < sysNameList.size(); i++) {
545                // if object has been deleted, it's not here; ignore it
546                NamedBean b = getBySystemName(sysNameList.get(i));
547                if (b != null) {
548                    b.removePropertyChangeListener(this);
549                }
550            }
551        }
552        turnoutManager = (TurnoutManager) manager;
553        getManager().addPropertyChangeListener(this);
554        updateNameList();
555    }
556
557    @Override
558    public Turnout getBySystemName(@Nonnull String name) {
559        return turnoutManager.getBySystemName(name);
560    }
561
562    @Override
563    public Turnout getByUserName(@Nonnull String name) {
564        return InstanceManager.getDefault(TurnoutManager.class).getByUserName(name);
565    }
566
567    @Override
568    protected String getMasterClassName() {
569        // Force message grouping
570        return jmri.jmrit.beantable.TurnoutTableAction.class.getName();
571    }
572
573    protected String getClassName() {
574        return jmri.jmrit.beantable.TurnoutTableAction.class.getName();
575    }
576
577    @Override
578    public void clickOn(Turnout t) {
579        t.setCommandedState( t.getCommandedState()== Turnout.CLOSED ? Turnout.THROWN : Turnout.CLOSED);
580    }
581
582    @Override
583    public void configureTable(JTable tbl) {
584
585        setColumnToHoldButton(tbl, EDITCOL, editButton());
586        setColumnToHoldButton(tbl, OPSEDITCOL, editButton());
587
588        //Hide the following columns by default
589        XTableColumnModel columnModel = (XTableColumnModel) tbl.getColumnModel();
590        TableColumn column = columnModel.getColumnByModelIndex(STRAIGHTCOL);
591        columnModel.setColumnVisible(column, false);
592        column = columnModel.getColumnByModelIndex(DIVERGCOL);
593        columnModel.setColumnVisible(column, false);
594        column = columnModel.getColumnByModelIndex(KNOWNCOL);
595        columnModel.setColumnVisible(column, false);
596        column = columnModel.getColumnByModelIndex(MODECOL);
597        columnModel.setColumnVisible(column, false);
598        column = columnModel.getColumnByModelIndex(SENSOR1COL);
599        columnModel.setColumnVisible(column, false);
600        column = columnModel.getColumnByModelIndex(SENSOR2COL);
601        columnModel.setColumnVisible(column, false);
602        column = columnModel.getColumnByModelIndex(OPSONOFFCOL);
603        columnModel.setColumnVisible(column, false);
604        column = columnModel.getColumnByModelIndex(OPSEDITCOL);
605        columnModel.setColumnVisible(column, false);
606        column = columnModel.getColumnByModelIndex(LOCKOPRCOL);
607        columnModel.setColumnVisible(column, false);
608        column = columnModel.getColumnByModelIndex(LOCKDECCOL);
609        columnModel.setColumnVisible(column, false);
610        column = columnModel.getColumnByModelIndex(FORGETCOL);
611        columnModel.setColumnVisible(column, false);
612        column = columnModel.getColumnByModelIndex(QUERYCOL);
613        columnModel.setColumnVisible(column, false);
614
615
616        // and then set user prefs
617        super.configureTable(tbl);
618
619        columnModel.getColumnByModelIndex(FORGETCOL).setHeaderValue(null);
620        columnModel.getColumnByModelIndex(QUERYCOL).setHeaderValue(null);
621
622    }
623
624    // update table if turnout lock or feedback changes
625    @Override
626    protected boolean matchPropertyName(java.beans.PropertyChangeEvent e) {
627        switch (e.getPropertyName()) {
628            case Turnout.PROPERTY_LOCKED:
629            case Turnout.PROPERTY_INVERTED:
630            case Turnout.PROPERTY_FEEDBACK_MODE: // feedback type setting change, NOT Turnout feedback status
631            case Turnout.PROPERTY_TURNOUT_DIVERGING_SPEED:
632            case Turnout.PROPERTY_TURNOUT_STRAIGHT_SPEED:
633            case Turnout.PROPERTY_TURNOUT_FEEDBACK_FIRST_SENSOR:
634            case Turnout.PROPERTY_TURNOUT_FEEDBACK_SECOND_SENSOR:
635            case Turnout.PROPERTY_DECODER_NAME:
636            case Turnout.PROPERTY_TURNOUT_OPERATION_STATE:
637            case Turnout.PROPERTY_KNOWN_STATE:
638            case Turnout.PROPERTY_COMMANDED_STATE:
639                return true;
640            default:
641                return super.matchPropertyName(e);
642        }
643    }
644
645    @Override
646    public void propertyChange(java.beans.PropertyChangeEvent e) {
647        switch (e.getPropertyName()) {
648            case TurnoutManager.PROPERTY_DEFAULT_CLOSED_SPEED:
649                updateClosedList();
650                break;
651            case TurnoutManager.PROPERTY_DEFAULT_THROWN_SPEED:
652                updateThrownList();
653                break;
654            default:
655                super.propertyChange(e);
656                break;
657        }
658    }
659
660    /**
661     * Customize the turnout table Value (State) column to show an
662     * appropriate graphic for the turnout state if _graphicState =
663     * true, or (default) just show the localized state text when the
664     * TableDataModel is being called from ListedTableAction.
665     *
666     * @param table a JTable of Turnouts
667     */
668    @Override
669    protected void configValueColumn(JTable table) {
670        // have the value column hold a JPanel (icon)
671        //setColumnToHoldButton(table, VALUECOL, new JLabel("12345678")); // for larger, wide round icon, but cannot be converted to JButton
672        // add extras, override BeanTableDataModel
673        log.debug("Turnout configValueColumn (I am {})", super.toString());
674        if (_graphicState) { // load icons, only once
675            table.setDefaultEditor(JLabel.class, new ImageIconRenderer()); // editor
676            table.setDefaultRenderer(JLabel.class, new ImageIconRenderer()); // item class copied from SwitchboardEditor panel
677        } else {
678            super.configValueColumn(table); // classic text style state indication
679        }
680    }
681
682    @Override
683    public JTable makeJTable(@Nonnull String name, @Nonnull TableModel model, @CheckForNull RowSorter<? extends TableModel> sorter) {
684        if (!(model instanceof TurnoutTableDataModel)){
685            throw new IllegalArgumentException("Model is not a TurnoutTableDataModel");
686        }
687        return configureJTable(name, new TurnoutTableJTable((TurnoutTableDataModel)model), sorter);
688    }
689
690    @Override
691    protected void setColumnIdentities(JTable table) {
692        super.setColumnIdentities(table);
693        java.util.Enumeration<TableColumn> columns;
694        if (table.getColumnModel() instanceof XTableColumnModel) {
695            columns = ((XTableColumnModel) table.getColumnModel()).getColumns(false);
696        } else {
697            columns = table.getColumnModel().getColumns();
698        }
699        while (columns.hasMoreElements()) {
700            TableColumn column = columns.nextElement();
701            switch (column.getModelIndex()) {
702                case FORGETCOL:
703                    column.setIdentifier("ForgetState");
704                    break;
705                case QUERYCOL:
706                    column.setIdentifier("QueryState");
707                    break;
708                case SENSOR1COL:
709                    column.setIdentifier("Sensor1");
710                    break;
711                case SENSOR2COL:
712                    column.setIdentifier("Sensor2");
713                    break;
714                default:
715                // use existing value
716            }
717        }
718    }
719
720    /**
721     * Pop up a TurnoutOperationConfig for the turnout.
722     *
723     * @param t   turnout
724     * @param box JComboBox that triggered the edit
725     */
726    protected void editTurnoutOperation( @Nonnull Turnout t, JComboBox<String> box) {
727        if (!editingOps.getAndSet(true)) { // don't open a second edit ops pane
728            TurnoutOperation op = t.getTurnoutOperation();
729            if (op == null) {
730                TurnoutOperation proto = InstanceManager.getDefault(TurnoutOperationManager.class).getMatchingOperationAlways(t);
731                if (proto != null) {
732                    op = proto.makeNonce(t);
733                    t.setTurnoutOperation(op);
734                }
735            }
736            if (op != null) {
737                if (!op.isNonce()) {
738                    op = op.makeNonce(t);
739                }
740                // make and show edit dialog
741                log.debug("TurnoutOpsEditDialog starting");
742                java.awt.Window w = JmriJOptionPane.findWindowForObject(box);
743                TurnoutOperationEditorDialog dialog = new TurnoutOperationEditorDialog(op, t, w);
744                dialog.setVisible(true);
745            } else {
746                JmriJOptionPane.showMessageDialog(box, Bundle.getMessage("TurnoutOperationErrorDialog"),
747                        Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
748            }
749        }
750    }
751
752    /**
753     * Create a {@literal JComboBox<String>} containing all the options for
754     * turnout automation parameters for this turnout.
755     *
756     * @param t the turnout
757     * @return the JComboBox
758     */
759    protected JComboBox<String> makeAutomationBox(Turnout t) {
760        String[] str = new String[]{"empty"};
761        final JComboBox<String> cb = new JComboBox<>(str);
762        final Turnout myTurnout = t;
763        TurnoutTableAction.updateAutomationBox(t, cb);
764        cb.addActionListener(new ActionListener() {
765            @Override
766            public void actionPerformed(ActionEvent e) {
767                setTurnoutOperation(myTurnout, cb);
768                cb.removeActionListener(this);  // avoid recursion
769                TurnoutTableAction.updateAutomationBox(myTurnout, cb);
770                cb.addActionListener(this);
771            }
772        });
773        return cb;
774    }
775
776    /**
777     * Set the turnout's operation info based on the contents of the combo box.
778     *
779     * @param t  turnout being configured
780     * @param cb JComboBox for ops for t in the TurnoutTable
781     */
782    protected void setTurnoutOperation( @Nonnull Turnout t, JComboBox<String> cb) {
783        switch (cb.getSelectedIndex()) {
784            case 0:   // Off
785                t.setInhibitOperation(true);
786                t.setTurnoutOperation(null);
787                break;
788            case 1:   // Default
789                t.setInhibitOperation(false);
790                t.setTurnoutOperation(null);
791                break;
792            default:  // named operation
793                t.setInhibitOperation(false);
794                t.setTurnoutOperation(InstanceManager.getDefault(TurnoutOperationManager.class).
795                        getOperation(((String) java.util.Objects.requireNonNull(cb.getSelectedItem()))));
796                break;
797        }
798    }
799
800    /**
801     * Create action to edit a turnout in Edit pane. (also used in windowTest)
802     *
803     * @param t the turnout to be edited
804     */
805    void editButton(Turnout t) {
806        jmri.jmrit.beantable.beanedit.TurnoutEditAction beanEdit = new jmri.jmrit.beantable.beanedit.TurnoutEditAction();
807        beanEdit.setBean(t);
808        beanEdit.actionPerformed(null);
809    }
810
811    /**
812     * Create a JButton to edit a turnout's operation.
813     *
814     * @return the JButton
815     */
816    protected JButton editButton() {
817        return new JButton(Bundle.getMessage("EditTurnoutOperation"));
818    }
819
820    private void updateClosedList() {
821        speedListClosed.remove(defaultClosedSpeedText);
822        defaultClosedSpeedText = (Bundle.getMessage("UseGlobal", "Global") + " " + turnoutManager.getDefaultClosedSpeed());
823        speedListClosed.add(0, defaultClosedSpeedText);
824        fireTableDataChanged();
825    }
826
827    private void updateThrownList() {
828        speedListThrown.remove(defaultThrownSpeedText);
829        defaultThrownSpeedText = (Bundle.getMessage("UseGlobal", "Global") + " " + turnoutManager.getDefaultThrownSpeed());
830        speedListThrown.add(0, defaultThrownSpeedText);
831        fireTableDataChanged();
832    }
833
834    public void showFeedbackChanged(boolean visible, JTable table ) {
835        XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel();
836        TableColumn column = columnModel.getColumnByModelIndex(KNOWNCOL);
837        columnModel.setColumnVisible(column, visible);
838        column = columnModel.getColumnByModelIndex(MODECOL);
839        columnModel.setColumnVisible(column, visible);
840        column = columnModel.getColumnByModelIndex(SENSOR1COL);
841        columnModel.setColumnVisible(column, visible);
842        column = columnModel.getColumnByModelIndex(SENSOR2COL);
843        columnModel.setColumnVisible(column, visible);
844        column = columnModel.getColumnByModelIndex(OPSONOFFCOL);
845        columnModel.setColumnVisible(column, visible);
846        column = columnModel.getColumnByModelIndex(OPSEDITCOL);
847        columnModel.setColumnVisible(column, visible);
848    }
849
850    public void showLockChanged(boolean visible, JTable table) {
851        XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel();
852        TableColumn column = ((XTableColumnModel) table.getColumnModel()).getColumnByModelIndex(LOCKDECCOL);
853        columnModel.setColumnVisible(column, visible);
854        column = columnModel.getColumnByModelIndex(LOCKOPRCOL);
855        columnModel.setColumnVisible(column, visible);
856    }
857
858    public void showTurnoutSpeedChanged(boolean visible, JTable table) {
859        XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel();
860        TableColumn column = ((XTableColumnModel) table.getColumnModel()).getColumnByModelIndex(STRAIGHTCOL);
861        columnModel.setColumnVisible(column, visible);
862        column = columnModel.getColumnByModelIndex(DIVERGCOL);
863        columnModel.setColumnVisible(column, visible);
864    }
865
866    public void showStateForgetAndQueryChanged(boolean visible, JTable table) {
867        XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel();
868        TableColumn column = columnModel.getColumnByModelIndex(FORGETCOL);
869        columnModel.setColumnVisible(column, visible);
870        column = columnModel.getColumnByModelIndex(QUERYCOL);
871        columnModel.setColumnVisible(column, visible);
872    }
873
874
875    /**
876     * Visualize state in table as a graphic, customized for Turnouts (4
877     * states).
878     * Renderer and Editor are identical, as the cell contents
879     * are not actually edited, only used to toggle state using
880     * {@link #clickOn(Turnout)}.
881     *
882     */
883    class ImageIconRenderer extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
884
885        protected JLabel label;
886        protected String rootPath = "resources/icons/misc/switchboard/"; // also used in display.switchboardEditor
887        protected char beanTypeChar = 'T'; // for Turnout
888        protected String onIconPath = rootPath + beanTypeChar + "-on-s.png";
889        protected String offIconPath = rootPath + beanTypeChar + "-off-s.png";
890        protected BufferedImage onImage;
891        protected BufferedImage offImage;
892        protected ImageIcon onIcon;
893        protected ImageIcon offIcon;
894        protected int iconHeight = -1;
895
896        @Override
897        public java.awt.Component getTableCellRendererComponent(
898                JTable table, Object value, boolean isSelected,
899                boolean hasFocus, int row, int column) {
900            log.debug("Renderer Item = {}, State = {}", row, value);
901            if (iconHeight < 0) { // load resources only first time, either for renderer or editor
902                loadIcons();
903                log.debug("icons loaded");
904            }
905            return updateLabel((String) value, row, table);
906        }
907
908        @Override
909        public java.awt.Component getTableCellEditorComponent(
910                JTable table, Object value, boolean isSelected,
911                int row, int column) {
912            log.debug("Renderer Item = {}, State = {}", row, value);
913            if (iconHeight < 0) { // load resources only first time, either for renderer or editor
914                loadIcons();
915                log.debug("icons loaded");
916            }
917            return updateLabel((String) value, row, table);
918        }
919
920        public JLabel updateLabel(String value, int row, JTable table) {
921            if (iconHeight > 0) { // if necessary, increase row height;
922                table.setRowHeight(row, Math.max(table.getRowHeight(), iconHeight - 5));
923            }
924            if (value.equals(closedText) && onIcon != null) {
925                label = new JLabel(onIcon);
926                label.setVerticalAlignment(JLabel.BOTTOM);
927                log.debug("onIcon set");
928            } else if (value.equals(thrownText) && offIcon != null) {
929                label = new JLabel(offIcon);
930                label.setVerticalAlignment(JLabel.BOTTOM);
931                log.debug("offIcon set");
932            } else if (value.equals(Bundle.getMessage("BeanStateInconsistent"))) {
933                label = new JLabel("X", JLabel.CENTER); // centered text alignment
934                label.setForeground(java.awt.Color.red);
935                log.debug("Turnout state inconsistent");
936                iconHeight = 0;
937            } else if (value.equals(Bundle.getMessage("BeanStateUnknown"))) {
938                label = new JLabel("?", JLabel.CENTER); // centered text alignment
939                log.debug("Turnout state unknown");
940                iconHeight = 0;
941            } else { // failed to load icon
942                label = new JLabel(value, JLabel.CENTER); // centered text alignment
943                log.warn("Error reading icons for TurnoutTable");
944                iconHeight = 0;
945            }
946            label.setToolTipText(value);
947            label.addMouseListener(new MouseAdapter() {
948                @Override
949                public final void mousePressed(MouseEvent evt) {
950                    log.debug("Clicked on icon in row {}", row);
951                    stopCellEditing();
952                }
953            });
954            return label;
955        }
956
957        @Override
958        public Object getCellEditorValue() {
959            log.debug("getCellEditorValue, me = {})", this.toString());
960            return this.toString();
961        }
962
963        /**
964         * Read and buffer graphics. Only called once for this table.
965         *
966         * @see #getTableCellEditorComponent(JTable, Object, boolean,
967         * int, int)
968         */
969        protected void loadIcons() {
970            try {
971                onImage = ImageIO.read(new File(onIconPath));
972                offImage = ImageIO.read(new File(offIconPath));
973            } catch (IOException ex) {
974                log.error("error reading image from {} or {}", onIconPath, offIconPath, ex);
975            }
976            log.debug("Success reading images");
977            int imageWidth = onImage.getWidth();
978            int imageHeight = onImage.getHeight();
979            // scale icons 50% to fit in table rows
980            java.awt.Image smallOnImage = onImage.getScaledInstance(imageWidth / 2, imageHeight / 2, java.awt.Image.SCALE_DEFAULT);
981            java.awt.Image smallOffImage = offImage.getScaledInstance(imageWidth / 2, imageHeight / 2, java.awt.Image.SCALE_DEFAULT);
982            onIcon = new ImageIcon(smallOnImage);
983            offIcon = new ImageIcon(smallOffImage);
984            iconHeight = onIcon.getIconHeight();
985        }
986
987    } // end of ImageIconRenderer class
988
989    protected static java.util.concurrent.atomic.AtomicBoolean editingOps = new java.util.concurrent.atomic.AtomicBoolean(false);
990
991    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TurnoutTableDataModel.class);
992
993}