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