001package jmri.jmrit.beantable;
002
003import java.awt.BorderLayout;
004import java.awt.Color;
005import java.awt.Container;
006import java.awt.FlowLayout;
007import java.awt.GridLayout;
008import java.awt.event.ActionEvent;
009import java.beans.PropertyChangeListener;
010import java.util.ArrayList;
011
012import javax.annotation.Nonnull;
013import javax.swing.*;
014import javax.swing.border.Border;
015import javax.swing.table.AbstractTableModel;
016import javax.swing.table.TableCellEditor;
017import javax.swing.table.TableColumn;
018import javax.swing.table.TableColumnModel;
019import javax.swing.table.TableRowSorter;
020
021import jmri.InstanceManager;
022import jmri.SignalGroup;
023import jmri.SignalGroupManager;
024import jmri.SignalHead;
025import jmri.SignalHeadManager;
026import jmri.SignalMast;
027import jmri.SignalMastManager;
028import jmri.NamedBean.DisplayOptions;
029import jmri.swing.NamedBeanComboBox;
030import jmri.swing.RowSorterUtil;
031import jmri.util.JmriJFrame;
032import jmri.util.AlphanumComparator;
033import jmri.util.StringUtil;
034import jmri.util.swing.JComboBoxUtil;
035import jmri.util.swing.JmriJOptionPane;
036import jmri.util.table.ButtonEditor;
037import jmri.util.table.ButtonRenderer;
038
039/**
040 * Swing action to create and register a Signal Group Table.
041 * <p>
042 * Based in part on RouteTableAction.java by Bob Jacobsen
043 *
044 * @author Kevin Dickerson Copyright (C) 2010
045 * @author Egbert Broerse 2017, 2018
046 */
047public class SignalGroupTableAction extends AbstractTableAction<SignalGroup> implements PropertyChangeListener {
048
049    /**
050     * Create an action with a specific title.
051     * <p>
052     * Note that the argument is the Action title, not the title of the
053     * resulting frame. Perhaps this should be changed?
054     *
055     * @param s title of the action
056     */
057    public SignalGroupTableAction(String s) {
058        super(s);
059        // disable ourself if there is no primary SignalGroup manager available
060        if (InstanceManager.getNullableDefault(SignalGroupManager.class) == null) {
061            super.setEnabled(false);
062        }
063    }
064
065    public SignalGroupTableAction() {
066        this(Bundle.getMessage("TitleSignalGroupTable"));
067    }
068
069    @Override
070    public void propertyChange(java.beans.PropertyChangeEvent e) {
071        if (e.getPropertyName().equals("UpdateCondition")) {
072            for (int i = _signalHeadsList.size() - 1; i >= 0; i--) {
073                SignalGroupSignalHead signalHead = _signalHeadsList.get(i);
074                SignalHead sigBean = signalHead.getBean();
075                if (curSignalGroup.isHeadIncluded(sigBean)) {
076                    signalHead.setIncluded(true);
077                    signalHead.setOnState(curSignalGroup.getHeadOnState(sigBean));
078                    signalHead.setOffState(curSignalGroup.getHeadOffState(sigBean));
079                } else {
080                    signalHead.setIncluded(false);
081                }
082            }
083        }
084    }
085
086    /**
087     * Create the JTable DataModel, along with the changes for the specific case
088     * of SignalGroups.
089     */
090    @Override
091    protected void createModel() {
092        m = new BeanTableDataModel<SignalGroup>() {
093            @SuppressWarnings("hiding")     // Field has same name as a field in the super class
094            static public final int COMMENTCOL = 2;
095            @SuppressWarnings("hiding")     // Field has same name as a field in the super class
096            static public final int DELETECOL = 3;
097            static public final int ENABLECOL = 4;
098            static public final int EDITCOL = 5; // default name: SETCOL
099
100            @Override
101            public int getColumnCount() {
102                return 6;
103            }
104
105            @Override
106            public String getColumnName(int col) {
107                switch (col) {
108                    case EDITCOL:
109                        return "";    // no heading on "Edit" column
110                    case ENABLECOL:
111                        return Bundle.getMessage("ColumnHeadEnabled");
112                    case COMMENTCOL:
113                        return Bundle.getMessage("ColumnComment");
114                    case DELETECOL:
115                        return "";
116                    default:
117                        return super.getColumnName(col);
118                }
119            }
120
121            @Override
122            public Class<?> getColumnClass(int col) {
123                switch (col) {
124                    case EDITCOL:
125                    case DELETECOL:
126                        return JButton.class;
127                    case ENABLECOL:
128                        return Boolean.class;
129                    case COMMENTCOL:
130                        return String.class;
131                    default:
132                        return super.getColumnClass(col);
133                }
134            }
135
136            @Override
137            public int getPreferredWidth(int col) {
138                switch (col) {
139                    case EDITCOL:
140                        return new JTextField(Bundle.getMessage("ButtonEdit")).getPreferredSize().width;
141                    case ENABLECOL:
142                        return new JTextField(6).getPreferredSize().width;
143                    case COMMENTCOL:
144                        return new JTextField(30).getPreferredSize().width;
145                    case DELETECOL:
146                        return new JTextField(Bundle.getMessage("ButtonDelete")).getPreferredSize().width;
147                    default:
148                        return super.getPreferredWidth(col);
149                }
150            }
151
152            @Override
153            public boolean isCellEditable(int row, int col) {
154                switch (col) {
155                    case COMMENTCOL:
156                    case EDITCOL:
157                    case ENABLECOL:
158                    case DELETECOL:
159                        return true;
160                    default:
161                        return super.isCellEditable(row, col);
162                }
163            }
164
165            @Override
166            public Object getValueAt(int row, int col) {
167                SignalGroup b;
168                switch (col) {
169                    case EDITCOL:
170                        return Bundle.getMessage("ButtonEdit");
171                    case ENABLECOL:
172                        return ((SignalGroup) getValueAt(row, SYSNAMECOL)).getEnabled();
173                    case COMMENTCOL:
174                        b = (SignalGroup) getValueAt(row, SYSNAMECOL);
175                        return (b != null) ? b.getComment() : null;
176                    case DELETECOL:
177                        return Bundle.getMessage("ButtonDelete");
178                    default:
179                        return super.getValueAt(row, col);
180                }
181            }
182
183            @Override
184            public void setValueAt(Object value, int row, int col) {
185                switch (col) {
186                    case EDITCOL:
187                        SwingUtilities.invokeLater(() -> {
188                            addPressed(null); // set up add/edit panel addFrame (starts as Add pane)
189                            _systemName.setText(((SignalGroup) getValueAt(row, SYSNAMECOL)).toString());
190                            editPressed(null); // adjust addFrame for Edit
191                        });
192                        break;
193                    case ENABLECOL:
194                        SignalGroup r = (SignalGroup) getValueAt(row, SYSNAMECOL);
195                        r.setEnabled(!(r.getEnabled()));
196                        break;
197                    case COMMENTCOL:
198                        getBySystemName(sysNameList.get(row)).setComment(
199                                (String) value);
200                        fireTableRowsUpdated(row, row);
201                        break;
202                    case DELETECOL:
203                        deleteBean(row, col);
204                        break;
205                    default:
206                        super.setValueAt(value, row, col);
207                        break;
208                }
209            }
210
211            @Override
212            protected void configDeleteColumn(JTable table) {
213                // have the delete column hold a button
214                SignalGroupTableAction.this.setColumnToHoldButton(table, DELETECOL,
215                        new JButton(Bundle.getMessage("ButtonDelete")));
216            }
217
218            /**
219             * Delete the bean after all the checking has been done.
220             * <p>
221             * (Deactivate the Signal Group), then use the superclass to delete
222             * it.
223             */
224            @Override
225            protected void doDelete(SignalGroup bean) {
226                //((SignalGroup)bean).deActivateSignalGroup();
227                super.doDelete(bean);
228            }
229
230            // want to update when enabled parameter changes
231            @Override
232            protected boolean matchPropertyName(java.beans.PropertyChangeEvent e) {
233                if (e.getPropertyName().equals("Enabled")) {
234                    return true;
235                } else {
236                    return super.matchPropertyName(e);
237                }
238            }
239
240            @Override
241            public SignalGroupManager getManager() {
242                return InstanceManager.getDefault(SignalGroupManager.class);
243            }
244
245            @Override
246            public SignalGroup getBySystemName(@Nonnull String name) {
247                return InstanceManager.getDefault(SignalGroupManager.class).getBySystemName(name);
248            }
249
250            @Override
251            public SignalGroup getByUserName(@Nonnull String name) {
252                return InstanceManager.getDefault(SignalGroupManager.class).getByUserName(name);
253            }
254
255            @Override
256            public int getDisplayDeleteMsg() {
257                return 0x00;/*return InstanceManager.getDefault(jmri.UserPreferencesManager.class).getWarnDeleteSignalGroup();*/ }
258
259            @Override
260            public void setDisplayDeleteMsg(int boo) {
261                /*InstanceManager.getDefault(jmri.UserPreferencesManager.class).setWarnDeleteSignalGroup(boo); */
262
263            }
264
265            @Override
266            protected String getMasterClassName() {
267                return getClassName();
268            }
269
270            @Override
271            public void clickOn(SignalGroup t) { // mute action
272                //((SignalGroup)t).setSignalGroup();
273            }
274
275            @Override
276            public String getValue(String s) { // not directly used but should be present to implement abstract class
277                return "Set";
278            }
279
280            /*            public JButton configureButton() {
281                return new JButton(" Set ");
282            }*/
283            @Override
284            protected String getBeanType() {
285                return "Signal Group";
286            }
287        };
288    }
289
290    @Override
291    protected void setTitle() {
292        f.setTitle(Bundle.getMessage("TitleSignalGroupTable"));
293    }
294
295    @Override
296    protected String helpTarget() {
297        return "package.jmri.jmrit.beantable.SignalGroupTable";
298    }
299
300    /**
301     * Read Appearance for a Signal Group Signal Head from the state comboBox.
302     * <p>
303     * Called from SignalGroupSubTableAction.
304     *
305     * @param box comboBox to read from
306     * @return index representing selected set to appearance for head
307     */
308    int signalStateFromBox(JComboBox<String> box) {
309        String mode = (String) box.getSelectedItem();
310        int result = StringUtil.getStateFromName(mode, signalStatesValues, signalStates);
311
312        if (result < 0) {
313            log.warn("unexpected mode string in signalState Aspect: {}", mode);
314            throw new IllegalArgumentException();
315        }
316        return result;
317    }
318
319    /**
320     * Set Appearance in a Signal Group Signal Head state comboBox. Called from
321     * SignalGroupSubTableAction
322     *
323     * @param mode Value to be set
324     * @param box  in which to enter mode
325     */
326    void setSignalStateBox(int mode, JComboBox<String> box) {
327        String result = StringUtil.getNameFromState(mode, signalStatesValues, signalStates);
328        box.setSelectedItem(result);
329    }
330
331    JTextField _systemName = new JTextField(10);
332    JTextField _userName = new JTextField(22);
333    JCheckBox _autoSystemName = new JCheckBox(Bundle.getMessage("LabelAutoSysName"));
334    String systemNameAuto = this.getClass().getName() + ".AutoSystemName";
335    jmri.UserPreferencesManager pref;
336
337    JmriJFrame addFrame = null;
338
339    SignalGroupSignalHeadModel _SignalGroupHeadModel;
340    JScrollPane _SignalGroupHeadScrollPane;
341
342    SignalMastAspectModel _AspectModel;
343    JScrollPane _SignalAppearanceScrollPane;
344
345    NamedBeanComboBox<SignalMast> mainSignalComboBox;
346
347    ButtonGroup selGroup = null;
348    JRadioButton allButton = null;
349    JRadioButton includedButton = null;
350
351    JLabel nameLabel = new JLabel(Bundle.getMessage("LabelSystemName"), JLabel.TRAILING);
352    JLabel userLabel = new JLabel(Bundle.getMessage("LabelUserName"), JLabel.TRAILING);
353    JLabel fixedSystemName = new JLabel("xxxxxxxxxxx");
354
355    JButton deleteButton = new JButton(Bundle.getMessage("ButtonDelete") + " " + Bundle.getMessage("BeanNameSignalGroup"));
356    JButton createButton = new JButton(Bundle.getMessage("ButtonCreate"));
357    JButton updateButton = new JButton(Bundle.getMessage("ButtonApply"));
358    JButton cancelButton = new JButton(Bundle.getMessage("ButtonCancel"));
359
360    static final String createInst = Bundle.getMessage("SignalGroupAddStatusInitial1", Bundle.getMessage("ButtonCreate")); // I18N to include original button name in help string
361    static final String updateInst = Bundle.getMessage("SignalGroupAddStatusInitial3", Bundle.getMessage("ButtonApply"));
362    static final String cancelInst = Bundle.getMessage("SignalGroupAddStatusInitial4", Bundle.getMessage("ButtonCancel"));
363
364    JLabel status1 = new JLabel(createInst);
365    JLabel status2 = new JLabel(cancelInst);
366
367    JPanel p2xs = null;   // Container for...
368    JPanel p2xsi = null;  // SignalHead list table
369    JPanel p3xsi = null;
370
371    SignalGroup curSignalGroup = null;
372    boolean signalGroupDirty = false;  // true to fire reminder to save work
373    private boolean checkEnabled = jmri.InstanceManager.getDefault(jmri.configurexml.ShutdownPreferences.class).isStoreCheckEnabled();
374    boolean inEditMode = false; // to warn and prevent opening more than 1 editing session
375
376    /**
377     * Respond to click on Add... button below Signal Group Table.
378     * <p>
379     * Create JPanel with options for configuration.
380     *
381     * @param e Event from origin; null when called from Edit button in Signal
382     *          Group Table row
383     */
384    @Override
385    protected void addPressed(ActionEvent e) {
386        pref = InstanceManager.getDefault(jmri.UserPreferencesManager.class);
387        if (inEditMode) {
388            log.debug("Can not open another editing session for Signal Groups.");
389            // add user warning that a 2nd session not allowed (cf. Logix)
390            // Already editing a Signal Group, ask for completion of that edit first
391            String workingTitle = _systemName.getText();
392            if (workingTitle == null || workingTitle.isEmpty()) {
393                workingTitle = Bundle.getMessage("NONE");
394                _systemName.setText(workingTitle);
395            }
396            JmriJOptionPane.showMessageDialog(addFrame,
397                    Bundle.getMessage("SigGroupEditBusyWarning", workingTitle),
398                    Bundle.getMessage("ErrorTitle"),
399                    JmriJOptionPane.ERROR_MESSAGE);
400            // cancelEdit(); not needed as second edit is blocked
401            return;
402        }
403
404        //inEditMode = true;
405        _mastAspectsList = null;
406
407        SignalHeadManager shm = InstanceManager.getDefault(SignalHeadManager.class);
408        _signalHeadsList = new ArrayList<>();
409        // create list of all available Single Output Signal Heads to choose from
410        for (SignalHead sh : shm.getNamedBeanSet()) {
411            String systemName = sh.getSystemName();
412            if (sh.getClass().getName().contains("SingleTurnoutSignalHead")) {
413                String userName = sh.getUserName();
414                // add every single output signal head item to the list
415                _signalHeadsList.add(new SignalGroupSignalHead(systemName, userName));
416            } else {
417                log.debug("Signal Head {} is not a Single Output Controlled Signal Head", systemName);
418            }
419        }
420
421        // Set up Add/Edit Signal Group window
422        if (addFrame == null) { // if it's not yet present, create addFrame
423
424            mainSignalComboBox = new NamedBeanComboBox<>(InstanceManager.getDefault(SignalMastManager.class), null, DisplayOptions.DISPLAYNAME);
425            JComboBoxUtil.setupComboBoxMaxRows(mainSignalComboBox);
426            mainSignalComboBox.setAllowNull(true); // causes NPE when user selects that 1st line, so do not respond to result null
427            addFrame = new JmriJFrame(Bundle.getMessage("AddSignalGroup"), false, true);
428            addFrame.addHelpMenu("package.jmri.jmrit.beantable.SignalGroupAddEdit", true);
429            addFrame.setEscapeKeyClosesWindow(true);
430            addFrame.setLocation(100, 30);
431            addFrame.getContentPane().setLayout(new BoxLayout(addFrame.getContentPane(), BoxLayout.Y_AXIS));
432            Container contentPane = addFrame.getContentPane();
433
434            JPanel namesGrid = new JPanel();
435            GridLayout layout = new GridLayout(2, 2, 10, 0); // (int rows, int cols, int hgap, int vgap)
436            namesGrid.setLayout(layout);
437            // row 1: add system name label + field/label
438            namesGrid.add(nameLabel);
439            nameLabel.setLabelFor(_systemName);
440            JPanel ps = new JPanel();
441            ps.setLayout(new BoxLayout(ps, BoxLayout.X_AXIS));
442            ps.add(_systemName);
443            _systemName.setToolTipText(Bundle.getMessage("SignalGroupSysNameTooltip"));
444            ps.add(fixedSystemName);
445            fixedSystemName.setVisible(false);
446            ps.add(_autoSystemName);
447            _autoSystemName.addActionListener((ActionEvent e1) -> {
448                autoSystemName();
449            });
450            if (pref.getSimplePreferenceState(systemNameAuto)) {
451                _autoSystemName.setSelected(true);
452            }
453            namesGrid.add(ps);
454            // row 2: add user name label + field
455            namesGrid.add(userLabel);
456            userLabel.setLabelFor(_userName);
457            JPanel p = new JPanel();
458            p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
459            p.add(_userName);
460            _userName.setToolTipText(Bundle.getMessage("SignalGroupUserNameTooltip"));
461            namesGrid.add(p);
462            contentPane.add(namesGrid);
463
464            // add Signal Masts/Heads Display Choice
465            JPanel py = new JPanel();
466            py.add(new JLabel(Bundle.getMessage("Show")));
467            selGroup = new ButtonGroup();
468            allButton = new JRadioButton(Bundle.getMessage("All"), true);
469            selGroup.add(allButton);
470            py.add(allButton);
471            allButton.addActionListener((ActionEvent e1) -> {
472                // Setup for display of all Signal Masts & SingleTO Heads, if needed
473                if (!showAll) {
474                    showAll = true;
475                    _SignalGroupHeadModel.fireTableDataChanged();
476                    _AspectModel.fireTableDataChanged();
477                }
478            });
479            includedButton = new JRadioButton(Bundle.getMessage("Included"), false);
480            selGroup.add(includedButton);
481            py.add(includedButton);
482            includedButton.addActionListener((ActionEvent e1) -> {
483                // Setup for display of included Turnouts only, if needed
484                if (showAll) {
485                    showAll = false;
486                    initializeIncludedList();
487                    _SignalGroupHeadModel.fireTableDataChanged();
488                    _AspectModel.fireTableDataChanged();
489                }
490            });
491            py.add(new JLabel("  " + Bundle.getMessage("_and_", Bundle.getMessage("LabelAspects"),
492                    Bundle.getMessage("SignalHeads"))));
493            contentPane.add(py);
494
495            // add main signal mast table
496            JPanel p3 = new JPanel();
497            p3.setLayout(new BoxLayout(p3, BoxLayout.Y_AXIS));
498            JPanel p31 = new JPanel();
499            p31.add(new JLabel(Bundle.getMessage("EnterMastAttached", Bundle.getMessage("BeanNameSignalMast"))));
500            p3.add(p31);
501            JPanel p32 = new JPanel();
502            p32.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("BeanNameSignalMast"))));
503            p32.add(mainSignalComboBox); // comboBox to pick a main Signal Mast
504            p3.add(p32);
505
506            p3xsi = new JPanel();
507            JPanel p3xsiSpace = new JPanel();
508            p3xsiSpace.setLayout(new BoxLayout(p3xsiSpace, BoxLayout.Y_AXIS));
509            p3xsiSpace.add(new JLabel(" "));
510            p3xsi.add(p3xsiSpace);
511
512            JPanel p31si = new JPanel();
513            p31si.setLayout(new BoxLayout(p31si, BoxLayout.Y_AXIS));
514            p31si.add(new JLabel(Bundle.getMessage("SelectAppearanceTrigger")));
515
516            p3xsi.add(p31si);
517            _AspectModel = new SignalMastAspectModel();
518            JTable SignalMastAspectTable = new JTable(_AspectModel);
519            TableRowSorter<SignalMastAspectModel> smaSorter = new TableRowSorter<>(_AspectModel);
520            smaSorter.setComparator(SignalMastAspectModel.ASPECT_COLUMN, new AlphanumComparator());
521            RowSorterUtil.setSortOrder(smaSorter, SignalMastAspectModel.ASPECT_COLUMN, SortOrder.ASCENDING);
522            SignalMastAspectTable.setRowSorter(smaSorter);
523            SignalMastAspectTable.setRowSelectionAllowed(false);
524            SignalMastAspectTable.setPreferredScrollableViewportSize(new java.awt.Dimension(200, 80));
525            TableColumnModel SignalMastAspectColumnModel = SignalMastAspectTable.getColumnModel();
526            TableColumn includeColumnA = SignalMastAspectColumnModel.
527                    getColumn(SignalGroupTableAction.SignalMastAspectModel.INCLUDE_COLUMN);
528            includeColumnA.setResizable(false);
529            includeColumnA.setMinWidth(30);
530            includeColumnA.setMaxWidth(60);
531            @SuppressWarnings("static-access")
532            TableColumn sNameColumnA = SignalMastAspectColumnModel.
533                    getColumn(_AspectModel.ASPECT_COLUMN);
534            sNameColumnA.setResizable(true);
535            sNameColumnA.setMinWidth(75);
536            sNameColumnA.setMaxWidth(140);
537
538            _SignalAppearanceScrollPane = new JScrollPane(SignalMastAspectTable);
539            p3xsi.add(_SignalAppearanceScrollPane, BorderLayout.CENTER);
540            p3.add(p3xsi);
541            p3xsi.setVisible(true);
542
543            mainSignalComboBox.addActionListener( // respond to comboBox selection
544            (ActionEvent event) -> {
545                if (mainSignalComboBox.getSelectedItem() == null) { // ie. empty first row was selected or set
546                    log.debug("Empty line in mainSignal comboBox");
547                    //setValidSignalMastAspects(); // clears the Aspect table
548                } else {
549                    if (curSignalGroup == null
550                            || mainSignalComboBox.getSelectedItem() != curSignalGroup.getSignalMast()) {
551                        log.debug("comboBox closed, choice: {}", mainSignalComboBox.getSelectedItem());
552                        setValidSignalMastAspects(); // refresh table with signal mast aspects
553                    } else {
554                        log.debug("Mast {} picked in mainSignal comboBox", mainSignalComboBox.getSelectedItem());
555                    }
556                }
557            });
558
559            // complete this panel
560            Border p3Border = BorderFactory.createEtchedBorder();
561            p3.setBorder(p3Border);
562            contentPane.add(p3);
563
564            p2xsi = new JPanel();
565            JPanel p2xsiSpace = new JPanel();
566            p2xsiSpace.setLayout(new BoxLayout(p2xsiSpace, BoxLayout.Y_AXIS));
567            p2xsiSpace.add(new JLabel("XXX"));
568            p2xsi.add(p2xsiSpace);
569
570            JPanel p21si = new JPanel();
571            p21si.setLayout(new BoxLayout(p21si, BoxLayout.Y_AXIS));
572            p21si.add(new JLabel(Bundle.getMessage("SelectInGroup", Bundle.getMessage("SignalHeads"))));
573            p2xsi.add(p21si);
574            _SignalGroupHeadModel = new SignalGroupSignalHeadModel();
575            JTable SignalGroupHeadTable = new JTable(_SignalGroupHeadModel);
576            TableRowSorter<SignalGroupSignalHeadModel> sgsSorter = new TableRowSorter<>(_SignalGroupHeadModel);
577
578            // use NamedBean's built-in Comparator interface for sorting the system name column
579            RowSorterUtil.setSortOrder(sgsSorter, SignalGroupSignalHeadModel.SNAME_COLUMN, SortOrder.ASCENDING);
580            SignalGroupHeadTable.setRowSorter(sgsSorter);
581            SignalGroupHeadTable.setRowSelectionAllowed(false);
582            SignalGroupHeadTable.setPreferredScrollableViewportSize(new java.awt.Dimension(480, 160));
583            TableColumnModel SignalGroupSignalColumnModel = SignalGroupHeadTable.getColumnModel();
584
585            TableColumn includeColumnSi = SignalGroupSignalColumnModel.
586                    getColumn(SignalGroupSignalHeadModel.INCLUDE_COLUMN);
587            includeColumnSi.setResizable(false);
588            includeColumnSi.setMinWidth(30);
589            includeColumnSi.setMaxWidth(60);
590
591            TableColumn sNameColumnSi = SignalGroupSignalColumnModel.
592                    getColumn(SignalGroupSignalHeadModel.SNAME_COLUMN);
593            sNameColumnSi.setResizable(true);
594            sNameColumnSi.setMinWidth(75);
595            sNameColumnSi.setMaxWidth(95);
596
597            TableColumn uNameColumnSi = SignalGroupSignalColumnModel.
598                    getColumn(SignalGroupSignalHeadModel.UNAME_COLUMN);
599            uNameColumnSi.setResizable(true);
600            uNameColumnSi.setMinWidth(100);
601            uNameColumnSi.setMaxWidth(260);
602
603            TableColumn stateOnColumnSi = SignalGroupSignalColumnModel.
604                    getColumn(SignalGroupSignalHeadModel.STATE_ON_COLUMN); // a 6 column table
605            stateOnColumnSi.setResizable(false);
606            stateOnColumnSi.setMinWidth(Bundle.getMessage("SignalHeadStateFlashingYellow").length()); // was 50
607            stateOnColumnSi.setMaxWidth(100);
608
609            TableColumn stateOffColumnSi = SignalGroupSignalColumnModel.
610                    getColumn(SignalGroupSignalHeadModel.STATE_OFF_COLUMN);
611            stateOffColumnSi.setResizable(false);
612            stateOffColumnSi.setMinWidth(50);
613            stateOffColumnSi.setMaxWidth(100);
614
615            TableColumn editColumnSi = SignalGroupSignalColumnModel.
616                    getColumn(SignalGroupSignalHeadModel.EDIT_COLUMN);
617            editColumnSi.setResizable(false);
618            editColumnSi.setMinWidth(Bundle.getMessage("ButtonEdit").length()); // was 50
619            editColumnSi.setMaxWidth(100);
620            JButton editButton = new JButton(Bundle.getMessage("ButtonEdit"));
621            setColumnToHoldButton(SignalGroupHeadTable, SignalGroupSignalHeadModel.EDIT_COLUMN, editButton);
622
623            _SignalGroupHeadScrollPane = new JScrollPane(SignalGroupHeadTable);
624            p2xsi.add(_SignalGroupHeadScrollPane, BorderLayout.CENTER);
625            p2xsi.setToolTipText(Bundle.getMessage("SignalGroupHeadTableTooltip")); // add tooltip to explain which head types are shown
626            contentPane.add(p2xsi);
627            p2xsi.setVisible(true);
628
629            // add notes panel
630            JPanel pa = new JPanel();
631            pa.setLayout(new BoxLayout(pa, BoxLayout.Y_AXIS));
632            // include status bar
633            JPanel p1 = new JPanel();
634            p1.setLayout(new FlowLayout());
635            status1.setFont(status1.getFont().deriveFont(0.9f * nameLabel.getFont().getSize())); // a bit smaller
636            status1.setForeground(Color.gray);
637            p1.add(status1);
638            JPanel p2 = new JPanel();
639            p2.setLayout(new FlowLayout());
640            status2.setFont(status1.getFont().deriveFont(0.9f * nameLabel.getFont().getSize())); // a bit smaller
641            status2.setForeground(Color.gray);
642            p2.add(status2);
643            pa.add(p1);
644            pa.add(p2);
645
646            Border pBorder = BorderFactory.createEtchedBorder();
647            pa.setBorder(pBorder);
648            contentPane.add(pa);
649
650            // buttons at bottom of panel
651            JPanel pb = new JPanel();
652            pb.setLayout(new FlowLayout(FlowLayout.TRAILING));
653
654            pb.add(cancelButton);
655            cancelButton.addActionListener(this::cancelPressed);
656            cancelButton.setVisible(true);
657            pb.add(deleteButton);
658            deleteButton.addActionListener(this::deletePressed);
659            deleteButton.setToolTipText(Bundle.getMessage("DeleteSignalGroupInSystem"));
660            // Add Create Group button
661            pb.add(createButton);
662            createButton.addActionListener(this::createPressed);
663            createButton.setToolTipText(Bundle.getMessage("TooltipCreateGroup"));
664            // [Update] Signal Group button in Add/Edit SignalGroup pane
665            pb.add(updateButton);
666            updateButton.addActionListener((ActionEvent e1) -> {
667                updatePressed(e1, false, false);
668            });
669            updateButton.setToolTipText(Bundle.getMessage("TooltipUpdateGroup"));
670
671            contentPane.add(pb);
672            // pack and release space
673            addFrame.pack();
674            p2xsiSpace.setVisible(false);
675        } else {
676            mainSignalComboBox.setSelectedItem(null);
677            addFrame.setTitle(Bundle.getMessage("AddSignalGroup")); // reset title for new group
678        }
679        status1.setText(createInst);
680        _autoSystemName.setVisible(true);
681        updateButton.setVisible(false);
682        createButton.setVisible(true);
683        // set listener for window closing
684        addFrame.addWindowListener(new java.awt.event.WindowAdapter() {
685            @Override
686            public void windowClosing(java.awt.event.WindowEvent e) {
687                // remind to save, if Signal Group was created or edited
688                if (signalGroupDirty && !checkEnabled) {
689                    InstanceManager.getDefault(jmri.UserPreferencesManager.class).
690                            showInfoMessage(Bundle.getMessage("ReminderTitle"),
691                                    Bundle.getMessage("ReminderSaveString", Bundle.getMessage("SignalGroup")),
692                                    getClassName(),
693                                    "remindSignalGroup"); // NOI18N
694                    signalGroupDirty = false;
695                }
696                // hide addFrame
697                if (addFrame != null) {
698                    addFrame.setVisible(false);
699                } // hide first, could be gone by the time of the close event, so prevent NPE
700                inEditMode = false; // release editing soon, as long as NPEs occor in the following methods
701                finishUpdate();
702                _SignalGroupHeadModel.dispose();
703                _AspectModel.dispose();
704            }
705        });
706        // display the pane
707        addFrame.setVisible(true);
708        autoSystemName();
709    }
710
711    void autoSystemName() {
712        if (_autoSystemName.isSelected()) {
713            _systemName.setEnabled(false);
714            nameLabel.setEnabled(false);
715        } else {
716            _systemName.setEnabled(true);
717            nameLabel.setEnabled(true);
718        }
719    }
720
721    void setColumnToHoldButton(JTable table, int column, JButton sample) {
722        // install a button renderer & editor
723        ButtonRenderer buttonRenderer = new ButtonRenderer();
724        table.setDefaultRenderer(JButton.class, buttonRenderer);
725        TableCellEditor buttonEditor = new ButtonEditor(new JButton());
726        table.setDefaultEditor(JButton.class, buttonEditor);
727        // ensure the table rows, columns have enough room for buttons
728        table.setRowHeight(sample.getPreferredSize().height);
729        table.getColumnModel().getColumn(column)
730                .setPreferredWidth((sample.getPreferredSize().width) + 4);
731    }
732
733    /**
734     * Initialize list of included signal head appearances for when "Included"
735     * is selected.
736     */
737    void initializeIncludedList() {
738        _includedMastAspectsList = new ArrayList<>();
739        for (int i = 0; i < _mastAspectsList.size(); i++) {
740            if (_mastAspectsList.get(i).isIncluded()) {
741                _includedMastAspectsList.add(_mastAspectsList.get(i));
742            }
743        }
744        _includedSignalHeadsList = new ArrayList<>();
745        for (int i = 0; i < _signalHeadsList.size(); i++) {
746            if (_signalHeadsList.get(i).isIncluded()) {
747                _includedSignalHeadsList.add(_signalHeadsList.get(i));
748            }
749        }
750    }
751
752    /**
753     * Respond to the Create button.
754     *
755     * @param e the action event
756     */
757    void createPressed(ActionEvent e) {
758        if (!_autoSystemName.isSelected()) {
759            if (!checkNewNamesOK()) {
760                log.debug("NewNames not OK");
761                return;
762            }
763        }
764        updatePressed(e, true, true); // to close pane after creating
765        pref.setSimplePreferenceState(systemNameAuto, _autoSystemName.isSelected());
766        // activate the signal group
767    }
768
769    /**
770     * Check name for a new SignalGroup object using the _systemName field on
771     * the addFrame pane. Not used when autoSystemName is checked.
772     *
773     * @return whether name entered is allowed
774     */
775    boolean checkNewNamesOK() {
776        // Get system name and user name from Add Signal Group pane
777        String sName = _systemName.getText();
778        String uName = _userName.getText(); // may be empty
779        if (sName.length() == 0) { // show warning in status bar
780            status1.setText(Bundle.getMessage("AddBeanStatusEnter"));
781            status1.setForeground(Color.red);
782            return false;
783        }
784        SignalGroup g;
785        // check if a SignalGroup with the same user name exists
786        if (!uName.isEmpty()) {
787            g = InstanceManager.getDefault(SignalGroupManager.class).getByUserName(uName);
788            if (g != null) {
789                // SignalGroup already exists
790                status1.setText(Bundle.getMessage("SignalGroupDuplicateUserNameWarning", uName));
791                return false;
792            }
793        }
794        // check if a SignalGroup with this system name already exists
795        sName = InstanceManager.getDefault(SignalGroupManager.class).makeSystemName(sName);
796        g = InstanceManager.getDefault(SignalGroupManager.class).getBySystemName(sName);
797        if (g != null) {
798            // SignalGroup already exists
799            status1.setText(Bundle.getMessage("SignalGroupDuplicateSystemNameWarning", sName));
800            return false;
801        }
802        return true;
803    }
804
805    /**
806     * Check selection in Main Mast comboBox and store object as mMast for
807     * further calculations.
808     *
809     * @return The new/updated SignalGroup object
810     */
811    boolean checkValidSignalMast() {
812        SignalMast mMast = mainSignalComboBox.getSelectedItem();
813        if (mMast == null) {
814            //log.warn("Signal Mast not selected. mainSignal = {}", mainSignalComboBox.getSelectedItem());
815            JmriJOptionPane.showMessageDialog(null,
816                    Bundle.getMessage("NoMastSelectedWarning"),
817                    Bundle.getMessage("ErrorTitle"),
818                    JmriJOptionPane.WARNING_MESSAGE);
819            return false;
820        }
821        return true;
822    }
823
824    /**
825     * Check name and return a new or existing SignalGroup object with the name
826     * as entered in the _systemName field on the addFrame pane.
827     *
828     * @return The new/updated SignalGroup object
829     */
830    SignalGroup checkNamesOK() {
831        // Get system name and user name
832        String sName = _systemName.getText();
833        String uName = _userName.getText();
834        SignalGroup g;
835        if (_autoSystemName.isSelected() && !inEditMode) {
836            // create new Signal Group with auto system name
837            log.debug("SignalGroupTableAction checkNamesOK new autogroup");
838            g = InstanceManager.getDefault(SignalGroupManager.class).newSignalGroupWithUserName(uName);
839        } else {
840            if (sName.length() == 0) { // show warning in status bar
841                status1.setText(Bundle.getMessage("AddBeanStatusEnter"));
842                status1.setForeground(Color.red);
843                return null;
844            }
845            try {
846                sName = InstanceManager.getDefault(SignalGroupManager.class).makeSystemName(sName);
847                g = InstanceManager.getDefault(SignalGroupManager.class).provideSignalGroup(sName, uName);
848            } catch (IllegalArgumentException ex) {
849                log.error("checkNamesOK; Unknown failure to create Signal Group with System Name: {}", sName); // NOI18N
850                g = null; // for later check
851            }
852        }
853        if (g == null) {
854            // should never get here
855            log.error("Unknown failure to create Signal Group with System Name: {}", sName); // NOI18N
856        }
857        return g;
858    }
859
860    /**
861     * Check all available Single Output Signal Heads against the list of signal
862     * head items registered with the group. Updates the list, which is stored
863     * in the field _includedSignalHeadsList.
864     *
865     * @param g Signal Group object
866     * @return The number of Signal Heads included in the group
867     */
868    int setHeadInformation(SignalGroup g) {
869        for (int i = 0; i < g.getNumHeadItems(); i++) {
870            SignalHead sig = g.getHeadItemBeanByIndex(i);
871            boolean valid = false;
872            for (int x = 0; x < _includedSignalHeadsList.size(); x++) {
873                SignalGroupSignalHead sh = _includedSignalHeadsList.get(x);
874                if (sig == sh.getBean()) {
875                    valid = true;
876                    break;
877                }
878            }
879            if (!valid) {
880                g.deleteSignalHead(sig);
881            }
882        }
883        for (int i = 0; i < _includedSignalHeadsList.size(); i++) {
884            SignalGroupSignalHead s = _includedSignalHeadsList.get(i);
885            SignalHead sig = s.getBean();
886            if (!g.isHeadIncluded(sig)) {
887                g.addSignalHead(sig);
888                g.setHeadOnState(sig, s.getOnStateInt());
889                g.setHeadOffState(sig, s.getOffStateInt());
890            }
891        }
892        return _includedSignalHeadsList.size();
893    }
894
895    /**
896     * Store included Aspects for the selected main Signal Mast in the Signal
897     * Group
898     *
899     * @param g Signal Group object
900     */
901    void setMastAspectInformation(SignalGroup g) {
902        g.clearSignalMastAspect();
903        for (int x = 0; x < _includedMastAspectsList.size(); x++) {
904            g.addSignalMastAspect(_includedMastAspectsList.get(x).getAspect());
905        }
906    }
907
908    /**
909     * Look up the list of valid Aspects for the selected main Signal Mast in
910     * the comboBox and store them in a table on the addFrame using _AspectModel
911     */
912    void setValidSignalMastAspects() {
913        SignalMast sm = mainSignalComboBox.getSelectedItem();
914        if (sm == null) {
915            log.debug("Null picked in mainSignal comboBox. Probably line 1 or no masts in system");
916            return;
917        }
918        log.debug("Mast {} picked in mainSignal comboBox", mainSignalComboBox.getSelectedItem());
919        java.util.Vector<String> aspects = sm.getValidAspects();
920
921        _mastAspectsList = new ArrayList<>(aspects.size());
922        for (int i = 0; i < aspects.size(); i++) {
923            _mastAspectsList.add(new SignalMastAspect(aspects.get(i)));
924        }
925        _AspectModel.fireTableDataChanged();
926    }
927
928    /**
929     * When user clicks Cancel during editing a Signal Group, close the
930     * Add/Edit pane and reset default entries.
931     *
932     * @param e Event from origin
933     */
934    void cancelPressed(ActionEvent e) {
935        cancelEdit();
936    }
937
938    /**
939     * Cancels edit mode
940     */
941    void cancelEdit() {
942        if (inEditMode) {
943            status1.setText(createInst);
944        }
945        if (addFrame != null) {
946            addFrame.setVisible(false);
947        } // hide first, may cause NPE unchecked
948        inEditMode = false; // release editing soon, as NPEs may occur in the following methods
949        finishUpdate();
950        _SignalGroupHeadModel.dispose();
951        _AspectModel.dispose();
952    }
953
954    /**
955     * Respond to the Edit button in the Signal Group Table after creating the
956     * Add/Edit pane with AddPressed supplying _SystemName. Hides the editable
957     * _systemName field on the Add Group pane and displays the value as a label
958     * instead.
959     *
960     * @param e Event from origin, null if invoked by clicking the Edit button
961     *          in a Signal Group Table row
962     */
963    void editPressed(ActionEvent e) {
964        // identify the Signal Group with this name if it already exists
965        String sName = InstanceManager.getDefault(SignalGroupManager.class).makeSystemName(_systemName.getText());
966        // sName is already filled in from the Signal Group table by addPressed()
967        SignalGroup g = InstanceManager.getDefault(SignalGroupManager.class).getBySystemName(sName);
968        if (g == null) {
969            // Signal Group does not exist, so cannot be edited
970            return;
971        }
972        g.addPropertyChangeListener(this);
973
974        // Signal Group was found, make its system name not changeable
975        curSignalGroup = g;
976        log.debug("curSignalGroup was set");
977
978        SignalMast sm = InstanceManager.getDefault(SignalMastManager.class).getSignalMast(g.getSignalMastName());
979        if (sm != null) {
980            java.util.Vector<String> aspects = sm.getValidAspects();
981            _mastAspectsList = new ArrayList<>(aspects.size());
982
983            for (int i = 0; i < aspects.size(); i++) {
984                _mastAspectsList.add(new SignalMastAspect(aspects.get(i)));
985            }
986        } else {
987            log.error("Failed to get signal mast {}", g.getSignalMastName()); // false indicates Can't find mast
988        }
989
990        nameLabel.setEnabled(true);
991        fixedSystemName.setText(sName);
992        fixedSystemName.setVisible(true);
993        _systemName.setVisible(false);
994        mainSignalComboBox.setSelectedItem(g.getSignalMast());
995        _userName.setText(g.getUserName());
996
997        int setRow = 0;
998
999        for (int i = _signalHeadsList.size() - 1; i >= 0; i--) {
1000            SignalGroupSignalHead sgsh = _signalHeadsList.get(i);
1001            SignalHead sigBean = sgsh.getBean();
1002            if (g.isHeadIncluded(sigBean)) {
1003                sgsh.setIncluded(true);
1004                sgsh.setOnState(g.getHeadOnState(sigBean));
1005                sgsh.setOffState(g.getHeadOffState(sigBean));
1006                setRow = i;
1007            } else {
1008                sgsh.setIncluded(false);
1009            }
1010        }
1011        _SignalGroupHeadScrollPane.getVerticalScrollBar().setValue(setRow * ROW_HEIGHT);
1012        _SignalGroupHeadModel.fireTableDataChanged();
1013
1014        for (int i = 0; i < _mastAspectsList.size(); i++) {
1015            SignalMastAspect _aspect = _mastAspectsList.get(i);
1016            String asp = _aspect.getAspect();
1017            if (g.isSignalMastAspectIncluded(asp)) {
1018                _aspect.setIncluded(true);
1019                setRow = i;
1020            } else {
1021                _aspect.setIncluded(false);
1022            }
1023        }
1024        _SignalAppearanceScrollPane.getVerticalScrollBar().setValue(setRow * ROW_HEIGHT);
1025
1026        _AspectModel.fireTableDataChanged();
1027        initializeIncludedList();
1028
1029        signalGroupDirty = true;  // to fire reminder to save work
1030        // set up buttons and notes fot Edit
1031        status1.setText(updateInst);
1032        updateButton.setVisible(true);
1033        createButton.setVisible(false);
1034        _autoSystemName.setVisible(false);
1035        fixedSystemName.setVisible(true);
1036        _systemName.setVisible(false);
1037        addFrame.setTitle(Bundle.getMessage("EditSignalGroup"));
1038        addFrame.setEscapeKeyClosesWindow(true);
1039        inEditMode = true; // to block opening another edit session
1040    }
1041
1042    /**
1043     * Respond to the Delete button in the Add/Edit pane.
1044     *
1045     * @param e the event heard
1046     */
1047    void deletePressed(ActionEvent e) {
1048        InstanceManager.getDefault(SignalGroupManager.class).deleteSignalGroup(curSignalGroup);
1049        curSignalGroup = null;
1050        log.debug("DeletePressed; curSignalGroup set to null");
1051        finishUpdate();
1052    }
1053
1054    /**
1055     * Respond to the Update button on the Edit Signal Group pane - store new
1056     * properties in the Signal Group.
1057     *
1058     * @param e              Event from origin, null if invoked by clicking the
1059     *                       Edit button in a Signal Group Table row
1060     * @param newSignalGroup False when called as Update, True after editing
1061     *                       Signal Head details
1062     * @param close          True if the pane is closing, False if it stays open
1063     */
1064    void updatePressed(ActionEvent e, boolean newSignalGroup, boolean close) {
1065        // Check if the User Name has been changed
1066        String uName = _userName.getText();
1067        SignalGroup g = checkNamesOK(); // look up signal group under edit. If this fails, we are stuck
1068        if (g == null) { // error logging/dialog handled in checkNamesOK()
1069            log.debug("null signalGroup under edit");
1070            return;
1071        }
1072        // We might want to check if the User Name has been changed. But there's
1073        // nothing to compare with so this is propably a newly created Signal Group.
1074        // TODO cannot be compared since curSignalGroup is null, causes NPE
1075        if (!checkValidSignalMast()) {
1076            log.debug("invalid signal mast under edit");
1077            return;
1078        }
1079        // user name is unique, change it
1080        g.setUserName(uName);
1081        initializeIncludedList();
1082        setHeadInformation(g);
1083        setMastAspectInformation(g);
1084        g.setSignalMast(mainSignalComboBox.getSelectedItem(), mainSignalComboBox.getSelectedItemDisplayName());
1085
1086        signalGroupDirty = true;  // to fire reminder to save work
1087        curSignalGroup = g;
1088        if (close) {
1089            finishUpdate();
1090        }
1091        status1.setForeground(Color.gray);
1092        status1.setText((newSignalGroup ? Bundle.getMessage("SignalGroupAddStatusCreated") : Bundle.getMessage("SignalGroupAddStatusUpdated"))
1093                + ": \"" + uName + "\"");
1094    }
1095
1096    /**
1097     * Clean up the Edit Signal Group pane.
1098     */
1099    void finishUpdate() {
1100        if (curSignalGroup != null) {
1101            curSignalGroup.removePropertyChangeListener(this);
1102        }
1103        _systemName.setVisible(true);
1104        fixedSystemName.setVisible(false);
1105        _systemName.setText("");
1106        _userName.setText("");
1107        _autoSystemName.setVisible(true);
1108        autoSystemName();
1109        // clear page
1110        mainSignalComboBox.setSelectedItem(null); // empty the "main mast" comboBox
1111        if (_signalHeadsList == null) {
1112            // prevent NPE when clicking Cancel/close pane with no work done, after first display of pane (no mast selected)
1113            log.debug("FinishUpdate; _signalHeadsList empty; no heads present");
1114        } else {
1115            for (int i = _signalHeadsList.size() - 1; i >= 0; i--) {
1116                _signalHeadsList.get(i).setIncluded(false);
1117            }
1118        }
1119        if (_mastAspectsList == null) {
1120            // prevent NPE when clicking Cancel/close pane with no work done, after first display of pane (no mast selected)
1121            log.debug("FinishUpdate; _mastAspectsList empty; no mast was selected");
1122        } else {
1123            for (int i = _mastAspectsList.size() - 1; i >= 0; i--) {
1124                _mastAspectsList.get(i).setIncluded(false);
1125            }
1126        }
1127        inEditMode = false;
1128        showAll = true;
1129        curSignalGroup = null;
1130        log.debug("FinishUpdate; curSignalGroup set to null. Hiding addFrame next");
1131        if (addFrame != null) {
1132            addFrame.setVisible(false);
1133        }
1134    }
1135
1136    /**
1137     * Table Model for masts and their "Set To" aspect.
1138     */
1139    public class SignalMastAspectModel extends AbstractTableModel implements PropertyChangeListener {
1140
1141        @Override
1142        public Class<?> getColumnClass(int c) {
1143            if (c == INCLUDE_COLUMN) {
1144                return Boolean.class;
1145            } else {
1146                return String.class;
1147            }
1148        }
1149
1150        @Override
1151        public String getColumnName(int col) {
1152            if (col == INCLUDE_COLUMN) {
1153                return Bundle.getMessage("Include");
1154            }
1155            if (col == ASPECT_COLUMN) {
1156                return Bundle.getMessage("LabelAspectType");
1157                // list contains Signal Mast Aspects (might be called "Appearances" by some but in code keep to JMRI bean names and Help)
1158            }
1159            return "";
1160        }
1161
1162        public void dispose() {
1163            InstanceManager.getDefault(SignalMastManager.class).removePropertyChangeListener(this);
1164        }
1165
1166        @Override
1167        public void propertyChange(java.beans.PropertyChangeEvent e) {
1168            if (e.getPropertyName().equals("length")) {
1169                // a new NamedBean is available in the manager
1170                fireTableDataChanged();
1171            }
1172        }
1173
1174        @Override
1175        public int getColumnCount() {
1176            return 2;
1177        }
1178
1179        @Override
1180        public boolean isCellEditable(int r, int c) {
1181            return ((c == INCLUDE_COLUMN));
1182        }
1183
1184        public static final int ASPECT_COLUMN = 0;
1185        public static final int INCLUDE_COLUMN = 1;
1186
1187        public void setSetToState(String x) {
1188        }
1189
1190        @Override
1191        public int getRowCount() {
1192            if (_mastAspectsList == null) {
1193                return 0;
1194            }
1195            else if (showAll) {
1196                return _mastAspectsList.size();
1197            } else {
1198                return _includedMastAspectsList.size();
1199            }
1200        }
1201
1202        @Override
1203        public Object getValueAt(int r, int c) {
1204            ArrayList<SignalMastAspect> aspectList = ( showAll ? _mastAspectsList : _includedMastAspectsList);
1205            // some error checking
1206            if (aspectList == null || r >= aspectList.size()) {
1207                // prevent NPE when clicking Add... in table to add new group (with 1 group existing using a different mast type)
1208                if (aspectList == null) {
1209                    log.debug("SGTA getValueAt: row value {} aspectList is null", r);
1210                } else {
1211                    log.debug("SGTA getValueAt: row value {} is greater than aspectList size {}", r, aspectList.size());
1212                }
1213                return null;
1214            }
1215            switch (c) {
1216                case INCLUDE_COLUMN:
1217                    return aspectList.get(r).isIncluded();
1218                case ASPECT_COLUMN:
1219                    return aspectList.get(r).getAspect();
1220                default:
1221                    return null;
1222            }
1223        }
1224
1225        @Override
1226        public void setValueAt(Object type, int r, int c) {
1227            log.debug("SigGroupEditSet A; row = {}", r);
1228            ArrayList<SignalMastAspect> aspectList = ( showAll ? _mastAspectsList : _includedMastAspectsList);
1229            if (_mastAspectsList == null || r >= aspectList.size()) {
1230                // prevent NPE when closing window after NPE in getValueAdd() happened
1231                log.debug("row value {} is greater than aspectList size {}", r, aspectList);
1232                return;
1233            }
1234            log.debug("SigGroupEditSet B; row = {}; aspectList.size() = {}.", r, aspectList.size());
1235            switch (c) {
1236                case INCLUDE_COLUMN:
1237                    aspectList.get(r).setIncluded(((Boolean) type));
1238                    break;
1239                case ASPECT_COLUMN:
1240                    aspectList.get(r).setAspect((String) type);
1241                    break;
1242                default:
1243                    break;
1244            }
1245        }
1246
1247    }
1248
1249    /**
1250     * Base table model for managing generic Signal Group outputs.
1251     */
1252    public abstract class SignalGroupOutputModel extends AbstractTableModel implements PropertyChangeListener {
1253
1254        @Override
1255        public Class<?> getColumnClass(int c) {
1256            if (c == INCLUDE_COLUMN) {
1257                return Boolean.class;
1258            } else {
1259                return String.class;
1260            }
1261        }
1262
1263        @Override
1264        public void propertyChange(java.beans.PropertyChangeEvent e) {
1265            if (e.getPropertyName().equals("length")) {
1266                // a new NamedBean is available in the manager
1267                fireTableDataChanged();
1268            } else if (e.getPropertyName().equals("UpdateCondition")) {
1269                fireTableDataChanged();
1270            }
1271        }
1272
1273        @Override
1274        public String getColumnName(int c) {
1275            return COLUMN_NAMES[c];
1276        }
1277
1278        @Override
1279        public int getColumnCount() {
1280            return 4;
1281        }
1282
1283        @Override
1284        public boolean isCellEditable(int r, int c) {
1285            return ((c == INCLUDE_COLUMN) || (c == STATE_COLUMN));
1286        }
1287
1288        public static final int SNAME_COLUMN = 0;
1289        public static final int UNAME_COLUMN = 1;
1290        public static final int INCLUDE_COLUMN = 2;
1291        public static final int STATE_COLUMN = 3;
1292
1293    }
1294
1295    /**
1296     * Table Model to manage Signal Head outputs in a Signal Group.
1297     */
1298    class SignalGroupSignalHeadModel extends SignalGroupOutputModel {
1299
1300        SignalGroupSignalHeadModel() {
1301            addPcl();
1302        }
1303
1304        final void addPcl(){
1305            InstanceManager.getDefault(SignalHeadManager.class).addPropertyChangeListener(this);
1306        }
1307
1308        @Override
1309        public boolean isCellEditable(int r, int c) {
1310            return ((c == INCLUDE_COLUMN) || (c == STATE_ON_COLUMN) || (c == STATE_OFF_COLUMN) || (c == EDIT_COLUMN));
1311        }
1312
1313        @Override
1314        public int getColumnCount() {
1315            return 6;
1316        }
1317
1318        public static final int STATE_ON_COLUMN = 3;
1319        public static final int STATE_OFF_COLUMN = 4;
1320        public static final int EDIT_COLUMN = 5;
1321
1322        @Override
1323        public Class<?> getColumnClass(int c) {
1324            switch (c) {
1325                case INCLUDE_COLUMN:
1326                    return Boolean.class;
1327                case EDIT_COLUMN:
1328                    return JButton.class;
1329                default:
1330                    return String.class;
1331            }
1332        }
1333
1334        @Override
1335        public String getColumnName(int c) {
1336            return COLUMN_SIG_NAMES[c];
1337        }
1338
1339        public void setSetToState(String x) {
1340        }
1341
1342        /**
1343         * The number of rows in the Signal Head table.
1344         *
1345         * @return The number of rows
1346         */
1347        @Override
1348        public int getRowCount() {
1349            return ( showAll ? _signalHeadsList.size() : _includedSignalHeadsList.size() );
1350        }
1351
1352        /**
1353         * Fill in info cells of the Signal Head table on the Add/Edit Group
1354         * Edit pane.
1355         *
1356         * @param r Index of the cell row
1357         * @param c Index of the cell column
1358         */
1359        @Override
1360        public Object getValueAt(int r, int c) {
1361            ArrayList<SignalGroupSignalHead> headsList = ( showAll ? _signalHeadsList : _includedSignalHeadsList);
1362            // some error checking
1363            if (r >= headsList.size()) {
1364                log.debug("Row num {} is greater than headsList size {}", r, headsList.size());
1365                return null;
1366            }
1367            switch (c) {
1368                case INCLUDE_COLUMN:
1369                    return headsList.get(r).isIncluded();
1370                case SNAME_COLUMN:
1371                    return headsList.get(r).getSysName();
1372                case UNAME_COLUMN:
1373                    return headsList.get(r).getUserName();
1374                case STATE_ON_COLUMN:
1375                    return headsList.get(r).getOnState();
1376                case STATE_OFF_COLUMN:
1377                    return headsList.get(r).getOffState();
1378                case EDIT_COLUMN:
1379                    return (Bundle.getMessage("ButtonEdit"));
1380                default:
1381                    return null;
1382            }
1383        }
1384
1385        /**
1386         * Fetch User Name (System Name if User Name is empty) for a row in the
1387         * Signal Head table.
1388         *
1389         * @param r index in the signal head table of head to be edited
1390         * @return name of signal head
1391         */
1392        public String getDisplayName(int r) {
1393            if (((String) getValueAt(r, UNAME_COLUMN) != null) && (!((String) getValueAt(r, UNAME_COLUMN)).isEmpty())) {
1394                return (String) getValueAt(r, UNAME_COLUMN);
1395            } else {
1396                return (String) getValueAt(r, SNAME_COLUMN);
1397            }
1398        }
1399
1400        /**
1401         * Fetch existing bean object for a row in the Signal Head table.
1402         *
1403         * @param r index in the signal head table of head to be edited
1404         * @return bean object of the head
1405         */
1406        public SignalHead getBean(int r) {
1407            return InstanceManager.getDefault(SignalHeadManager.class).getSignalHead((String) getValueAt(r, SNAME_COLUMN));
1408        }
1409
1410        /**
1411         * Store info from the cells of the Signal Head table of the Add/Edit
1412         * Group Edit pane.
1413         *
1414         * @param type The contents from the table
1415         * @param r    Index of the cell row of the entry
1416         * @param c    Index of the cell column of the entry
1417         */
1418        @Override
1419        public void setValueAt(Object type, int r, int c) {
1420            ArrayList<SignalGroupSignalHead> headsList = (showAll ? _signalHeadsList : _includedSignalHeadsList);
1421            switch (c) {
1422                case INCLUDE_COLUMN:
1423                    headsList.get(r).setIncluded(((Boolean) type));
1424                    break;
1425                case STATE_ON_COLUMN:
1426                    headsList.get(r).setSetOnState((String) type);
1427                    break;
1428                case STATE_OFF_COLUMN:
1429                    headsList.get(r).setSetOffState((String) type);
1430                    break;
1431                case EDIT_COLUMN:
1432                    headsList.get(r).setIncluded(true);
1433                    SwingUtilities.invokeLater(() -> {
1434                        signalHeadEditPressed(r);
1435                    });
1436                    break;
1437                default:
1438                    break;
1439            }
1440        }
1441
1442        /**
1443         * Remove listener from Signal Head in group. Called on Delete.
1444         */
1445        public void dispose() {
1446            InstanceManager.getDefault(SignalHeadManager.class).removePropertyChangeListener(this);
1447        }
1448    }
1449
1450    JmriJFrame signalHeadEditFrame = null;
1451
1452    /**
1453     * Open an editor to set the details of a Signal Head as part of a Signal
1454     * Group when user clicks the Edit button in the Signal Head table in the
1455     * lower half of the Edit Signal Group pane.
1456     * (renamed from signalEditPressed in 4.7.1 to explain what's in here)
1457     *
1458     * @see SignalGroupSubTableAction#editHead(SignalGroup, String)
1459     * SignalGroupSubTableAction.editHead
1460     *
1461     * @param row Index of line clicked in the displayed Signal Head table
1462     */
1463    void signalHeadEditPressed(int row) {
1464        if (curSignalGroup == null) {
1465            log.debug("From signalHeadCreatePressed");
1466            if (!_autoSystemName.isSelected()) { // when creating a new Group with autoSystemName, allow empty sName field
1467                if (!checkNewNamesOK()) {
1468                    log.debug("signalHeadEditPressed: checkNewNamesOK = false");
1469                    return;
1470                }
1471            }
1472            if (!checkValidSignalMast()) {
1473                return;
1474            }
1475            updatePressed(null, true, false);
1476            // Read new entries provided in the Add pane before opening the Edit Signal Head subpane
1477        }
1478        if (!curSignalGroup.isHeadIncluded(_SignalGroupHeadModel.getBean(row))) {
1479            curSignalGroup.addSignalHead(_SignalGroupHeadModel.getBean(row));
1480        }
1481        _SignalGroupHeadModel.fireTableDataChanged();
1482        log.debug("signalHeadEditPressed: opening sbaTableAction for edit");
1483        SignalGroupSubTableAction editSignalHead = new SignalGroupSubTableAction();
1484        // calls separate class file SignalGroupSubTableAction to edit details for Signal Head
1485        editSignalHead.editHead(curSignalGroup, _SignalGroupHeadModel.getDisplayName(row));
1486    }
1487
1488    private boolean showAll = true; // false indicates: show only included Signal Masts & SingleTO Heads
1489
1490    private static int ROW_HEIGHT;
1491
1492    private static String[] COLUMN_NAMES = { // used in class SignalGroupOutputModel (Turnouts and Sensors)
1493        Bundle.getMessage("ColumnSystemName"),
1494        Bundle.getMessage("ColumnUserName"),
1495        Bundle.getMessage("Include"),
1496        Bundle.getMessage("ColumnLabelSetState")
1497    };
1498    private static String[] COLUMN_SIG_NAMES = { // used in class SignalGroupSignalHeadModel
1499        Bundle.getMessage("ColumnSystemName"),
1500        Bundle.getMessage("ColumnUserName"),
1501        Bundle.getMessage("Include"),
1502        Bundle.getMessage("OnAppearance"),
1503        Bundle.getMessage("OffAppearance"),
1504        "" // No label above last (Edit) column
1505    };
1506
1507    private static String[] signalStates = new String[]{Bundle.getMessage("SignalHeadStateDark"), Bundle.getMessage("SignalHeadStateRed"), Bundle.getMessage("SignalHeadStateYellow"), Bundle.getMessage("SignalHeadStateGreen"), Bundle.getMessage("SignalHeadStateLunar")};
1508    private static int[] signalStatesValues = new int[]{SignalHead.DARK, SignalHead.RED, SignalHead.YELLOW, SignalHead.GREEN, SignalHead.LUNAR};
1509
1510    private ArrayList<SignalGroupSignalHead> _signalHeadsList;        // array of all single output signal heads
1511    private ArrayList<SignalGroupSignalHead> _includedSignalHeadsList; // subset of heads included in sh table
1512
1513    private ArrayList<SignalMastAspect> _mastAspectsList;        // array of all valid aspects for the main signal mast
1514    private ArrayList<SignalMastAspect> _includedMastAspectsList; // subset of aspects included in asp table
1515
1516    /**
1517     * Class to store definition of a Signal Head as part of a Signal Group.
1518     * Includes properties for what to display (renamed from SignalGroupSignal
1519     * in 4.7.1 to explain what's in here)
1520     */
1521    private static class SignalGroupSignalHead {
1522
1523        SignalHead _signalHead = null;
1524        boolean _included;
1525
1526        /**
1527         * Create an object to hold name and configuration of a Signal Head as
1528         * part of a Signal Group.
1529         * Filters only existing Single Turnout Signal
1530         * Heads from the loaded configuration.
1531         * Used while editing Signal Groups.
1532         * Contains whether it is included in a group, the On state and Off
1533         * state
1534         *
1535         * @param sysName  System Name of the grouphead
1536         * @param userName Optional User Name
1537         */
1538        SignalGroupSignalHead(String sysName, String userName) {
1539            _included = false;
1540            SignalHead anySigHead = InstanceManager.getDefault(SignalHeadManager.class).getBySystemName(sysName);
1541            if (anySigHead != null) {
1542                if (anySigHead.getClass().getName().contains("SingleTurnoutSignalHead")) {
1543                    jmri.implementation.SingleTurnoutSignalHead oneSigHead = (jmri.implementation.SingleTurnoutSignalHead) InstanceManager.getDefault(SignalHeadManager.class).getBySystemName(sysName);
1544                    if (oneSigHead != null) {
1545                        _onState = oneSigHead.getOnAppearance();
1546                        _offState = oneSigHead.getOffAppearance();
1547                        _signalHead = oneSigHead;
1548                    } else {
1549                        log.error("SignalGroupSignalHead: Failed to get oneSigHead head {}", sysName);
1550                    }
1551                }
1552            } else {
1553                log.error("SignalGroupSignalHead: Failed to get signal head {}", sysName);
1554            }
1555
1556        }
1557
1558        SignalHead getBean() {
1559            return _signalHead;
1560        }
1561
1562        String getSysName() {
1563            return _signalHead.getSystemName();
1564        }
1565
1566        String getUserName() {
1567            return _signalHead.getUserName();
1568        }
1569
1570        boolean isIncluded() {
1571            return _included;
1572        }
1573
1574        void setIncluded(boolean include) {
1575            _included = include;
1576        }
1577
1578        /**
1579         * Retrieve On setting for Signal Head in Signal Group. Should match
1580         * entries in setOnState()
1581         *
1582         * @return localized string as the name for the Signal Head Appearance
1583         *         when this head is On
1584         */
1585        String getOnState() {
1586            switch (_onState) {
1587                case SignalHead.DARK:
1588                    return Bundle.getMessage("SignalHeadStateDark");
1589                case SignalHead.RED:
1590                    return Bundle.getMessage("SignalHeadStateRed");
1591                case SignalHead.YELLOW:
1592                    return Bundle.getMessage("SignalHeadStateYellow");
1593                case SignalHead.GREEN:
1594                    return Bundle.getMessage("SignalHeadStateGreen");
1595                case SignalHead.LUNAR:
1596                    return Bundle.getMessage("SignalHeadStateLunar");
1597                case SignalHead.FLASHRED:
1598                    return Bundle.getMessage("SignalHeadStateFlashingRed");
1599                case SignalHead.FLASHYELLOW:
1600                    return Bundle.getMessage("SignalHeadStateFlashingYellow");
1601                case SignalHead.FLASHGREEN:
1602                    return Bundle.getMessage("SignalHeadStateFlashingGreen");
1603                case SignalHead.FLASHLUNAR:
1604                    return Bundle.getMessage("SignalHeadStateFlashingLunar");
1605                default:
1606                    // fall through
1607                    break;
1608            }
1609            return "";
1610        }
1611
1612        /**
1613         * Retrieve Off setting for Signal Head in Signal Group. Should match
1614         * entries in setOffState()
1615         *
1616         * @return localized string as the name for the Signal Head Appearance
1617         *         when this head is Off
1618         */
1619        String getOffState() {
1620            switch (_offState) {
1621                case SignalHead.DARK:
1622                    return Bundle.getMessage("SignalHeadStateDark");
1623                case SignalHead.RED:
1624                    return Bundle.getMessage("SignalHeadStateRed");
1625                case SignalHead.YELLOW:
1626                    return Bundle.getMessage("SignalHeadStateYellow");
1627                case SignalHead.GREEN:
1628                    return Bundle.getMessage("SignalHeadStateGreen");
1629                case SignalHead.LUNAR:
1630                    return Bundle.getMessage("SignalHeadStateLunar");
1631                case SignalHead.FLASHRED:
1632                    return Bundle.getMessage("SignalHeadStateFlashingRed");
1633                case SignalHead.FLASHYELLOW:
1634                    return Bundle.getMessage("SignalHeadStateFlashingYellow");
1635                case SignalHead.FLASHGREEN:
1636                    return Bundle.getMessage("SignalHeadStateFlashingGreen");
1637                case SignalHead.FLASHLUNAR:
1638                    return Bundle.getMessage("SignalHeadStateFlashingLunar");
1639                default:
1640                    // fall through
1641                    break;
1642            }
1643            return "";
1644        }
1645
1646        int getOnStateInt() {
1647            return _onState;
1648        }
1649
1650        int getOffStateInt() {
1651            return _offState;
1652        }
1653
1654        /**
1655         * Store On setting for Signal Head in Signal Group. Should match
1656         * entries in getOnState()
1657         *
1658         * @param state Localized name for the Signal Head Appearance when this head
1659         *                  is On
1660         */
1661        void setSetOnState(String state) {
1662            if (state.equals(Bundle.getMessage("SignalHeadStateDark"))) {
1663                _onState = SignalHead.DARK;
1664            } else if (state.equals(Bundle.getMessage("SignalHeadStateRed"))) {
1665                _onState = SignalHead.RED;
1666            } else if (state.equals(Bundle.getMessage("SignalHeadStateYellow"))) {
1667                _onState = SignalHead.YELLOW;
1668            } else if (state.equals(Bundle.getMessage("SignalHeadStateGreen"))) {
1669                _onState = SignalHead.GREEN;
1670            } else if (state.equals(Bundle.getMessage("SignalHeadStateLunar"))) {
1671                _onState = SignalHead.LUNAR;
1672            } else if (state.equals(Bundle.getMessage("SignalHeadStateFlashingRed"))) {
1673                _onState = SignalHead.FLASHRED;
1674            } else if (state.equals(Bundle.getMessage("SignalHeadStateFlashingYellow"))) {
1675                _onState = SignalHead.FLASHYELLOW;
1676            } else if (state.equals(Bundle.getMessage("SignalHeadStateFlashingGreen"))) {
1677                _onState = SignalHead.FLASHGREEN;
1678            } else if (state.equals(Bundle.getMessage("SignalHeadStateFlashingLunar"))) {
1679                _onState = SignalHead.FLASHLUNAR;
1680            }
1681        }
1682
1683        /**
1684         * Store Off setting for Signal Head in Signal Group. Should match
1685         * entries in getOffState()
1686         *
1687         * @param state Localized name for the Signal Head Appearance when this head
1688         *                  is Off
1689         */
1690        void setSetOffState(String state) {
1691            if (state.equals(Bundle.getMessage("SignalHeadStateDark"))) {
1692                _offState = SignalHead.DARK;
1693            } else if (state.equals(Bundle.getMessage("SignalHeadStateRed"))) {
1694                _offState = SignalHead.RED;
1695            } else if (state.equals(Bundle.getMessage("SignalHeadStateYellow"))) {
1696                _offState = SignalHead.YELLOW;
1697            } else if (state.equals(Bundle.getMessage("SignalHeadStateGreen"))) {
1698                _offState = SignalHead.GREEN;
1699            } else if (state.equals(Bundle.getMessage("SignalHeadStateLunar"))) {
1700                _offState = SignalHead.LUNAR;
1701            } else if (state.equals(Bundle.getMessage("SignalHeadStateFlashingRed"))) {
1702                _offState = SignalHead.FLASHRED;
1703            } else if (state.equals(Bundle.getMessage("SignalHeadStateFlashingYellow"))) {
1704                _offState = SignalHead.FLASHYELLOW;
1705            } else if (state.equals(Bundle.getMessage("SignalHeadStateFlashingGreen"))) {
1706                _offState = SignalHead.FLASHGREEN;
1707            } else if (state.equals(Bundle.getMessage("SignalHeadStateFlashingLunar"))) {
1708                _offState = SignalHead.FLASHLUNAR;
1709            }
1710        }
1711
1712        int _onState = 0x00;
1713        int _offState = 0x00;
1714
1715        public void setOnState(int state) {
1716            _onState = state;
1717        }
1718
1719        public void setOffState(int state) {
1720            _offState = state;
1721        }
1722    }
1723
1724    /**
1725     * Definition of main Signal Mast in a Signal Group.
1726     */
1727    private static class SignalMastAspect {
1728
1729        SignalMastAspect(String aspect) {
1730            _aspect = aspect;
1731        }
1732
1733        boolean _include;
1734        String _aspect;
1735
1736        void setIncluded(boolean include) {
1737            _include = include;
1738        }
1739
1740        boolean isIncluded() {
1741            return _include;
1742        }
1743
1744        void setAspect(String asp) {
1745            _aspect = asp;
1746        }
1747
1748        String getAspect() {
1749            return _aspect;
1750        }
1751
1752    }
1753
1754    @Override
1755    protected String getClassName() {
1756        return SignalGroupTableAction.class.getName();
1757    }
1758
1759    @Override
1760    public String getClassDescription() {
1761        return Bundle.getMessage("TitleSignalGroupTable");
1762    }
1763
1764    @Override
1765    public void setMessagePreferencesDetails() {
1766        InstanceManager.getDefault(jmri.UserPreferencesManager.class).
1767                setPreferenceItemDetails(getClassName(), "remindSignalGroup", Bundle.getMessage("HideSaveReminder"));  // NOI18N
1768        super.setMessagePreferencesDetails();
1769    }
1770
1771    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SignalGroupTableAction.class);
1772
1773}