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