001package jmri.jmrix.dccpp.swing;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.awt.Component;
005import java.awt.Dimension;
006import java.awt.FlowLayout;
007import java.awt.GridBagLayout;
008import java.awt.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.util.ArrayList;
011import java.util.HashMap;
012import java.util.List;
013import java.util.Map;
014import javax.swing.BoxLayout;
015import javax.swing.DefaultCellEditor;
016import javax.swing.JButton;
017import javax.swing.JCheckBox;
018import javax.swing.JMenu;
019import javax.swing.JMenuBar;
020import javax.swing.JOptionPane;
021import javax.swing.JPanel;
022import javax.swing.JScrollPane;
023import javax.swing.JTabbedPane;
024import javax.swing.JTable;
025import javax.swing.RowSorter;
026import javax.swing.SortOrder;
027import javax.swing.UIManager;
028import javax.swing.event.ChangeEvent;
029import javax.swing.event.EventListenerList;
030import javax.swing.table.TableCellRenderer;
031import javax.swing.table.TableModel;
032import javax.swing.table.TableRowSorter;
033import jmri.jmrix.dccpp.DCCppListener;
034import jmri.jmrix.dccpp.DCCppMessage;
035import jmri.jmrix.dccpp.DCCppReply;
036import jmri.jmrix.dccpp.DCCppSensorManager;
037import jmri.jmrix.dccpp.DCCppTrafficController;
038import jmri.jmrix.dccpp.DCCppTurnoutManager;
039import jmri.util.JmriJFrame;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043/*
044 * <hr>
045 * This file is part of JMRI.
046 * <p>
047 * JMRI is free software; you can redistribute it and/or modify it under
048 * the terms of version 2 of the GNU General Public License as published
049 * by the Free Software Foundation. See the "COPYING" file for a copy
050 * of this license.
051 * <p>
052 * JMRI is distributed in the hope that it will be useful, but WITHOUT
053 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
054 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
055 * for more details.
056 *
057 * @author   Mark Underwood Copyright (C) 2011
058 */
059public class ConfigBaseStationFrame extends JmriJFrame implements DCCppListener {
060
061    // Map of Mnemonic KeyEvent values to GUI Components
062    private static final Map<String, Integer> Mnemonics = new HashMap<>();
063
064    static {
065        Mnemonics.put("SensorTab", KeyEvent.VK_E); // NOI18N
066        Mnemonics.put("TurnoutTab", KeyEvent.VK_T); // NOI18N
067        Mnemonics.put("OutputTab", KeyEvent.VK_O); // NOI18N
068        Mnemonics.put("ButtonAdd", KeyEvent.VK_A); // NOI18N
069        Mnemonics.put("CloseButton", KeyEvent.VK_O); // NOI18N
070        Mnemonics.put("SaveButton", KeyEvent.VK_S); // NOI18N
071    }
072
073    protected EventListenerList listenerList = new javax.swing.event.EventListenerList();
074
075    private final DCCppTrafficController tc;
076
077    private JTabbedPane tabbedPane;
078    private JPanel sensorPanel;
079
080    private SensorTableModel sensorModel;
081    private TurnoutTableModel turnoutModel;
082    private OutputTableModel outputModel;
083    private JTable sensorTable;
084    private JTable turnoutTable;
085    private JTable outputTable;
086    private TableRowSorter<TableModel> sensorSorter;
087    private TableRowSorter<TableModel> turnoutSorter;
088    private TableRowSorter<TableModel> outputSorter;
089
090    private List<JMenu> menuList;
091
092    private enum CurrentTab {
093        SENSOR, TURNOUT, OUTPUT
094    }
095    private CurrentTab cTab;
096
097    @SuppressFBWarnings(value = "EI_EXPOSE_REP2",
098            justification = "2D array of different types passed as complex parameter. "
099            + "Better to switch to passing use-specific objects rather than "
100            + "papering this over with a deep copy of the arguments. "
101            + "In any case, there's no risk of exposure here.")
102    public ConfigBaseStationFrame(DCCppSensorManager sm,
103            DCCppTurnoutManager tm,
104            DCCppTrafficController t) {
105        super(false, false);
106        tc = t;
107        initGui();
108    }
109
110    private void initGui() {
111
112        // NOTE: Look at jmri.jmrit.vsdecoder.swing.ManageLocationsFrame
113        // for how to add a tab for turnouts and other things.
114        this.setTitle(Bundle.getMessage("FieldManageBaseStationFrameTitle"));
115        this.buildMenu();
116
117        // Panel for managing sensors
118        sensorPanel = new JPanel();
119        sensorPanel.setLayout(new GridBagLayout());
120
121        JButton addButton = new JButton(Bundle.getMessage("ButtonAddX", Bundle.getMessage("BeanNameSensor")));
122        addButton.setToolTipText(Bundle.getMessage("ToolTipButtonMSFAdd"));
123        addButton.setMnemonic(Mnemonics.get("ButtonAdd")); // NOI18N
124        addButton.addActionListener((ActionEvent e) -> {
125            addButtonPressed(e);
126        });
127
128        JButton closeButton = new JButton(Bundle.getMessage("ButtonClose"));
129        closeButton.setToolTipText(Bundle.getMessage("ToolTipButtonClose"));
130        closeButton.setMnemonic(Mnemonics.get("CloseButton")); // NOI18N
131        closeButton.addActionListener((ActionEvent e) -> {
132            closeButtonPressed(e);
133        });
134        JButton saveButton = new JButton(Bundle.getMessage("ButtonSaveX", Bundle.getMessage("Sensors")));
135        saveButton.setToolTipText(Bundle.getMessage("ToolTipButtonMSFSave"));
136        saveButton.setMnemonic(Mnemonics.get("SaveButton")); // NOI18N
137        saveButton.addActionListener((ActionEvent e) -> {
138            saveButtonPressed(e);
139        });
140
141        JScrollPane sensorScrollPanel = new JScrollPane();
142        sensorModel = new SensorTableModel();
143        sensorTable = new JTable(sensorModel);
144        sensorTable.setFillsViewportHeight(true);
145        sensorScrollPanel.getViewport().add(sensorTable);
146        sensorTable.setPreferredScrollableViewportSize(new Dimension(520, 200));
147        sensorTable.getColumn(Bundle.getMessage("ColumnDelete")).setCellRenderer(new ButtonRenderer());
148        sensorTable.removeColumn(sensorTable.getColumn("isNew"));
149        sensorTable.removeColumn(sensorTable.getColumn("isDirty"));
150        sensorTable.removeColumn(sensorTable.getColumn("isDelete"));
151        sensorTable.addMouseListener(new java.awt.event.MouseAdapter() {
152            @Override
153            public void mouseClicked(java.awt.event.MouseEvent evt) {
154                handleTableMouseClick(sensorTable, evt);
155            }
156        });
157        sensorTable.setAutoCreateRowSorter(true);
158        sensorSorter = new TableRowSorter<>(sensorTable.getModel());
159        sensorTable.setRowSorter(sensorSorter);
160        List<RowSorter.SortKey> sensorSortKeys = new ArrayList<>();
161        sensorSortKeys.add(new RowSorter.SortKey(sensorTable.getColumn(Bundle.getMessage("FieldTableIndexColumn")).getModelIndex(), SortOrder.ASCENDING));
162        sensorSorter.setSortKeys(sensorSortKeys);
163        sensorSorter.sort();
164        sensorSorter.setSortable(sensorTable.getColumn(Bundle.getMessage("ColumnDelete")).getModelIndex(), false);
165
166        JScrollPane turnoutScrollPanel = new JScrollPane();
167        turnoutModel = new TurnoutTableModel();
168        turnoutTable = new JTable(turnoutModel);
169        turnoutTable.setFillsViewportHeight(true);
170        turnoutScrollPanel.getViewport().add(turnoutTable);
171        turnoutTable.setPreferredScrollableViewportSize(new Dimension(520, 200));
172        turnoutTable.getColumn(Bundle.getMessage("ColumnDelete")).setCellRenderer(new ButtonRenderer());
173        turnoutTable.removeColumn(turnoutTable.getColumn("isNew"));
174        turnoutTable.removeColumn(turnoutTable.getColumn("isDirty"));
175        turnoutTable.removeColumn(turnoutTable.getColumn("isDelete"));
176        turnoutTable.addMouseListener(new java.awt.event.MouseAdapter() {
177            @Override
178            public void mouseClicked(java.awt.event.MouseEvent evt) {
179                handleTableMouseClick(turnoutTable, evt);
180            }
181        });
182        turnoutTable.setAutoCreateRowSorter(true);
183        turnoutSorter = new TableRowSorter<>(turnoutTable.getModel());
184        turnoutTable.setRowSorter(turnoutSorter);
185        List<RowSorter.SortKey> turnoutSortKeys = new ArrayList<>();
186        turnoutSortKeys.add(new RowSorter.SortKey(sensorTable.getColumn(Bundle.getMessage("FieldTableIndexColumn")).getModelIndex(), SortOrder.ASCENDING));
187        turnoutSorter.setSortKeys(turnoutSortKeys);
188        turnoutSorter.setSortable(sensorTable.getColumn(Bundle.getMessage("ColumnDelete")).getModelIndex(), false);
189        turnoutSorter.sort();
190
191        JScrollPane outputScrollPanel = new JScrollPane();
192        outputModel = new OutputTableModel();
193        outputTable = new JTable(outputModel);
194        outputTable.setFillsViewportHeight(true);
195        outputScrollPanel.getViewport().add(outputTable);
196        outputTable.setPreferredScrollableViewportSize(new Dimension(520, 200));
197        outputTable.getColumn(Bundle.getMessage("ColumnDelete")).setCellRenderer(new ButtonRenderer());
198        outputTable.removeColumn(outputTable.getColumn("isNew"));
199        outputTable.removeColumn(outputTable.getColumn("isDirty"));
200        outputTable.removeColumn(outputTable.getColumn("isDelete"));
201        outputTable.addMouseListener(new java.awt.event.MouseAdapter() {
202            @Override
203            public void mouseClicked(java.awt.event.MouseEvent evt) {
204                handleTableMouseClick(outputTable, evt);
205            }
206        });
207        outputTable.setAutoCreateRowSorter(true);
208        outputSorter = new TableRowSorter<>(outputTable.getModel());
209        outputTable.setRowSorter(outputSorter);
210        List<RowSorter.SortKey> outputSortKeys = new ArrayList<>();
211        outputSortKeys.add(new RowSorter.SortKey(sensorTable.getColumn(Bundle.getMessage("FieldTableIndexColumn")).getModelIndex(), SortOrder.ASCENDING));
212        outputSorter.setSortKeys(outputSortKeys);
213        outputSorter.setSortable(sensorTable.getColumn(Bundle.getMessage("ColumnDelete")).getModelIndex(), false);
214        outputSorter.sort();
215
216        tabbedPane = new JTabbedPane();
217        tabbedPane.addTab(Bundle.getMessage("Sensors"), sensorScrollPanel);
218        tabbedPane.setToolTipTextAt(0, Bundle.getMessage("ToolTipSensorTab"));
219        tabbedPane.setMnemonicAt(0, Mnemonics.get("SensorTab")); // NOI18N
220        tabbedPane.addTab(Bundle.getMessage("Turnouts"), turnoutScrollPanel);
221        tabbedPane.setToolTipTextAt(0, Bundle.getMessage("ToolTipTurnoutTab"));
222        tabbedPane.setMnemonicAt(0, Mnemonics.get("TurnoutTab")); // NOI18N
223        tabbedPane.addTab(Bundle.getMessage("FieldOutputsTabTitle"), outputScrollPanel);
224        tabbedPane.setToolTipTextAt(0, Bundle.getMessage("ToolTipOutputTab"));
225        tabbedPane.setMnemonicAt(0, Mnemonics.get("OutputTab")); // NOI18N
226        cTab = CurrentTab.SENSOR;
227        tabbedPane.setSelectedIndex(0);
228        tabbedPane.addChangeListener((ChangeEvent e) -> {
229            switch (tabbedPane.getSelectedIndex()) {
230                case 2:
231                    // Set Add to "Add Output"
232                    cTab = CurrentTab.OUTPUT;
233                    addButton.setText(Bundle.getMessage("ButtonAddX", Bundle.getMessage("Output")));
234                    saveButton.setText(Bundle.getMessage("ButtonSaveX", Bundle.getMessage("FieldOutputsTabTitle")));
235                    log.debug("Current Tab is: {}", tabbedPane.getSelectedIndex());
236                    break;
237                case 1:
238                    // Set Add to "Add Turnout"
239                    cTab = CurrentTab.TURNOUT;
240                    addButton.setText(Bundle.getMessage("ButtonAddX", Bundle.getMessage("BeanNameTurnout")));
241                    saveButton.setText(Bundle.getMessage("ButtonSaveX", Bundle.getMessage("Turnouts")));
242                    log.debug("Current Tab is: {}", tabbedPane.getSelectedIndex());
243                    break;
244                case 0:
245                default:
246                    // Set Add to "Add Sensor"
247                    cTab = CurrentTab.SENSOR;
248                    addButton.setText(Bundle.getMessage("ButtonAddX", Bundle.getMessage("BeanNameSensor")));
249                    saveButton.setText(Bundle.getMessage("ButtonSaveX", Bundle.getMessage("Sensors")));
250                    log.debug("Current Tab is: {}", tabbedPane.getSelectedIndex());
251            }
252        });
253
254        JPanel buttonPane = new JPanel(new FlowLayout(FlowLayout.LEFT));
255        JPanel buttonPane2 = new JPanel(new FlowLayout(FlowLayout.RIGHT));
256
257        buttonPane.add(addButton);
258        buttonPane.add(saveButton);
259        buttonPane2.add(closeButton);
260
261        this.getContentPane().setLayout(new BoxLayout(this.getContentPane(), BoxLayout.Y_AXIS));
262        this.getContentPane().add(tabbedPane);
263        this.getContentPane().add(buttonPane);
264        this.getContentPane().add(buttonPane2);
265        this.pack();
266        this.setVisible(true);
267    }
268
269    private void buildMenu() {
270        JMenu fileMenu = new JMenu(Bundle.getMessage("MenuFile"));
271
272        JMenu editMenu = new JMenu(Bundle.getMessage("MenuEdit"));
273        menuList = new ArrayList<>(3);
274
275        menuList.add(fileMenu);
276        menuList.add(editMenu);
277
278        this.setJMenuBar(new JMenuBar());
279        this.getJMenuBar().add(fileMenu);
280        this.getJMenuBar().add(editMenu);
281        //this.addHelpMenu("package.jmri.jmrit.vsdecoder.swing.ManageLocationsFrame", true); // NOI18N
282    }
283
284    // DCCppListener Methods
285    @Override
286    public void message(DCCppReply r) {
287        // When we get a SensorDefReply message, add the
288        // sensor information to the data map for the model.
289        if (r.isSensorDefReply()) {
290            List<Object> v = new ArrayList<>();
291            v.add(r.getSensorDefNumInt());
292            v.add(r.getSensorDefPinInt());
293            v.add(r.getSensorDefPullupBool());
294            sensorModel.insertData(v, false);
295            sensorSorter.sort();
296        } else if (r.isTurnoutDefReply()) {
297            List<Object> v = new ArrayList<>();
298            v.add(r.getTurnoutDefNumInt());
299            v.add(r.getTurnoutDefAddrInt());
300            v.add(r.getTurnoutDefSubAddrInt());
301            turnoutModel.insertData(v, false);
302            turnoutSorter.sort();
303        } else if (r.isOutputListReply()) {
304            List<Object> v = new ArrayList<>();
305            v.add(r.getOutputNumInt());
306            v.add(r.getOutputListPinInt());
307            v.add((r.getOutputListIFlagInt() & 0x01) == 1); // (bool) Invert
308            v.add((r.getOutputListIFlagInt() & 0x02) == 2); // (bool) Restore State
309            v.add((r.getOutputListIFlagInt() & 0x04) == 4); // (bool) Force High
310            outputModel.insertData(v, false);
311            outputSorter.sort();
312        }
313    }
314
315    @Override
316    public void message(DCCppMessage m) {
317        // Do nothing
318    }
319
320    @Override
321    public void notifyTimeout(DCCppMessage m) {
322        // Do nothing
323    }
324
325    /**
326     * Handle mouse clicks within a table.
327     * <p>
328     * This is currently the workings behind the "Delete" button in the table.
329     *
330     * @param table the table where the event occurred
331     * @param evt   the mouse click
332     */
333    private void handleTableMouseClick(JTable table, java.awt.event.MouseEvent evt) {
334        int row = table.rowAtPoint(evt.getPoint());
335        int col = table.columnAtPoint(evt.getPoint());
336        if (row < 0 || col < 0) {
337            return;
338        }
339        DCCppTableModel model = (DCCppTableModel) table.getModel();
340        if (col == table.convertColumnIndexToView(model.getDeleteColumn())) {
341            // This is a row delete action.  Handle it as such.
342            int sel = table.convertRowIndexToModel(row);
343            int idx = (int) model.getValueAt(sel, 0);
344            log.debug("idx = {}", sel);
345            int value = JOptionPane.showConfirmDialog(null, Bundle.getMessage("DeleteWarningMessage", Integer.toString(idx)),
346                    Bundle.getMessage("WarningTitle"),
347                    JOptionPane.OK_CANCEL_OPTION);
348            if (value == JOptionPane.OK_OPTION) {
349               if (null != cTab) {
350                    switch (cTab) {
351                        case SENSOR:
352                            tc.sendDCCppMessage(DCCppMessage.makeSensorDeleteMsg(idx), this);
353                            sensorModel.removeRow(sel);
354                            log.debug("Delete sensor {}", idx);
355                            break;
356                        case TURNOUT:
357                            DCCppMessage m = new DCCppMessage("T " + Integer.toString(idx));
358                            tc.sendDCCppMessage(m, this);
359                            log.debug("Sending: {}", m);
360                            turnoutModel.removeRow(sel);
361                            break;
362                        case OUTPUT:
363                            tc.sendDCCppMessage(DCCppMessage.makeOutputDeleteMsg(idx), this);
364                            outputModel.removeRow(sel);
365                            break;
366                        default:
367                            jmri.util.LoggingUtil.warnOnce(log, "Unexpected cTab value = {}", cTab);
368                            break;
369                   }
370                }
371            }
372
373        }
374    }
375
376    /**
377     * Responder for pressing the "Add" button. Response depends on which tab is
378     * active.
379     *
380     * @param e the press event
381     */
382    private void addButtonPressed(ActionEvent e) {
383        if (null != cTab) {
384            switch (cTab) {
385                case SENSOR: {
386                    List<Object> v = new ArrayList<>();
387                    v.add(0);     // Index
388                    v.add(0);     // Pin
389                    v.add(false); // Pullup
390                    sensorModel.insertData(v, true);
391                    break;
392                }
393                case TURNOUT: {
394                    List<Object> v = new ArrayList<>();
395                    v.add(0); // Index
396                    v.add(0); // Address
397                    v.add(0); // Subaddress
398                    turnoutModel.insertData(v, true);
399                    break;
400                }
401                case OUTPUT: {
402                    List<Object> v = new ArrayList<>();
403                    v.add(0); // Index
404                    v.add(0); // Pin
405                    v.add(false); // Invert
406                    v.add(false); // Restore state
407                    v.add(false); // Force high/low
408                    outputModel.insertData(v, true);
409                    break;
410                }
411                default:
412                    break;
413            }
414        }
415    }
416
417    /**
418     * Respond to the user pressing the "Save Sensors/Turnouts/Outputs" button.
419     *
420     * @param e the button press event
421     */
422    private void saveButtonPressed(ActionEvent e) {
423        int value = JOptionPane.showConfirmDialog(null, Bundle.getMessage("FieldMCFSaveDialogConfirmMessage"),
424                Bundle.getMessage("ConfirmSaveDialogTitle"),
425                JOptionPane.YES_NO_OPTION);
426        if (sensorTable.getCellEditor() != null) {
427            sensorTable.getCellEditor().stopCellEditing();
428        }
429        if (turnoutTable.getCellEditor() != null) {
430            turnoutTable.getCellEditor().stopCellEditing();
431        }
432        if (outputTable.getCellEditor() != null) {
433            outputTable.getCellEditor().stopCellEditing();
434        }
435        if (value == JOptionPane.YES_OPTION) {
436            saveTableValues();
437        }
438    }
439
440    /**
441     * Save the values for the currently selected tab.
442     */
443    private void saveTableValues() {
444        if (null != cTab) {
445            switch (cTab) {
446                case SENSOR:
447                    for (int i = 0; i < sensorModel.getRowData().size(); i++) {
448
449                        List<Object> r = sensorModel.getRowData().get(i);
450                        boolean isnew = (boolean) r.get(4);
451                        boolean isdirty = (boolean) r.get(5);
452                        boolean isdelete = (boolean) r.get(6);
453                        int row = sensorModel.getRowData().indexOf(r);
454                        if (isnew) {
455                            tc.sendDCCppMessage(DCCppMessage.makeSensorAddMsg((int) r.get(0),
456                                    (int) r.get(1),
457                                    ((boolean) r.get(2) ? 1 : 0)), this);
458                            sensorModel.setNewRow(row, false);
459                        } else if (isdelete) {
460                            tc.sendDCCppMessage(DCCppMessage.makeSensorDeleteMsg((int) r.get(0)), this);
461                            sensorModel.getRowData().remove(r);
462                        } else if (isdirty) {
463                            // Send a Delete, then an Add (for now).
464                            tc.sendDCCppMessage(DCCppMessage.makeSensorDeleteMsg((int) r.get(0)), this);
465                            // WARNING: Conversions here are brittle. Be careful.
466                            tc.sendDCCppMessage(DCCppMessage.makeSensorAddMsg((int) r.get(0),
467                                    (int) r.get(1),
468                                    ((boolean) r.get(2) ? 1 : 0)), this);
469                            sensorModel.setNewRow(row, false);
470                            sensorModel.setDirtyRow(row, false);
471                        }
472                    }
473                    break;
474                case TURNOUT:
475                    for (int i = 0; i < turnoutModel.getRowData().size(); i++) {
476
477                        List<Object> r = turnoutModel.getRowData().get(i);
478                        boolean isnew = (boolean) r.get(4);
479                        boolean isdirty = (boolean) r.get(5);
480                        boolean isdelete = (boolean) r.get(6);
481                        int row = turnoutModel.getRowData().indexOf(r);
482                        if (isnew) {
483                            // WARNING: Conversions here are brittle. Be careful.
484                            tc.sendDCCppMessage(DCCppMessage.makeTurnoutAddMsg((int) r.get(0),
485                                    (int) r.get(1), (int) r.get(2)), this);
486                            turnoutModel.setNewRow(row, false);
487                        } else if (isdelete) {
488                            DCCppMessage m = new DCCppMessage("T " + Integer.toString((int) r.get(0)));
489                            tc.sendDCCppMessage(m, this);
490                            log.debug("Sending: {}", m);
491                            turnoutModel.getRowData().remove(r);
492                        } else if (isdirty) {
493                            tc.sendDCCppMessage(DCCppMessage.makeTurnoutDeleteMsg((int) r.get(0)), this);
494                            // Send a Delete, then an Add (for now).
495                            // WARNING: Conversions here are brittle. Be careful.
496                            tc.sendDCCppMessage(DCCppMessage.makeTurnoutAddMsg((int) r.get(0),
497                                    (int) r.get(1), (int) r.get(2)), this);
498                            turnoutModel.setNewRow(row, false);
499                            turnoutModel.setDirtyRow(row, false);
500                        }
501                    }
502                    break;
503                case OUTPUT:
504                    for (int i = 0; i < outputModel.getRowData().size(); i++) {
505
506                        List<Object> r = outputModel.getRowData().get(i);
507                        boolean isnew = (boolean) r.get(6);
508                        boolean isdirty = (boolean) r.get(7);
509                        boolean isdelete = (boolean) r.get(8);
510                        int row = outputModel.getRowData().indexOf(r);
511                        if (isnew) {
512                            // WARNING: Conversions here are brittle. Be careful.
513                            int f = ((boolean) r.get(2) ? 1 : 0); // Invert
514                            f += ((boolean) r.get(3) ? 2 : 0); // Restore
515                            f += ((boolean) r.get(4) ? 4 : 0); // Force
516                            tc.sendDCCppMessage(DCCppMessage.makeOutputAddMsg((int) r.get(0),
517                                    (int) r.get(1), f), this);
518                            outputModel.setNewRow(row, false);
519                        } else if (isdelete) {
520                            tc.sendDCCppMessage(DCCppMessage.makeOutputDeleteMsg((int) r.get(0)), this);
521                            outputModel.getRowData().remove(r);
522                        } else if (isdirty) {
523                            // Send a Delete, then an Add (for now).
524                            tc.sendDCCppMessage(DCCppMessage.makeOutputDeleteMsg((int) r.get(0)), this);
525                            int f = ((boolean) r.get(2) ? 1 : 0); // Invert
526                            f += ((boolean) r.get(3) ? 2 : 0); // Restore
527                            f += ((boolean) r.get(4) ? 4 : 0); // Force
528                            tc.sendDCCppMessage(DCCppMessage.makeOutputAddMsg((int) r.get(0),
529                                    (int) r.get(1), f), this);
530                            outputModel.setNewRow(row, false);
531                            outputModel.setDirtyRow(row, false);
532                        }
533                    }
534                    break;
535                default:
536                    break;
537            }
538        }
539
540        // Offer to write the changes to EEPROM
541        int value = JOptionPane.showConfirmDialog(null, Bundle.getMessage("FieldMCFCloseDialogConfirmMessage"),
542                Bundle.getMessage("FieldMCFCloseDialogTitle"),
543                JOptionPane.YES_NO_OPTION);
544
545        if (value == JOptionPane.YES_OPTION) {
546            tc.sendDCCppMessage(new DCCppMessage("E"), this);
547            log.debug("Sending: <E> (Write To EEPROM)");
548            // These might not actually be necessary...
549            sensorModel.fireTableDataChanged();
550            turnoutModel.fireTableDataChanged();
551            outputModel.fireTableDataChanged();
552        }
553    }
554
555    /**
556     * Respond to the user pressing the "Close" button.
557     *
558     * @param e the button press event
559     */
560    private void closeButtonPressed(ActionEvent e) {
561        // If clicked while editing, stop the cell editor(s)
562        if (sensorTable.getCellEditor() != null) {
563            sensorTable.getCellEditor().stopCellEditing();
564        }
565        if (turnoutTable.getCellEditor() != null) {
566            turnoutTable.getCellEditor().stopCellEditing();
567        }
568        if (outputTable.getCellEditor() != null) {
569            outputTable.getCellEditor().stopCellEditing();
570        }
571
572        // If clicked while changes not saved to BaseStation, offer
573        // the option of saving.
574        if (sensorModel.isDirty() || turnoutModel.isDirty() || outputModel.isDirty()) {
575            int value = JOptionPane.showConfirmDialog(null, Bundle.getMessage("FieldMCFSaveDialogConfirmMessage"),
576                    Bundle.getMessage("ConfirmSaveDialogTitle"),
577                    JOptionPane.YES_NO_OPTION);
578            if (value == JOptionPane.YES_OPTION) {
579                saveTableValues();
580            }
581
582            // Offer to write the changes to EEPROM
583            value = JOptionPane.showConfirmDialog(null, Bundle.getMessage("FieldMCFCloseDialogConfirmMessage"),
584                    Bundle.getMessage("FieldMCFCloseDialogTitle"),
585                    JOptionPane.YES_NO_OPTION);
586
587            if (value == JOptionPane.YES_OPTION) {
588                tc.sendDCCppMessage(new DCCppMessage("E"), this);
589                log.debug("Sending: <E> (Write To EEPROM)");
590                sensorModel.fireTableDataChanged();
591            }
592
593        } else {
594            JOptionPane.showMessageDialog(null, Bundle.getMessage("FieldMCFCloseNoChangesDialog"));
595        }
596
597        // Close the window
598        dispose();
599    }
600
601    private final static Logger log = LoggerFactory.getLogger(ConfigBaseStationFrame.class);
602
603    /**
604     * Private class to serve as TableModel for Sensors.
605     */
606    private static class SensorTableModel extends DCCppTableModel {
607
608        public SensorTableModel() {
609            super(4, 5, 6, 7);
610            // Use i18n-ized column titles.
611            columnNames = new String[7];
612            columnNames[0] = Bundle.getMessage("FieldTableIndexColumn");
613            columnNames[1] = Bundle.getMessage("FieldTablePinColumn");
614            columnNames[2] = Bundle.getMessage("FieldTablePullupColumn");
615            columnNames[3] = Bundle.getMessage("ColumnDelete");
616            columnNames[4] = "isNew";       // hidden column // NOI18N
617            columnNames[5] = "isDirty";     // hidden column // NOI18N
618            columnNames[6] = "isDelete";    // hidden column // NOI18N
619        }
620
621        @Override
622        public Class<?> getColumnClass(int columnIndex) {
623            switch (columnIndex) {
624                case 0:
625                case 1:
626                    return Integer.class;
627                case 2:
628                    return Boolean.class;
629                case 3:
630                    return ButtonEditor.class;
631                case 4:
632                case 5:
633                case 6:
634                    return Boolean.class;
635                default:
636                    return super.getColumnClass(columnIndex);
637            }
638        }
639    }
640
641    /**
642     * Private class to serve as TableModel for Reporters and Ops Locations.
643     */
644    private static class TurnoutTableModel extends DCCppTableModel {
645
646        public TurnoutTableModel() {
647            super(4, 5, 6, 7);
648            // Use i18n-ized column titles.
649            columnNames = new String[7];
650            columnNames[0] = Bundle.getMessage("FieldTableIndexColumn");
651            columnNames[1] = Bundle.getMessage("AddressCol");
652            columnNames[2] = Bundle.getMessage("FieldTableSubaddrColumn");
653            columnNames[3] = Bundle.getMessage("ColumnDelete");
654            columnNames[4] = "isNew";        // hidden column // NOI18N
655            columnNames[5] = "isDirty";      // hidden column // NOI18N
656            columnNames[6] = "isDelete";     // hidden column // NOI18N
657        }
658
659        @Override
660        public Class<?> getColumnClass(int columnIndex) {
661            switch (columnIndex) {
662                case 0:
663                case 1:
664                case 2:
665                    return Integer.class;
666                case 3:
667                    return ConfigBaseStationFrame.ButtonEditor.class;
668                case 4:
669                case 5:
670                case 6:
671                    return Boolean.class;
672                default:
673                    return super.getColumnClass(columnIndex);
674            }
675        }
676    }
677
678    /**
679     * Private class to serve as TableModel for Reporters and Ops Locations.
680     */
681    private static class OutputTableModel extends DCCppTableModel {
682
683        public OutputTableModel() {
684            super(6, 7, 8, 9);
685            // Use i18n-ized column titles.
686            columnNames = new String[9];
687            columnNames[0] = Bundle.getMessage("FieldTableIndexColumn");
688            columnNames[1] = Bundle.getMessage("FieldTablePinColumn");
689            columnNames[2] = Bundle.getMessage("FieldTableInvertColumn");
690            columnNames[3] = Bundle.getMessage("FieldTableOutputRestoreStateColumn");
691            columnNames[4] = Bundle.getMessage("FieldTableOutputForceToColumn");
692            columnNames[5] = Bundle.getMessage("ColumnDelete");
693            columnNames[6] = "isNew";        // hidden column // NOI18N
694            columnNames[7] = "isDirty";      // hidden column // NOI18N
695            columnNames[8] = "isDelete";     // hidden column // NOI18N
696        }
697
698        @Override
699        public int getDeleteColumn() {
700            return (5);
701        }
702
703        @Override
704        public Class<?> getColumnClass(int columnIndex) {
705            switch (columnIndex) {
706                case 0:
707                case 1:
708                    return Integer.class;
709                case 2:
710                case 3:
711                case 4:
712                    return Boolean.class;
713                case 5:
714                    return ConfigBaseStationFrame.ButtonEditor.class;
715                case 6:
716                case 7:
717                case 8:
718                    return Boolean.class;
719                default:
720                    return super.getColumnClass(columnIndex);
721            }
722        }
723    }
724
725    static class ButtonRenderer extends JButton implements TableCellRenderer {
726
727        public ButtonRenderer() {
728            super.setOpaque(true);
729            super.setSelected(false);
730        }
731
732        @Override
733        public Component getTableCellRendererComponent(JTable table, Object value,
734                boolean isSelected, boolean hasFocus, int row, int column) {
735            if (isSelected) {
736                setForeground(table.getSelectionForeground());
737                setBackground(table.getSelectionBackground());
738            } else {
739                setForeground(table.getForeground());
740                setBackground(UIManager.getColor("Button.background"));
741            }
742            setText((value == null) ? "" : value.toString());
743            return this;
744        }
745    }
746
747    /**
748     * Button Editor class to replace the DefaultCellEditor in the table for the
749     * delete button.
750     * <p>
751     * NOTE: This isn't actually used anymore except as being a unique class
752     * type that can be returned from the TableModel classes for the column that
753     * includes the Delete buttons.
754     */
755    class ButtonEditor extends DefaultCellEditor {
756
757        protected JButton button;
758        private String label;
759        public ButtonEditor(JCheckBox checkBox, JTable t) {
760            super(checkBox);
761            button = new JButton();
762            button.setOpaque(true);
763            button.addActionListener((ActionEvent e) -> {
764                //fireEditingStopped();
765            });
766        }
767
768        @Override
769        public Component getTableCellEditorComponent(JTable table, Object value,
770                boolean isSelected, int row, int column) {
771            if (isSelected) {
772                button.setForeground(table.getSelectionForeground());
773                button.setBackground(table.getSelectionBackground());
774            } else {
775                button.setForeground(table.getForeground());
776                button.setBackground(table.getBackground());
777            }
778            label = (value == null) ? "" : value.toString();
779            button.setText(label);
780            return button;
781        }
782
783        @Override
784        public Object getCellEditorValue() {
785            return label;
786        }
787    }
788
789}