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