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