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    static public final int INVERTCOL = BeanTableDataModel.NUMCOLUMN;
036    static public final int LOCKCOL = INVERTCOL + 1;
037    static public final int EDITCOL = LOCKCOL + 1;
038    static public final int KNOWNCOL = EDITCOL + 1;
039    static public final int MODECOL = KNOWNCOL + 1;
040    static public final int SENSOR1COL = MODECOL + 1;
041    static public final int SENSOR2COL = SENSOR1COL + 1;
042    static public final int OPSONOFFCOL = SENSOR2COL + 1;
043    static public final int OPSEDITCOL = OPSONOFFCOL + 1;
044    static public final int LOCKOPRCOL = OPSEDITCOL + 1;
045    static public final int LOCKDECCOL = LOCKOPRCOL + 1;
046    static public final int STRAIGHTCOL = LOCKDECCOL + 1;
047    static public final int DIVERGCOL = STRAIGHTCOL + 1;
048    static public final int FORGETCOL = DIVERGCOL + 1;
049    static public 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        return getClassName();
570    }
571
572    protected String getClassName() {
573        return jmri.jmrit.beantable.TurnoutTableAction.class.getName();
574    }
575
576    @Override
577    public void clickOn(Turnout t) {
578        t.setCommandedState( t.getCommandedState()== Turnout.CLOSED ? Turnout.THROWN : Turnout.CLOSED);
579    }
580
581    @Override
582    public void configureTable(JTable tbl) {
583
584        setColumnToHoldButton(tbl, EDITCOL, editButton());
585        setColumnToHoldButton(tbl, OPSEDITCOL, editButton());
586
587        //Hide the following columns by default
588        XTableColumnModel columnModel = (XTableColumnModel) tbl.getColumnModel();
589        TableColumn column = columnModel.getColumnByModelIndex(STRAIGHTCOL);
590        columnModel.setColumnVisible(column, false);
591        column = columnModel.getColumnByModelIndex(DIVERGCOL);
592        columnModel.setColumnVisible(column, false);
593        column = columnModel.getColumnByModelIndex(KNOWNCOL);
594        columnModel.setColumnVisible(column, false);
595        column = columnModel.getColumnByModelIndex(MODECOL);
596        columnModel.setColumnVisible(column, false);
597        column = columnModel.getColumnByModelIndex(SENSOR1COL);
598        columnModel.setColumnVisible(column, false);
599        column = columnModel.getColumnByModelIndex(SENSOR2COL);
600        columnModel.setColumnVisible(column, false);
601        column = columnModel.getColumnByModelIndex(OPSONOFFCOL);
602        columnModel.setColumnVisible(column, false);
603        column = columnModel.getColumnByModelIndex(OPSEDITCOL);
604        columnModel.setColumnVisible(column, false);
605        column = columnModel.getColumnByModelIndex(LOCKOPRCOL);
606        columnModel.setColumnVisible(column, false);
607        column = columnModel.getColumnByModelIndex(LOCKDECCOL);
608        columnModel.setColumnVisible(column, false);
609        column = columnModel.getColumnByModelIndex(FORGETCOL);
610        columnModel.setColumnVisible(column, false);
611        column = columnModel.getColumnByModelIndex(QUERYCOL);
612        columnModel.setColumnVisible(column, false);
613
614
615        // and then set user prefs
616        super.configureTable(tbl);
617
618        columnModel.getColumnByModelIndex(FORGETCOL).setHeaderValue(null);
619        columnModel.getColumnByModelIndex(QUERYCOL).setHeaderValue(null);
620
621    }
622
623    // update table if turnout lock or feedback changes
624    @Override
625    protected boolean matchPropertyName(java.beans.PropertyChangeEvent e) {
626        switch (e.getPropertyName()) {
627            case "locked":
628            case "inverted":
629            case "feedbackchange": // feedback type setting change, NOT Turnout feedback status
630            case "TurnoutDivergingSpeedChange":
631            case "TurnoutStraightSpeedChange":
632            case "turnoutFeedbackFirstSensorChange":
633            case "turnoutFeedbackSecondSensorChange":
634            case "decoderNameChange":
635            case "TurnoutOperationState":
636            case "KnownState":
637                return true;
638            default:
639                return super.matchPropertyName(e);
640        }
641    }
642
643    @Override
644    public void propertyChange(java.beans.PropertyChangeEvent e) {
645        switch (e.getPropertyName()) {
646            case "DefaultTurnoutClosedSpeedChange":
647                updateClosedList();
648                break;
649            case "DefaultTurnoutThrownSpeedChange":
650                updateThrownList();
651                break;
652            default:
653                super.propertyChange(e);
654                break;
655        }
656    }
657
658    /**
659     * Customize the turnout table Value (State) column to show an
660     * appropriate graphic for the turnout state if _graphicState =
661     * true, or (default) just show the localized state text when the
662     * TableDataModel is being called from ListedTableAction.
663     *
664     * @param table a JTable of Turnouts
665     */
666    @Override
667    protected void configValueColumn(JTable table) {
668        // have the value column hold a JPanel (icon)
669        //setColumnToHoldButton(table, VALUECOL, new JLabel("12345678")); // for larger, wide round icon, but cannot be converted to JButton
670        // add extras, override BeanTableDataModel
671        log.debug("Turnout configValueColumn (I am {})", super.toString());
672        if (_graphicState) { // load icons, only once
673            table.setDefaultEditor(JLabel.class, new ImageIconRenderer()); // editor
674            table.setDefaultRenderer(JLabel.class, new ImageIconRenderer()); // item class copied from SwitchboardEditor panel
675        } else {
676            super.configValueColumn(table); // classic text style state indication
677        }
678    }
679
680    @Override
681    public JTable makeJTable(@Nonnull String name, @Nonnull TableModel model, @CheckForNull RowSorter<? extends TableModel> sorter) {
682        if (!(model instanceof TurnoutTableDataModel)){
683            throw new IllegalArgumentException("Model is not a TurnoutTableDataModel");
684        }
685        return configureJTable(name, new TurnoutTableJTable((TurnoutTableDataModel)model), sorter);
686    }
687
688    @Override
689    protected void setColumnIdentities(JTable table) {
690        super.setColumnIdentities(table);
691        java.util.Enumeration<TableColumn> columns;
692        if (table.getColumnModel() instanceof XTableColumnModel) {
693            columns = ((XTableColumnModel) table.getColumnModel()).getColumns(false);
694        } else {
695            columns = table.getColumnModel().getColumns();
696        }
697        while (columns.hasMoreElements()) {
698            TableColumn column = columns.nextElement();
699            switch (column.getModelIndex()) {
700                case FORGETCOL:
701                    column.setIdentifier("ForgetState");
702                    break;
703                case QUERYCOL:
704                    column.setIdentifier("QueryState");
705                    break;
706                case SENSOR1COL:
707                    column.setIdentifier("Sensor1");
708                    break;
709                case SENSOR2COL:
710                    column.setIdentifier("Sensor2");
711                    break;
712                default:
713                // use existing value
714            }
715        }
716    }
717
718    /**
719     * Pop up a TurnoutOperationConfig for the turnout.
720     *
721     * @param t   turnout
722     * @param box JComboBox that triggered the edit
723     */
724    protected void editTurnoutOperation(Turnout t, JComboBox<String> box) {
725        if (!editingOps.getAndSet(true)) { // don't open a second edit ops pane
726            TurnoutOperation op = t.getTurnoutOperation();
727            if (op == null) {
728                TurnoutOperation proto = InstanceManager.getDefault(TurnoutOperationManager.class).getMatchingOperationAlways(t);
729                if (proto != null) {
730                    op = proto.makeNonce(t);
731                    t.setTurnoutOperation(op);
732                }
733            }
734            if (op != null) {
735                if (!op.isNonce()) {
736                    op = op.makeNonce(t);
737                }
738                // make and show edit dialog
739                log.debug("TurnoutOpsEditDialog starting");
740                TurnoutOperationEditorDialog dialog = new TurnoutOperationEditorDialog(op, t, box);
741                dialog.setVisible(true);
742            } else {
743                JmriJOptionPane.showMessageDialog(box, Bundle.getMessage("TurnoutOperationErrorDialog"),
744                        Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
745            }
746        }
747    }
748
749    /**
750     * Create a {@literal JComboBox<String>} containing all the options for
751     * turnout automation parameters for this turnout.
752     *
753     * @param t the turnout
754     * @return the JComboBox
755     */
756    protected JComboBox<String> makeAutomationBox(Turnout t) {
757        String[] str = new String[]{"empty"};
758        final JComboBox<String> cb = new JComboBox<>(str);
759        final Turnout myTurnout = t;
760        TurnoutTableAction.updateAutomationBox(t, cb);
761        cb.addActionListener(new ActionListener() {
762            @Override
763            public void actionPerformed(ActionEvent e) {
764                setTurnoutOperation(myTurnout, cb);
765                cb.removeActionListener(this);  // avoid recursion
766                TurnoutTableAction.updateAutomationBox(myTurnout, cb);
767                cb.addActionListener(this);
768            }
769        });
770        return cb;
771    }
772
773    /**
774     * Set the turnout's operation info based on the contents of the combo box.
775     *
776     * @param t  turnout being configured
777     * @param cb JComboBox for ops for t in the TurnoutTable
778     */
779    protected void setTurnoutOperation(Turnout t, JComboBox<String> cb) {
780        switch (cb.getSelectedIndex()) {
781            case 0:   // Off
782                t.setInhibitOperation(true);
783                t.setTurnoutOperation(null);
784                break;
785            case 1:   // Default
786                t.setInhibitOperation(false);
787                t.setTurnoutOperation(null);
788                break;
789            default:  // named operation
790                t.setInhibitOperation(false);
791                t.setTurnoutOperation(InstanceManager.getDefault(TurnoutOperationManager.class).
792                        getOperation(((String) java.util.Objects.requireNonNull(cb.getSelectedItem()))));
793                break;
794        }
795    }
796
797    /**
798     * Create action to edit a turnout in Edit pane. (also used in windowTest)
799     *
800     * @param t the turnout to be edited
801     */
802    void editButton(Turnout t) {
803        jmri.jmrit.beantable.beanedit.TurnoutEditAction beanEdit = new jmri.jmrit.beantable.beanedit.TurnoutEditAction();
804        beanEdit.setBean(t);
805        beanEdit.actionPerformed(null);
806    }
807
808    /**
809     * Create a JButton to edit a turnout's operation.
810     *
811     * @return the JButton
812     */
813    protected JButton editButton() {
814        return new JButton(Bundle.getMessage("EditTurnoutOperation"));
815    }
816
817    private void updateClosedList() {
818        speedListClosed.remove(defaultClosedSpeedText);
819        defaultClosedSpeedText = (Bundle.getMessage("UseGlobal", "Global") + " " + turnoutManager.getDefaultClosedSpeed());
820        speedListClosed.add(0, defaultClosedSpeedText);
821        fireTableDataChanged();
822    }
823
824    private void updateThrownList() {
825        speedListThrown.remove(defaultThrownSpeedText);
826        defaultThrownSpeedText = (Bundle.getMessage("UseGlobal", "Global") + " " + turnoutManager.getDefaultThrownSpeed());
827        speedListThrown.add(0, defaultThrownSpeedText);
828        fireTableDataChanged();
829    }
830
831    public void showFeedbackChanged(boolean visible, JTable table ) {
832        XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel();
833        TableColumn column = columnModel.getColumnByModelIndex(KNOWNCOL);
834        columnModel.setColumnVisible(column, visible);
835        column = columnModel.getColumnByModelIndex(MODECOL);
836        columnModel.setColumnVisible(column, visible);
837        column = columnModel.getColumnByModelIndex(SENSOR1COL);
838        columnModel.setColumnVisible(column, visible);
839        column = columnModel.getColumnByModelIndex(SENSOR2COL);
840        columnModel.setColumnVisible(column, visible);
841        column = columnModel.getColumnByModelIndex(OPSONOFFCOL);
842        columnModel.setColumnVisible(column, visible);
843        column = columnModel.getColumnByModelIndex(OPSEDITCOL);
844        columnModel.setColumnVisible(column, visible);
845    }
846
847    public void showLockChanged(boolean visible, JTable table) {
848        XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel();
849        TableColumn column = ((XTableColumnModel) table.getColumnModel()).getColumnByModelIndex(LOCKDECCOL);
850        columnModel.setColumnVisible(column, visible);
851        column = columnModel.getColumnByModelIndex(LOCKOPRCOL);
852        columnModel.setColumnVisible(column, visible);
853    }
854
855    public void showTurnoutSpeedChanged(boolean visible, JTable table) {
856        XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel();
857        TableColumn column = ((XTableColumnModel) table.getColumnModel()).getColumnByModelIndex(STRAIGHTCOL);
858        columnModel.setColumnVisible(column, visible);
859        column = columnModel.getColumnByModelIndex(DIVERGCOL);
860        columnModel.setColumnVisible(column, visible);
861    }
862
863    public void showStateForgetAndQueryChanged(boolean visible, JTable table) {
864        XTableColumnModel columnModel = (XTableColumnModel) table.getColumnModel();
865        TableColumn column = columnModel.getColumnByModelIndex(FORGETCOL);
866        columnModel.setColumnVisible(column, visible);
867        column = columnModel.getColumnByModelIndex(QUERYCOL);
868        columnModel.setColumnVisible(column, visible);
869    }
870
871
872    /**
873     * Visualize state in table as a graphic, customized for Turnouts (4
874     * states).
875     * Renderer and Editor are identical, as the cell contents
876     * are not actually edited, only used to toggle state using
877     * {@link #clickOn(Turnout)}.
878     *
879     */
880    class ImageIconRenderer extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
881
882        protected JLabel label;
883        protected String rootPath = "resources/icons/misc/switchboard/"; // also used in display.switchboardEditor
884        protected char beanTypeChar = 'T'; // for Turnout
885        protected String onIconPath = rootPath + beanTypeChar + "-on-s.png";
886        protected String offIconPath = rootPath + beanTypeChar + "-off-s.png";
887        protected BufferedImage onImage;
888        protected BufferedImage offImage;
889        protected ImageIcon onIcon;
890        protected ImageIcon offIcon;
891        protected int iconHeight = -1;
892
893        @Override
894        public java.awt.Component getTableCellRendererComponent(
895                JTable table, Object value, boolean isSelected,
896                boolean hasFocus, int row, int column) {
897            log.debug("Renderer Item = {}, State = {}", row, value);
898            if (iconHeight < 0) { // load resources only first time, either for renderer or editor
899                loadIcons();
900                log.debug("icons loaded");
901            }
902            return updateLabel((String) value, row, table);
903        }
904
905        @Override
906        public java.awt.Component getTableCellEditorComponent(
907                JTable table, Object value, boolean isSelected,
908                int row, int column) {
909            log.debug("Renderer Item = {}, State = {}", row, value);
910            if (iconHeight < 0) { // load resources only first time, either for renderer or editor
911                loadIcons();
912                log.debug("icons loaded");
913            }
914            return updateLabel((String) value, row, table);
915        }
916
917        public JLabel updateLabel(String value, int row, JTable table) {
918            if (iconHeight > 0) { // if necessary, increase row height;
919                table.setRowHeight(row, Math.max(table.getRowHeight(), iconHeight - 5));
920            }
921            if (value.equals(closedText) && onIcon != null) {
922                label = new JLabel(onIcon);
923                label.setVerticalAlignment(JLabel.BOTTOM);
924                log.debug("onIcon set");
925            } else if (value.equals(thrownText) && offIcon != null) {
926                label = new JLabel(offIcon);
927                label.setVerticalAlignment(JLabel.BOTTOM);
928                log.debug("offIcon set");
929            } else if (value.equals(Bundle.getMessage("BeanStateInconsistent"))) {
930                label = new JLabel("X", JLabel.CENTER); // centered text alignment
931                label.setForeground(java.awt.Color.red);
932                log.debug("Turnout state inconsistent");
933                iconHeight = 0;
934            } else if (value.equals(Bundle.getMessage("BeanStateUnknown"))) {
935                label = new JLabel("?", JLabel.CENTER); // centered text alignment
936                log.debug("Turnout state unknown");
937                iconHeight = 0;
938            } else { // failed to load icon
939                label = new JLabel(value, JLabel.CENTER); // centered text alignment
940                log.warn("Error reading icons for TurnoutTable");
941                iconHeight = 0;
942            }
943            label.setToolTipText(value);
944            label.addMouseListener(new MouseAdapter() {
945                @Override
946                public final void mousePressed(MouseEvent evt) {
947                    log.debug("Clicked on icon in row {}", row);
948                    stopCellEditing();
949                }
950            });
951            return label;
952        }
953
954        @Override
955        public Object getCellEditorValue() {
956            log.debug("getCellEditorValue, me = {})", this.toString());
957            return this.toString();
958        }
959
960        /**
961         * Read and buffer graphics. Only called once for this table.
962         *
963         * @see #getTableCellEditorComponent(JTable, Object, boolean,
964         * int, int)
965         */
966        protected void loadIcons() {
967            try {
968                onImage = ImageIO.read(new File(onIconPath));
969                offImage = ImageIO.read(new File(offIconPath));
970            } catch (IOException ex) {
971                log.error("error reading image from {} or {}", onIconPath, offIconPath, ex);
972            }
973            log.debug("Success reading images");
974            int imageWidth = onImage.getWidth();
975            int imageHeight = onImage.getHeight();
976            // scale icons 50% to fit in table rows
977            java.awt.Image smallOnImage = onImage.getScaledInstance(imageWidth / 2, imageHeight / 2, java.awt.Image.SCALE_DEFAULT);
978            java.awt.Image smallOffImage = offImage.getScaledInstance(imageWidth / 2, imageHeight / 2, java.awt.Image.SCALE_DEFAULT);
979            onIcon = new ImageIcon(smallOnImage);
980            offIcon = new ImageIcon(smallOffImage);
981            iconHeight = onIcon.getIconHeight();
982        }
983
984    } // end of ImageIconRenderer class
985
986    protected static java.util.concurrent.atomic.AtomicBoolean editingOps = new java.util.concurrent.atomic.AtomicBoolean(false);
987
988    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TurnoutTableDataModel.class);
989
990}