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