001package jmri.jmrit.beantable;
002
003import java.awt.*;
004import java.awt.event.ActionEvent;
005import java.awt.event.ActionListener;
006import java.beans.PropertyChangeEvent;
007import java.beans.PropertyChangeListener;
008import javax.annotation.Nonnull;
009import javax.swing.*;
010
011import jmri.*;
012import jmri.jmrit.beantable.oblock.*;
013import jmri.jmrit.logix.OBlock;
014import jmri.jmrit.logix.OBlockManager;
015import jmri.jmrit.logix.PortalManager;
016import jmri.util.JmriJFrame;
017import jmri.util.gui.GuiLafPreferencesManager;
018import jmri.util.swing.JmriJOptionPane;
019
020/**
021 * GUI to define OBlocks, OPaths and Portals. Overrides some of the AbstractTableAction methods as this is a hybrid pane.
022 * Relies on {@link jmri.jmrit.beantable.oblock.TableFrames}.
023 *
024 * @author Pete Cressman (C) 2009, 2010
025 * @author Egbert Broerse (C) 2020
026 */
027public class OBlockTableAction extends AbstractTableAction<OBlock> implements PropertyChangeListener {
028
029    // for tabs or desktop interface
030    protected boolean _tabbed = false; // updated from prefs
031    protected JPanel dataPanel;
032    protected JTabbedPane dataTabs;
033    protected boolean init = false;
034
035    // basic table models
036    OBlockTableModel oblocks;
037    PortalTableModel portals;
038    SignalTableModel signals;
039    BlockPortalTableModel blockportals;
040    // tables created on demand inside TableFrames:
041    // - BlockPathTable(block)
042    // - PathTurnoutTable(block)
043
044    @Nonnull
045    protected OBlockManager oblockManager = InstanceManager.getDefault(jmri.jmrit.logix.OBlockManager.class);
046    @Nonnull
047    protected PortalManager portalManager = InstanceManager.getDefault(jmri.jmrit.logix.PortalManager.class);
048
049    TableFrames tf;
050    OBlockTableFrame otf;
051    OBlockTablePanel otp;
052
053    // edit frames
054    //OBlockEditFrame oblockFrame; // instead we use NewBean util + Edit
055    PortalEditFrame portalFrame;
056    SignalEditFrame signalFrame;
057    // on demand frames
058    //    PathTurnoutFrame ptFrame; created from TableFrames
059    //    BlockPathEditFrame bpFrame; created from TableFrames
060
061    /**
062     * Create an action with a specific title.
063     * <p>
064     * Note that the argument is the Action title, not the title of the
065     * resulting frame. Perhaps this should be changed?
066     *
067     * @param actionName title of the action
068     */
069    public OBlockTableAction(String actionName) {
070        super(actionName);
071        includeAddButton = false; // not required per se as we override the actionPerformed method
072    }
073
074    /**
075     * Default constructor
076     */
077    public OBlockTableAction() {
078        this(Bundle.getMessage("TitleOBlockTable"));
079    }
080
081    /**
082     * Configure managers for all tabs on OBlocks table pane.
083     * @param om the manager to assign
084     */
085    @Override
086    public void setManager(@Nonnull Manager<OBlock> om) {
087        oblockManager.removePropertyChangeListener(this);
088        if (om instanceof OBlockManager) {
089            oblockManager = (OBlockManager) om;
090            if (m != null) { // model
091                m.setManager(oblockManager);
092            }
093        }
094        oblockManager.addPropertyChangeListener(this);
095    }
096
097    // add the 3 buttons to add new OBlock, Portal, Signal
098    @Override
099    public void addToFrame(@Nonnull BeanTableFrame<OBlock> f) {
100        JButton addOblockButton = new JButton(Bundle.getMessage("ButtonAddOBlock"));
101        otp.addToBottomBox(addOblockButton);
102        addOblockButton.addActionListener(this::addOBlockPressed);
103
104        JButton addPortalButton = new JButton(Bundle.getMessage("ButtonAddPortal"));
105        otp.addToBottomBox(addPortalButton);
106        addPortalButton.addActionListener(this::addPortalPressed);
107
108        JButton addSignalButton = new JButton(Bundle.getMessage("ButtonAddSignal"));
109        otp.addToBottomBox(addSignalButton);
110        addSignalButton.addActionListener(this::addSignalPressed);
111    }
112
113    /**
114     * Open OBlock tables action handler.
115     * @see jmri.jmrit.beantable.oblock.TableFrames
116     * @param e menu action
117     */
118    @Override
119    public void actionPerformed(ActionEvent e) {
120        _tabbed = InstanceManager.getDefault(GuiLafPreferencesManager.class).isOblockEditTabbed();
121        initTableFrames();
122    }
123
124    private void initTableFrames() {
125        // initialise core OBlock Edit functionality
126        tf = new TableFrames(); // tf contains OBlock Edit methods and links to tableDataModels, is a JmriJFrame that must be hidden
127
128        if (_tabbed) { // add the tables on a JTabbedPane, choose in Preferences > Display > GUI
129            log.debug("Tabbed starting");
130            // create the JTable model, with changes for specific NamedBean
131            createModel();
132            // create the frame
133            otf = new OBlockTableFrame(otp, helpTarget()) {
134
135                /**
136                 * Include "Add OBlock..." and "Add XYZ..." buttons
137                 */
138                @Override
139                void extras() {
140                    addToFrame(this); //creates multiple sets, wrong level to call
141                }
142            };
143            setTitle();
144
145            //tf.setParentFrame(otf); // needed?
146            //tf.makePrivateWindow(); // prevents tf "OBlock Tables..." to show up in the Windows menu
147            //tf.setVisible(false); // hide the TableFrames container when _tabbed
148
149            otf.pack();
150            otf.setVisible(true);
151        } else {
152            tf.initComponents();
153            // original simulated desktop interface is created in tf.initComponents() and takes care of itself if !_tabbed
154            // only required for _desktop, creates InternalFrames
155            //tf.setVisible(true);
156        }
157    }
158
159    /**
160     * Create the JTable DataModel, along with the extra stuff for this specific NamedBean type.
161     * Is directly called to prepare the Tables &gt; OBlock Table entry in the left sidebar list, bypassing actionPerformed(a)
162     */
163    @Override
164    protected void createModel() { // Tabbed
165        if (tf == null) {
166            initTableFrames();
167        }
168        oblocks = tf.getOblockTableModel();
169        portals = tf.getPortalTableModel();
170        signals = tf.getSignalTableModel();
171        blockportals = tf.getPortalXRefTableModel();
172
173        otp = new OBlockTablePanel(oblocks, portals, signals, blockportals, tf, helpTarget());
174
175//        if (f == null) {
176//            f = new OBlockTableFrame(otp, helpTarget());
177//        }
178//        setMenuBar(f); // comes after the Help menu is added by f = new
179        // BeanTableFrame(etc.) handled in stand alone application
180//        setTitle(); // TODO see if some of this is required or should be turned off to prevent/hide the stray JFrame that opens
181//        addToFrame(f);
182//        f.pack();
183//        f.setVisible(true); <--- another empty pane!
184
185        init = true;
186    }
187
188    @Override
189    public JPanel getPanel() {
190        createModel();
191        return otp;
192    }
193
194    /**
195     * Include the correct title.
196     */
197    @Override
198    protected void setTitle() {
199        if (_tabbed && otf != null) {
200            otf.setTitle(Bundle.getMessage("TitleOBlocksTabbedFrame"));
201        }
202    }
203
204    @Override
205    public void setMenuBar(BeanTableFrame<OBlock> f) {
206        if (_tabbed) {
207            //final JmriJFrame finalF = f;   // needed for anonymous ActionListener class on dialogs, see TurnoutTableAction ?
208            JMenuBar menuBar = f.getJMenuBar();
209            if (menuBar == null) {
210                log.debug("NULL MenuBar");
211                return;
212            }
213            MenuElement[] subElements;
214            JMenu fileMenu = null;
215            for (int i = 0; i < menuBar.getMenuCount(); i++) {
216                if (menuBar.getComponent(i) instanceof JMenu) {
217                    if (((JMenu) menuBar.getComponent(i)).getText().equals(Bundle.getMessage("MenuFile"))) {
218                        fileMenu = menuBar.getMenu(i);
219                    }
220                }
221            }
222            if (fileMenu == null) {
223                log.debug("NULL FileMenu");
224                return;
225            }
226            subElements = fileMenu.getSubElements();
227            for (MenuElement subElement : subElements) {
228                MenuElement[] popsubElements = subElement.getSubElements();
229                for (MenuElement popsubElement : popsubElements) {
230                    if (popsubElement instanceof JMenuItem) {
231                        if (((JMenuItem) popsubElement).getText().equals(Bundle.getMessage("PrintTable"))) {
232                            JMenuItem printMenu = (JMenuItem) popsubElement;
233                            fileMenu.remove(printMenu);
234                            break;
235                        }
236                    }
237                }
238            }
239            fileMenu.add(otp.getPrintItem());
240
241            menuBar.add(otp.getOptionMenu());
242            menuBar.add(otp.getTablesMenu());
243            log.debug("setMenuBar for OBLOCKS");
244
245            // check for menu (copied from TurnoutTableAction)
246//            boolean menuAbsent = true;
247//            for (int m = 0; m < menuBar.getMenuCount(); ++m) {
248//                String name = menuBar.getMenu(m).getAccessibleContext().getAccessibleName();
249//                if (name.equals(Bundle.getMessage("MenuOptions"))) {
250//                    // using first menu for check, should be identical to next JMenu Bundle
251//                    menuAbsent = false;
252//                    break;
253//                }
254//            }
255//            if (menuAbsent) { // create it
256//                int pos = menuBar.getMenuCount() - 1; // count the number of menus to insert the TableMenu before 'Window' and 'Help'
257//                int offset = 1;
258//                log.debug("setMenuBar number of menu items = {}", pos);
259//                for (int i = 0; i <= pos; i++) {
260//                    if (menuBar.getComponent(i) instanceof JMenu) {
261//                        if (((JMenu) menuBar.getComponent(i)).getText().equals(Bundle.getMessage("MenuHelp"))) {
262//                            offset = -1; // correct for use as part of ListedTableAction where the Help Menu is not yet present
263//                        }
264//                    }
265//                }
266                // add separate items, actionhandlers? next 2 menuItem examples copied from TurnoutTableAction
267
268        //            JMenuItem item = new JMenuItem(Bundle.getMessage("TurnoutAutomationMenuItemEdit"));
269        //            opsMenu.add(item);
270        //            item.addActionListener(new ActionListener() {
271        //                @Override
272        //                public void actionPerformed(ActionEvent e) {
273        //                    new TurnoutOperationFrame(finalF);
274        //                }
275        //            });
276        //            menuBar.add(opsMenu, pos + offset); // test
277        //
278        //            JMenu speedMenu = new JMenu(Bundle.getMessage("SpeedsMenu"));
279        //            item = new JMenuItem(Bundle.getMessage("SpeedsMenuItemDefaults"));
280        //            speedMenu.add(item);
281        //            item.addActionListener(new ActionListener() {
282        //                @Override
283        //                public void actionPerformed(ActionEvent e) {
284        //                    //setDefaultSpeeds(finalF);
285        //                }
286        //            });
287        //            menuBar.add(speedMenu, pos + offset + 1); // add this menu to the right of the previous
288        //    }
289            f.addHelpMenu("package.jmri.jmrit.beantable.OBlockTable", true);
290        }
291    }
292
293    JTextField startAddress = new JTextField(10);
294    JTextField userName = new JTextField(40);
295    SpinnerNumberModel rangeSpinner = new SpinnerNumberModel(1, 1, 100, 1); // maximum 100 items
296    JSpinner numberToAddSpinner = new JSpinner(rangeSpinner);
297    JCheckBox rangeBox = new JCheckBox(Bundle.getMessage("AddRangeBox"));
298    JCheckBox autoSystemNameBox = new JCheckBox(Bundle.getMessage("LabelAutoSysName"));
299    JLabel statusBar = new JLabel(Bundle.getMessage("HardwareAddStatusEnter"), JLabel.LEADING);
300    jmri.UserPreferencesManager pref;
301    JmriJFrame addOBlockFrame = null;
302    // for prefs persistence
303    String systemNameAuto = this.getClass().getName() + ".AutoSystemName";
304
305    // Three [Addx...] buttons on tabbed bottom box handlers
306    
307    @Override
308    protected void addPressed(ActionEvent e) {
309        log.warn("This should not have happened");
310    }
311
312    protected void addOBlockPressed(ActionEvent e) {
313        pref = jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class);
314
315        if (addOBlockFrame == null) {
316            addOBlockFrame = new JmriJFrame(Bundle.getMessage("TitleAddOBlock"), false, true);
317            addOBlockFrame.addHelpMenu("package.jmri.jmrit.beantable.OBlockTable", true);
318            addOBlockFrame.getContentPane().setLayout(new BoxLayout(addOBlockFrame.getContentPane(), BoxLayout.Y_AXIS));
319
320            ActionListener okListener = this::createObPressed;
321            ActionListener cancelListener = this::cancelObPressed;
322
323            AddNewBeanPanel anbp = new AddNewBeanPanel(startAddress, userName, numberToAddSpinner, rangeBox, autoSystemNameBox, "ButtonCreate", okListener, cancelListener, statusBar);
324            addOBlockFrame.add(anbp);
325            addOBlockFrame.getRootPane().setDefaultButton(anbp.ok);
326            addOBlockFrame.setEscapeKeyClosesWindow(true);
327            startAddress.setToolTipText(Bundle.getMessage("SysNameToolTip", "OB")); // override tooltip with bean specific letter
328        }
329        startAddress.setBackground(Color.white);
330        // reset status bar text
331        status(Bundle.getMessage("AddBeanStatusEnter"), false);
332        if (pref.getSimplePreferenceState(systemNameAuto)) {
333            autoSystemNameBox.setSelected(true);
334        }
335        addOBlockFrame.pack();
336        addOBlockFrame.setVisible(true);
337    }
338
339    void cancelObPressed(ActionEvent e) {
340        addOBlockFrame.setVisible(false);
341        addOBlockFrame.dispose();
342        addOBlockFrame = null;
343    }
344
345    /**
346     * Respond to Create new OBlock button pressed on Add OBlock pane.
347     * Adapted from {@link MemoryTableAction#addPressed(ActionEvent)}
348     *
349     * @param e the click event
350     */
351    void createObPressed(ActionEvent e) {
352        int numberOfOblocks = 1;
353
354        if (rangeBox.isSelected()) {
355            numberOfOblocks = (Integer) numberToAddSpinner.getValue();
356        }
357
358        if (numberOfOblocks >= 65 // limited by JSpinnerModel to 100
359            && JmriJOptionPane.showConfirmDialog(addOBlockFrame,
360                Bundle.getMessage("WarnExcessBeans", Bundle.getMessage("OBlocks"), numberOfOblocks),
361                Bundle.getMessage("WarningTitle"),
362                JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION ) {
363                return;
364        }
365
366        String uName = NamedBean.normalizeUserName(userName.getText());
367        if (uName != null && uName.isEmpty()) {
368            uName = null;
369        }
370        String sName = startAddress.getText().trim();
371        // initial check for empty entries
372        if (autoSystemNameBox.isSelected()) {
373            startAddress.setBackground(Color.white);
374        } else if (sName.equals("")) {
375            status(Bundle.getMessage("WarningSysNameEmpty"), true);
376            startAddress.setBackground(Color.red);
377            return;
378        } else if (!sName.startsWith("OB")) {
379            sName = "OB" + sName;
380        }
381        // Add some entry pattern checking, before assembling sName and handing it to the OBlockManager
382        StringBuilder statusMessage = new StringBuilder(Bundle.getMessage("ItemCreateFeedback", Bundle.getMessage("BeanNameOBlock")));
383        String errorMessage = null;
384        for (int x = 0; x < numberOfOblocks; x++) {
385            if (uName != null && !uName.isEmpty() && oblockManager.getByUserName(uName) != null && !pref.getPreferenceState(getClassName(), "duplicateUserName")) {
386                jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class).
387                        showErrorMessage(Bundle.getMessage("ErrorTitle"), Bundle.getMessage("ErrorDuplicateUserName", uName), getClassName(), "duplicateUserName", false, true);
388                // show in status bar
389                errorMessage = Bundle.getMessage("ErrorDuplicateUserName", uName);
390                status(errorMessage, true);
391                uName = null; // new OBlock objects always receive a valid system name using the next free index, but uName names must not be in use so use none in that case
392            }
393            if (!sName.isEmpty() && oblockManager.getBySystemName(sName) != null && !pref.getPreferenceState(getClassName(), "duplicateSystemName")) {
394                jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class).
395                        showErrorMessage(Bundle.getMessage("ErrorTitle"), Bundle.getMessage("ErrorDuplicateSystemName", sName), getClassName(), "duplicateSystemName", false, true);
396                // show in status bar
397                errorMessage = Bundle.getMessage("ErrorDuplicateSystemName", sName);
398                status(errorMessage, true);
399                return; // new OBlock objects are always valid, but system names must not be in use so skip in that case
400            }
401            OBlock oblk;
402            String xName = "";
403            try {
404                if (autoSystemNameBox.isSelected()) {
405                    assert uName != null;
406                    oblk = oblockManager.createNewOBlock(uName);
407                    if (oblk == null) {
408                        xName = uName;
409                        throw new java.lang.IllegalArgumentException();
410                    }
411                } else {
412                    oblk = oblockManager.createNewOBlock(sName, uName);
413                    if (oblk == null) {
414                        xName = sName;
415                        throw new java.lang.IllegalArgumentException();
416                    }
417                }
418            } catch (IllegalArgumentException ex) {
419                // uName input no good
420                handleCreateException(xName);
421                errorMessage = Bundle.getMessage("ErrorAddFailedCheck");
422                status(errorMessage, true);
423                return; // without creating
424            }
425
426            // add first and last names to statusMessage uName feedback string
427            // only mention first and last of rangeBox added
428            if (x == 0 || x == numberOfOblocks - 1) {
429                statusMessage.append(" ").append(sName).append(" (").append(uName).append(")");
430            }
431            if (x == numberOfOblocks - 2) {
432                statusMessage.append(" ").append(Bundle.getMessage("ItemCreateUpTo")).append(" ");
433            }
434
435            // bump system & uName names
436            if (!autoSystemNameBox.isSelected()) {
437                sName = nextName(sName);
438            }
439            if (uName != null) {
440                uName = nextName(uName);
441            }
442        } // end of for loop creating rangeBox of OBlocks
443
444        // provide feedback to uName
445        if (errorMessage == null) {
446            status(statusMessage.toString(), false);
447            // statusBar.setForeground(Color.red); handled when errorMassage is set to differentiate urgency
448        }
449
450        pref.setSimplePreferenceState(systemNameAuto, autoSystemNameBox.isSelected());
451        // Notify changes
452        oblocks.fireTableDataChanged();
453    }
454
455    void addPortalPressed(ActionEvent e) {
456        if (portalFrame == null) {
457            portalFrame = new PortalEditFrame(Bundle.getMessage("TitleAddPortal"), null, portals);
458        }
459        //portalFrame.updatePortalList();
460        portalFrame.resetFrame();
461        portalFrame.pack();
462        portalFrame.setVisible(true);
463    }
464
465    void addSignalPressed(ActionEvent e) {
466        if (!signals.editMode()) {
467            signals.setEditMode(true);
468            if (signalFrame == null) {
469                signalFrame = new SignalEditFrame(Bundle.getMessage("TitleAddSignal"), null, null, signals);
470            }
471            //signalFrame.updateSignalList();
472            signalFrame.resetFrame();
473            signalFrame.pack();
474            signalFrame.setVisible(true);
475        }
476    }
477
478    void handleCreateException(String sysName) {
479        JmriJOptionPane.showMessageDialog(addOBlockFrame,
480                Bundle.getMessage("ErrorOBlockAddFailed", sysName) + "\n" + Bundle.getMessage("ErrorAddFailedCheck"),
481                Bundle.getMessage("ErrorTitle"),
482                JmriJOptionPane.ERROR_MESSAGE);
483    }
484
485    /**
486     * Create or update the blockPathTableModel. Used in EditBlockPath pane.
487     *
488//     * @param block to build a table for
489     */
490//    private void setBlockPathTableModel(OBlock block) {
491//        BlockPathTableModel blockPathTableModel = tf.getBlockPathTableModel(block);
492//    }
493
494//    @Override // loops with ListedTableItem.dispose()
495//    public void dispose() {
496//        //jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class).setSimplePreferenceState(getClassName() + ":LengthUnitMetric", centimeterBox.isSelected());
497//        f.dispose();
498//        super.dispose();
499//    }
500
501    @Override
502    protected String getClassName() {
503        return OBlockTableAction.class.getName();
504    }
505
506    @Override
507    public String getClassDescription() {
508        return Bundle.getMessage("TitleOBlockTable");
509    }
510
511//    @Override
512//    public void addToPanel(AbstractTableTabAction<OBlock> f) {
513//        // not used (checkboxes etc.)
514//    }
515
516    /**
517     * {@inheritDoc}
518     */
519    @Override
520    public void propertyChange(PropertyChangeEvent e) {
521        String property = e.getPropertyName();
522        if (log.isDebugEnabled()) {
523            log.debug("PropertyChangeEvent property = {} source= {}", property, e.getSource().getClass().getName());
524        }
525        switch (property) {
526            case "StateStored":
527                //isStateStored.setSelected(oblockManager.isStateStored());
528                break;
529            case "UseFastClock":
530            default:
531                //isFastClockUsed.setSelected(portalManager.isFastClockUsed());
532                break;
533        }
534    }
535
536    void status(String message, boolean warn){
537        statusBar.setText(message);
538        statusBar.setForeground(warn ? Color.red : Color.gray);
539    }
540
541    @Override
542    protected String helpTarget() {
543        return "package.jmri.jmrit.beantable.OBlockTable";
544    }
545
546    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OBlockTableAction.class);
547
548}