001package jmri.jmrit.signalling;
002
003import java.awt.BorderLayout;
004import java.awt.event.ActionEvent;
005import java.awt.event.ActionListener;
006import java.beans.PropertyChangeListener;
007import java.util.List;
008import java.util.Set;
009
010import javax.swing.BorderFactory;
011import javax.swing.BoxLayout;
012import javax.swing.JButton;
013import javax.swing.JLabel;
014import javax.swing.JPanel;
015import javax.swing.JScrollPane;
016import javax.swing.JTable;
017import javax.swing.JTextField;
018import javax.swing.SortOrder;
019import javax.swing.table.AbstractTableModel;
020import javax.swing.table.TableCellEditor;
021import javax.swing.table.TableRowSorter;
022
023import jmri.InstanceManager;
024import jmri.SignalMast;
025import jmri.SignalMastLogic;
026import jmri.jmrit.display.EditorManager;
027import jmri.jmrit.display.layoutEditor.LayoutBlockManager;
028import jmri.jmrit.display.layoutEditor.LayoutEditor;
029import jmri.swing.RowSorterUtil;
030import jmri.util.JmriJFrame;
031import jmri.util.swing.JmriJOptionPane;
032import jmri.util.table.ButtonEditor;
033import jmri.util.table.ButtonRenderer;
034
035/**
036 * Frame for the Signal Mast Table - Edit Logic Pane.
037 *
038 * @author Kevin Dickerson Copyright (C) 2011
039 * @author Egbert Broerse Copyright (C) 2017
040 */
041public class SignallingSourcePanel extends jmri.util.swing.JmriPanel implements PropertyChangeListener {
042
043    SignalMastLogic sml;
044    SignalMast sourceMast;
045    JLabel fixedSourceMastLabel = new JLabel();
046
047    JButton discoverPairs = new JButton(Bundle.getMessage("ButtonDiscover"));  // NOI18N
048
049    SignalMastAspectModel _AppearanceModel;
050    JScrollPane _SignalAppearanceScrollPane;
051
052    /**
053     * Create a Signalling Source configuration Pane showing a list of defined
054     * destination masts and allowing creation of new source-destination pairs
055     * as well as showing a button to start Autodetect configuration.
056     * @param sourceMast The source mast for this SML Source Pairs pane
057     */
058    public SignallingSourcePanel(final SignalMast sourceMast) {
059        super();
060        sml = InstanceManager.getDefault(jmri.SignalMastLogicManager.class).getSignalMastLogic(sourceMast);
061        this.sourceMast = sourceMast;
062        fixedSourceMastLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("SourceMast")) + " " + sourceMast.getDisplayName());  // NOI18N
063        if (sml != null) {
064            _signalMastList = sml.getDestinationList();
065        }
066
067        InstanceManager.getDefault(LayoutBlockManager.class).addPropertyChangeListener(this);
068        InstanceManager.getDefault(jmri.SignalMastLogicManager.class).addPropertyChangeListener(this);
069
070        setLayout(new BorderLayout());
071
072        JPanel header = new JPanel();
073        header.setLayout(new BoxLayout(header, BoxLayout.Y_AXIS));
074
075        JPanel sourcePanel = new JPanel();
076        sourcePanel.add(fixedSourceMastLabel);
077        header.add(sourcePanel);
078        add(header, BorderLayout.NORTH);
079
080        _AppearanceModel = new SignalMastAspectModel();
081        JTable table = new JTable(_AppearanceModel);
082        TableRowSorter<SignalMastAspectModel> sorter = new TableRowSorter<>(_AppearanceModel);
083        RowSorterUtil.setSortOrder(sorter, SignalMastAspectModel.SYSNAME_COLUMN, SortOrder.ASCENDING);
084        table.setRowSorter(sorter);
085        table.setRowSelectionAllowed(false);
086        table.setPreferredScrollableViewportSize(new java.awt.Dimension(600, 120));
087        _AppearanceModel.configureTable(table);
088        _SignalAppearanceScrollPane = new JScrollPane(table);
089        _AppearanceModel.fireTableDataChanged();
090        add(_SignalAppearanceScrollPane, BorderLayout.CENTER);
091
092        JPanel footer = new JPanel();
093
094        footer.add(discoverPairs);
095        discoverPairs.addActionListener(new ActionListener() {
096            @Override
097            public void actionPerformed(ActionEvent e) {
098                discoverPressed(e);
099            }
100        });
101
102        JButton addLogic = new JButton(Bundle.getMessage("AddLogic"));  // NOI18N
103        footer.add(addLogic);
104        addLogic.addActionListener(new ActionListener() {
105            @Override
106            public void actionPerformed(ActionEvent e) {
107                class WindowMaker implements Runnable {
108
109                    WindowMaker() {
110                    }
111
112                    @Override
113                    public void run() {
114                        SignallingAction sigLog = new SignallingAction(); // opens a frame, opens a panel in that frame
115                        sigLog.setMast(sourceMast, null);
116                        sigLog.actionPerformed(null);
117                        // unable to receive changes directly from created panel, so listen to common SML ancestor
118                    }
119                }
120                WindowMaker t = new WindowMaker();
121                javax.swing.SwingUtilities.invokeLater(t);
122            }
123        });
124
125        add(footer, BorderLayout.SOUTH);
126    }
127
128    /**
129     * Remove references to and from this object, so that it can eventually be
130     * garbage-collected.
131     */
132    @Override
133    public void dispose() {
134        InstanceManager.getDefault(LayoutBlockManager.class).removePropertyChangeListener(this);
135        InstanceManager.getDefault(jmri.SignalMastLogicManager.class).removePropertyChangeListener(this);
136        super.dispose();
137    }
138
139    JmriJFrame signalMastLogicFrame = null;
140    JLabel sourceLabel = new JLabel();
141
142    /**
143     * Respond to the Discover button being pressed.
144     * Check whether AdvancedRouting is turned on and any Layout Editor Panels
145     * are present. For each LE Panel, call discoverSignallingDest()
146     * {@link jmri.SignalMastLogicManager#discoverSignallingDest(SignalMast, LayoutEditor)}
147     * @param e The button event
148     */
149    void discoverPressed(ActionEvent e) {
150        if (!InstanceManager.getDefault(LayoutBlockManager.class).isAdvancedRoutingEnabled()) {
151            int response = JmriJOptionPane.showConfirmDialog(this, Bundle.getMessage("EnableLayoutBlockRouting"),
152                Bundle.getMessage("QuestionTitle"), JmriJOptionPane.YES_NO_OPTION);
153            if ( response == JmriJOptionPane.YES_OPTION ) {
154                InstanceManager.getDefault(LayoutBlockManager.class).enableAdvancedRouting(true);
155                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("LayoutBlockRoutingEnabledShort"));  // NOI18N
156            }
157        }
158
159        Set<LayoutEditor> layout = InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class);
160        if (!layout.isEmpty()) {
161            signalMastLogicFrame = new JmriJFrame(Bundle.getMessage("DiscoverMastsTitle"), false, false);  // NOI18N
162            signalMastLogicFrame.setPreferredSize(null);
163            JPanel panel1 = new JPanel();
164            sourceLabel = new JLabel(Bundle.getMessage("DiscoveringMasts"));  // NOI18N
165            sourceLabel.setBorder(BorderFactory.createEmptyBorder(5, 20, 5, 20));
166            panel1.add(sourceLabel);
167            signalMastLogicFrame.add(sourceLabel);
168            signalMastLogicFrame.pack();
169            signalMastLogicFrame.setVisible(true);
170
171            for (LayoutEditor editor : layout) {
172                try {
173                    InstanceManager.getDefault(jmri.SignalMastLogicManager.class).discoverSignallingDest(sourceMast, editor);
174                } catch (jmri.JmriException ex) {
175                    signalMastLogicFrame.setVisible(false);
176                    JmriJOptionPane.showMessageDialog(this, ex.toString());
177                }
178            }
179            signalMastLogicFrame.setVisible(false);
180            signalMastLogicFrame.dispose();
181            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("GenComplete"));  // NOI18N
182        } else {
183            // don't take the trouble of searching
184            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("GenSkipped"));  // NOI18N
185        }
186    }
187
188    /**
189     * Listen for property changes in the SML that's being configured.
190     * @param e The button event
191     */
192    @Override
193    public void propertyChange(java.beans.PropertyChangeEvent e) {
194        if (e.getPropertyName().equals("autoSignalMastGenerateComplete")) {  // NOI18N
195            if (sml == null) {
196                updateDetails();
197            }
198            log.debug("Generate complete for a LE panel ({}): mast = {}", this.hashCode(), sourceMast);
199        }
200        if (e.getPropertyName().equals("advancedRoutingEnabled")) {  // NOI18N
201            boolean newValue = (Boolean) e.getNewValue();
202            discoverPairs.setEnabled(newValue);
203        }
204        log.debug("SSP 173 Event: {}; Source: {}", e.getPropertyName(), e.toString()); // doesn't get notified, newDestination  // NOI18N
205        if (e.getPropertyName().equals("length")) { // redraw the Pairs table  // NOI18N
206            updateDetails();
207        }
208    }
209
210    private List<SignalMast> _signalMastList;
211
212    /**
213     * Refresh the list of destination Signal Masts available for edit in the current SML.
214     */
215    private void updateDetails() {
216        SignalMastLogic old = sml;
217        sml = InstanceManager.getDefault(jmri.SignalMastLogicManager.class).getSignalMastLogic(sourceMast);
218        if (sml != null) {
219            _signalMastList = sml.getDestinationList();
220            _AppearanceModel.updateSignalMastLogic(old, sml);
221        }
222    }
223
224    /**
225     * TableModel to store SML control Signal Masts and their Set To Aspect.
226     */
227    public class SignalMastAspectModel extends AbstractTableModel implements PropertyChangeListener {
228
229        SignalMastAspectModel() {
230            super();
231            if (sml != null) {
232                sml.addPropertyChangeListener(this); // pick up creation of a new pair in the sml
233            }
234        }
235
236        void updateSignalMastLogic(SignalMastLogic smlOld, SignalMastLogic smlNew) {
237            if (smlOld != null) {
238                smlOld.removePropertyChangeListener(this);
239            }
240            if (smlNew != null) {
241                smlNew.addPropertyChangeListener(this);
242            }
243            fireTableDataChanged();
244        }
245
246        @Override
247        public Class<?> getColumnClass(int c) {
248            if (c == ACTIVE_COLUMN) {
249                return Boolean.class;
250            }
251            if (c == ENABLE_COLUMN) {
252                return Boolean.class;
253            }
254            if (c == EDIT_COLUMN) {
255                return JButton.class;
256            }
257            if (c == DEL_COLUMN) {
258                return JButton.class;
259            }
260            return String.class;
261        }
262
263        public void configureTable(JTable table) {
264            // allow reordering of the columns
265            table.getTableHeader().setReorderingAllowed(true);
266
267            // have to shut off autoResizeMode to get horizontal scroll to work (JavaSwing p 541)
268            table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
269
270            // resize columns as requested
271            for (int i = 0; i < table.getColumnCount(); i++) {
272                int width = getPreferredWidth(i);
273                table.getColumnModel().getColumn(i).setPreferredWidth(width);
274            }
275            table.sizeColumnsToFit(-1);
276
277            configEditColumn(table);
278        }
279
280        @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "DB_DUPLICATE_SWITCH_CLAUSES",
281                                justification="better to keep cases in column order rather than to combine")
282        public int getPreferredWidth(int col) {
283            switch (col) {
284                case SYSNAME_COLUMN:
285                    return new JTextField(15).getPreferredSize().width;
286                case ENABLE_COLUMN:
287                case ACTIVE_COLUMN:
288                    return new JTextField(5).getPreferredSize().width;
289                case USERNAME_COLUMN:
290                    return new JTextField(15).getPreferredSize().width;
291                case EDIT_COLUMN: // not actually used due to the configureTable, setColumnToHoldButton, configureButton
292                    return new JTextField(22).getPreferredSize().width;
293                case DEL_COLUMN: // not actually used due to the configureTable, setColumnToHoldButton, configureButton
294                    return new JTextField(22).getPreferredSize().width;
295                default:
296                    log.warn("Unexpected column in getPreferredWidth: {}", col);  // NOI18N
297                    return new JTextField(8).getPreferredSize().width;
298            }
299        }
300
301        @Override
302        public String getColumnName(int col) {
303            if (col == USERNAME_COLUMN) {
304                return Bundle.getMessage("ColumnUserName");  // NOI18N
305            }
306            if (col == SYSNAME_COLUMN) {
307                return Bundle.getMessage("DestMast");  // NOI18N
308            }
309            if (col == ACTIVE_COLUMN) {
310                return Bundle.getMessage("SensorStateActive"); // "Active"  // NOI18N
311            }
312            if (col == ENABLE_COLUMN) {
313                return Bundle.getMessage("ColumnHeadEnabled");  // NOI18N
314            }
315            if (col == EDIT_COLUMN) {
316                return ""; //no title above Edit buttons
317            }
318            if (col == DEL_COLUMN) {
319                return ""; //no title above Delete buttons
320            }
321            return "";
322        }
323
324        /**
325         * Remove references to and from this object, so that it can eventually be
326         * garbage-collected.
327         */
328        public void dispose() {
329            if (sml != null) {
330                sml.removePropertyChangeListener(this);
331            }
332        }
333
334        /**
335         * Listen for changes to specific properties of the displayed Signal Masts.
336         * @param e The ChangeEvent heard
337         */
338        @Override
339        public void propertyChange(java.beans.PropertyChangeEvent e) {
340            if (e.getPropertyName().equals("length")) {  // NOI18N
341                // should pick up adding a new destination mast, but doesn't refresh table by itself
342                _signalMastList = sml.getDestinationList();
343                int length = (Integer) e.getNewValue();
344                if (length == 0) {
345                    sml.removePropertyChangeListener(this);
346                    sml = null;
347                }
348                fireTableDataChanged();
349            } else if (e.getPropertyName().equals("updatedDestination")) {  // NOI18N
350                // a new NamedBean is available in the manager
351                _signalMastList = sml.getDestinationList();
352                fireTableDataChanged();
353            } else if ((e.getPropertyName().equals("state")) || (e.getPropertyName().equals("Enabled"))) {  // NOI18N
354                fireTableDataChanged();
355                fireTableRowsUpdated(0, _signalMastList.size()-1);
356            }
357            log.debug("SSP 310 Event: {}", e.getPropertyName());  // NOI18N
358        }
359
360        /**
361         * Display buttons in 2 columns of the manual control signal masts table.
362         * @param table The control signal mast table to be configured
363         */
364        protected void configEditColumn(JTable table) {
365            // have the Delete column hold a button
366            setColumnToHoldButton(table, EDIT_COLUMN,
367                    new JButton(Bundle.getMessage("ButtonEdit")));  // NOI18N
368            setColumnToHoldButton(table, DEL_COLUMN,
369                    new JButton(Bundle.getMessage("ButtonDelete")));  // NOI18N
370        }
371
372        /**
373         * Helper function for {@link #configEditColumn(JTable)}.
374         * @param table  The control signal mast table to be configured
375         * @param column Index for the column to put the button in
376         * @param sample JButton to put there
377         */
378        protected void setColumnToHoldButton(JTable table, int column, JButton sample) {
379            // install a button renderer & editor
380            ButtonRenderer buttonRenderer = new ButtonRenderer();
381            table.setDefaultRenderer(JButton.class, buttonRenderer);
382            TableCellEditor buttonEditor = new ButtonEditor(new JButton());
383            table.setDefaultEditor(JButton.class, buttonEditor);
384            // ensure the table rows, columns have enough room for buttons
385            table.setRowHeight(sample.getPreferredSize().height);
386            table.getColumnModel().getColumn(column)
387                    .setPreferredWidth((sample.getPreferredSize().width) + 4);
388        }
389
390        /**
391         * Get the number of columns in the signal masts table.
392         * @return Fixed value of 6
393         */
394        @Override
395        public int getColumnCount() {
396            return 6;
397        }
398
399        /**
400         * Query whether the cells in a table column should respond to clicks.
401         * @param r Index for the cell row
402         * @param c Index for the cell column
403         */
404        @Override
405        public boolean isCellEditable(int r, int c) {
406            if (c == EDIT_COLUMN) {
407                return true;
408            }
409            if (c == DEL_COLUMN) {
410                return true;
411            }
412            if (c == ENABLE_COLUMN) {
413                return true;
414            }
415            return ((c == USERNAME_COLUMN));
416        }
417
418        /**
419         * Respond to the Edit Logic button being clicked.
420         * @param r Index for the cell row
421         */
422        protected void editPair(int r) {
423
424            class WindowMaker implements Runnable {
425
426                int row;
427
428                WindowMaker(int r) {
429                    row = r;
430                }
431
432                @Override
433                public void run() {
434                    log.debug("SML Edit existing logic started");  // NOI18N
435                    SignallingAction sigLog = new SignallingAction();
436                    sigLog.setMast(sourceMast, _signalMastList.get(row));
437                    sigLog.actionPerformed(null);
438                    // Note: we cannot tell if Edit pair was cancelled
439                }
440            }
441            WindowMaker t = new WindowMaker(r);
442            javax.swing.SwingUtilities.invokeLater(t);
443        }
444
445        protected void deletePair(int r) {
446            InstanceManager.getDefault(jmri.SignalMastLogicManager.class).removeSignalMastLogic(sml, _signalMastList.get(r));
447        }
448
449        public static final int SYSNAME_COLUMN = 0;
450        public static final int USERNAME_COLUMN = 1;
451        public static final int ACTIVE_COLUMN = 2;
452        public static final int ENABLE_COLUMN = 3;
453        public static final int EDIT_COLUMN = 4;
454        public static final int DEL_COLUMN = 5;
455
456        public void setSetToState(String x) {
457        }
458
459        /**
460         * Get the number of Included signal masts for this SML.
461         */
462        @Override
463        public int getRowCount() {
464            if (_signalMastList == null) {
465                return 0;
466            }
467            return _signalMastList.size();
468        }
469
470        /**
471         * Retrieve the contents to display in a cell in the table, in terms of model
472         * @param r index for the cell row
473         * @param c index for the cell column
474         * @return The value (text) stored in the cell
475         */
476        @Override
477        public Object getValueAt(int r, int c) {
478            if (sml == null) {
479                return null;
480            }
481            // some error checking
482            if (r >= _signalMastList.size()) {
483                log.debug("row is greater than turnout list size");  // NOI18N
484                return null;
485            }
486            switch (c) {
487                case USERNAME_COLUMN:
488                    return _signalMastList.get(r).getUserName();
489                case SYSNAME_COLUMN:  // slot number
490                    return _signalMastList.get(r).getSystemName();
491                case ACTIVE_COLUMN:
492                    return sml.isActive(_signalMastList.get(r));
493                case ENABLE_COLUMN:
494                    return sml.isEnabled(_signalMastList.get(r));
495                case EDIT_COLUMN:
496                    return Bundle.getMessage("ButtonEdit");  // NOI18N
497                case DEL_COLUMN:
498                    return Bundle.getMessage("ButtonDelete");  // NOI18N
499                default:
500                    return null;
501            }
502        }
503
504        /**
505         * Process the contents from a table cell, in terms of model
506         * @param type the object type of the cell contents
507         * @param r index for the cell row
508         * @param c index for the cell column
509         */
510        @Override
511        public void setValueAt(Object type, int r, int c) {
512            if (c == EDIT_COLUMN) {
513                editPair(r);
514            } else if (c == DEL_COLUMN) {
515                deletePair(r);
516            } else if (c == ENABLE_COLUMN) {
517                boolean b = ((Boolean) type).booleanValue();
518                if (b) {
519                    sml.setEnabled(_signalMastList.get(r));
520                } else {
521                    sml.setDisabled(_signalMastList.get(r));
522                }
523
524            }
525        }
526    }
527
528    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SignallingSourcePanel.class);
529
530}