001package jmri.jmrit.beantable;
002
003import java.awt.Color;
004import java.awt.event.ActionEvent;
005import java.awt.event.ActionListener;
006
007import javax.annotation.Nonnull;
008import javax.swing.*;
009
010import jmri.Block;
011import jmri.InstanceManager;
012import jmri.Manager;
013import jmri.NamedBean;
014import jmri.UserPreferencesManager;
015import jmri.jmrit.beantable.block.BlockTableDataModel;
016import jmri.BlockManager;
017import jmri.util.JmriJFrame;
018import jmri.util.swing.JmriJOptionPane;
019
020/**
021 * Swing action to create and register a BlockTable GUI.
022 *
023 * @author Bob Jacobsen Copyright (C) 2003, 2008
024 * @author Egbert Broerse Copyright (C) 2017
025 */
026public class BlockTableAction extends AbstractTableAction<Block> {
027
028    /**
029     * Create an action with a specific title.
030     * <p>
031     * Note that the argument is the Action title, not the title of the
032     * resulting frame. Perhaps this should be changed?
033     *
034     * @param actionName the Action title
035     */
036    public BlockTableAction(String actionName) {
037        super(actionName);
038
039        // disable ourself if there is no primary Block manager available
040        if (InstanceManager.getNullableDefault(BlockManager.class) == null) {
041            BlockTableAction.this.setEnabled(false);
042        }
043    }
044
045    public BlockTableAction() {
046        this(Bundle.getMessage("TitleBlockTable"));
047    }
048
049    /**
050     * Create the JTable DataModel, along with the changes for the specific case
051     * of Block objects.
052     */
053    @Override
054    protected void createModel() {
055        m = new BlockTableDataModel(getManager());
056    }
057
058    @Nonnull
059    @Override
060    protected Manager<Block> getManager() {
061        return InstanceManager.getDefault(BlockManager.class);
062    }
063
064    @Override
065    protected void setTitle() {
066        f.setTitle(Bundle.getMessage("TitleBlockTable")); // NOI18N
067    }
068
069    private final JRadioButton inchBox = new JRadioButton(Bundle.getMessage("LengthInches")); // NOI18N
070    private final JRadioButton centimeterBox = new JRadioButton(Bundle.getMessage("LengthCentimeters")); // NOI18N
071    public static final String BLOCK_METRIC_PREF = BlockTableAction.class.getName() + ":LengthUnitMetric"; // NOI18N
072
073    private void initRadioButtons(){
074
075        inchBox.setToolTipText(Bundle.getMessage("InchBoxToolTip")); // NOI18N
076        centimeterBox.setToolTipText(Bundle.getMessage("CentimeterBoxToolTip")); // NOI18N
077
078        ButtonGroup group = new ButtonGroup();
079        group.add(inchBox);
080        group.add(centimeterBox);
081        inchBox.setSelected(true);
082        centimeterBox.setSelected( InstanceManager.getDefault(UserPreferencesManager.class)
083            .getSimplePreferenceState(BLOCK_METRIC_PREF));
084
085        inchBox.addActionListener( e -> metricSelectionChanged());
086        centimeterBox.addActionListener( e -> metricSelectionChanged());
087
088        // disabling keyboard input as when focused, does not fire actionlistener
089        // and appears selected causing mismatch with button selected and what the table thinks is selected.
090        inchBox.setFocusable(false);
091        centimeterBox.setFocusable(false);
092    }
093
094    /**
095     * Add the radioButtons (only 1 may be selected).
096     */
097    @Override
098    public void addToFrame(BeanTableFrame<Block> f) {
099        initRadioButtons();
100        f.addToBottomBox(inchBox, this.getClass().getName());
101        f.addToBottomBox(centimeterBox, this.getClass().getName());
102    }
103
104    /**
105     * Insert 2 table specific menus.
106     * <p>
107     * Account for the Window and Help menus,
108     * which are already added to the menu bar as part of the creation of the
109     * JFrame, by adding the menus 2 places earlier unless the table is part of
110     * the ListedTableFrame, that adds the Help menu later on.
111     *
112     * @param f the JFrame of this table
113     */
114    @Override
115    public void setMenuBar(BeanTableFrame<Block> f) {
116        final JmriJFrame finalF = f; // needed for anonymous ActionListener class
117        JMenuBar menuBar = f.getJMenuBar();
118        // count the number of menus to insert the TableMenus before 'Window' and 'Help'
119        int pos = menuBar.getMenuCount() - 1;
120        int offset = 1;
121        log.debug("setMenuBar number of menu items = {}", pos);
122        for (int i = 0; i <= pos; i++) {
123            var comp = menuBar.getComponent(i);
124            if ( comp instanceof JMenu
125                && ((JMenu)comp).getText().equals(Bundle.getMessage("MenuHelp"))) {
126                offset = -1; // correct for use as part of ListedTableAction where the Help Menu is not yet present
127            }
128        }
129        _restoreRule = getRestoreRule();
130
131        JMenu pathMenu = new JMenu(Bundle.getMessage("MenuPaths"));
132        JMenuItem item = new JMenuItem(Bundle.getMessage("MenuItemDeletePaths"));
133        pathMenu.add(item);
134        item.addActionListener( e -> deletePaths(finalF) );
135        menuBar.add(pathMenu, pos + offset);
136
137        JMenu speedMenu = new JMenu(Bundle.getMessage("SpeedsMenu"));
138        item = new JMenuItem(Bundle.getMessage("SpeedsMenuItemDefaults"));
139        speedMenu.add(item);
140        item.addActionListener( e -> ((BlockTableDataModel)m).setDefaultSpeeds(finalF));
141        menuBar.add(speedMenu, pos + offset + 1); // put it to the right of the Paths menu
142
143        JMenu valuesMenu = new JMenu(Bundle.getMessage("ValuesMenu"));
144        ButtonGroup valuesButtonGroup = new ButtonGroup();
145        JRadioButtonMenuItem jrbmi = new JRadioButtonMenuItem(Bundle.getMessage("ValuesMenuRestoreAlways"));  // NOI18N
146        jrbmi.addItemListener( e -> setRestoreRule(RestoreRule.RESTOREALWAYS) );
147        valuesButtonGroup.add(jrbmi);
148        valuesMenu.add(jrbmi);
149        jrbmi.setSelected(_restoreRule == RestoreRule.RESTOREALWAYS);
150
151        jrbmi = new JRadioButtonMenuItem(Bundle.getMessage("ValuesMenuRestoreOccupiedOnly"));  // NOI18N
152        jrbmi.addItemListener( e -> setRestoreRule(RestoreRule.RESTOREOCCUPIEDONLY) );
153        valuesButtonGroup.add(jrbmi);
154        valuesMenu.add(jrbmi);
155        jrbmi.setSelected(_restoreRule == RestoreRule.RESTOREOCCUPIEDONLY);
156
157        jrbmi = new JRadioButtonMenuItem(Bundle.getMessage("ValuesMenuRestoreOnlyIfAllOccupied"));  // NOI18N
158        jrbmi.addItemListener( e -> setRestoreRule(RestoreRule.RESTOREONLYIFALLOCCUPIED) );
159        valuesButtonGroup.add(jrbmi);
160        valuesMenu.add(jrbmi);
161        jrbmi.setSelected(_restoreRule == RestoreRule.RESTOREONLYIFALLOCCUPIED);
162
163        valuesMenu.addSeparator();
164
165        item = new JMenuItem(Bundle.getMessage("MenuItemReloadBlockValues"));
166        valuesMenu.add(item);
167        item.addActionListener(e -> reloadBlockValues());
168
169        menuBar.add(valuesMenu, pos + offset + 2); // put it to the right of the Speed menu
170    }
171
172    /**
173     * Save the restore rule selection. Called by menu item change events.
174     *
175     * @param newRule The RestoreRule enum constant
176     */
177    void setRestoreRule(RestoreRule newRule) {
178        _restoreRule = newRule;
179        InstanceManager.getDefault(jmri.UserPreferencesManager.class).
180                setProperty(getClassName(), "Restore Rule", newRule.name());  // NOI18N
181    }
182
183    /**
184     * Retrieve the restore rule selection from user preferences
185     *
186     * @return restoreRule
187     */
188    public static RestoreRule getRestoreRule() {
189        RestoreRule rr = RestoreRule.RESTOREONLYIFALLOCCUPIED; //default to previous JMRI behavior
190        Object rro = InstanceManager.getDefault(jmri.UserPreferencesManager.class).
191                getProperty("jmri.jmrit.beantable.BlockTableAction", "Restore Rule");   // NOI18N
192        if (rro != null) {
193            try {
194                rr = RestoreRule.valueOf(rro.toString());
195            } catch (IllegalArgumentException ignored) {
196                log.warn("Invalid Block Restore Rule value '{}' ignored", rro);  // NOI18N
197            }
198        }
199        return rr;
200    }
201
202    private void metricSelectionChanged() {
203        InstanceManager.getDefault(UserPreferencesManager.class)
204            .setSimplePreferenceState(BLOCK_METRIC_PREF, centimeterBox.isSelected());
205        ((BlockTableDataModel)m).setMetric(centimeterBox.isSelected());
206    }
207
208    private void reloadBlockValues() {
209        try {
210            new jmri.jmrit.display.layoutEditor.BlockValueFile().readBlockValues();
211        } catch (org.jdom2.JDOMException jde) {
212            log.error("JDOM Exception when retreiving block values", jde);
213        } catch (java.io.IOException ioe) {
214            log.error("I/O Exception when retreiving block values", ioe);
215        }
216    }
217
218    @Override
219    protected String helpTarget() {
220        return "package.jmri.jmrit.beantable.BlockTable";
221    }
222
223    private JmriJFrame addFrame = null;
224    private final JTextField sysName = new JTextField(20);
225    private final JTextField userName = new JTextField(20);
226
227    private final SpinnerNumberModel numberToAddSpinnerNumberModel =
228        new SpinnerNumberModel(1, 1, 100, 1); // maximum 100 items
229    private final JSpinner numberToAddSpinner = new JSpinner(numberToAddSpinnerNumberModel);
230    private final JCheckBox addRangeCheckBox = new JCheckBox(Bundle.getMessage("AddRangeBox"));
231    private final JCheckBox _autoSystemNameCheckBox = new JCheckBox(Bundle.getMessage("LabelAutoSysName"));
232    private final JLabel statusBar = new JLabel(Bundle.getMessage("AddBeanStatusEnter"), SwingConstants.LEADING);
233    private JButton newButton = null;
234
235    /**
236     * Rules for restoring block values     *
237     */
238    public enum RestoreRule {
239        RESTOREALWAYS,
240        RESTOREOCCUPIEDONLY,
241        RESTOREONLYIFALLOCCUPIED;
242    }
243
244    private RestoreRule _restoreRule;
245
246    @Override
247    protected void addPressed(ActionEvent e) {
248        if (addFrame == null) {
249            addFrame = new JmriJFrame(Bundle.getMessage("TitleAddBlock"), false, true);
250            addFrame.setEscapeKeyClosesWindow(true);
251            addFrame.addHelpMenu("package.jmri.jmrit.beantable.BlockAddEdit", true); // NOI18N
252            addFrame.getContentPane().setLayout(new BoxLayout(addFrame.getContentPane(), BoxLayout.Y_AXIS));
253            ActionListener oklistener = this::okPressed;
254            ActionListener cancellistener = this::cancelPressed;
255
256            AddNewBeanPanel anbp = new AddNewBeanPanel(sysName, userName,
257                numberToAddSpinner, addRangeCheckBox, _autoSystemNameCheckBox,
258                "ButtonCreate", oklistener, cancellistener, statusBar);
259            addFrame.add(anbp);
260            newButton = anbp.ok;
261            sysName.setToolTipText(Bundle.getMessage("SysNameToolTip", "B"));
262        }
263        sysName.setBackground(Color.white);
264        // reset statusBar text
265        statusBar.setText(Bundle.getMessage("AddBeanStatusEnter"));
266        statusBar.setForeground(Color.gray);
267        if (InstanceManager.getDefault(jmri.UserPreferencesManager.class).getSimplePreferenceState(systemNameAuto)) {
268            _autoSystemNameCheckBox.setSelected(true);
269        }
270        if (newButton!=null){
271            addFrame.getRootPane().setDefaultButton(newButton);
272        }
273        addRangeCheckBox.setSelected(false);
274        addFrame.pack();
275        addFrame.setVisible(true);
276    }
277
278    private final String systemNameAuto = this.getClass().getName() + ".AutoSystemName";
279
280    void cancelPressed(ActionEvent e) {
281        addFrame.setVisible(false);
282        addFrame.dispose();
283        addFrame = null;
284    }
285
286    /**
287     * Respond to Create new item pressed on Add Block pane.
288     *
289     * @param e the click event
290     */
291    void okPressed(ActionEvent e) {
292
293        int numberOfBlocks = 1;
294
295        if (addRangeCheckBox.isSelected()) {
296            numberOfBlocks = (Integer) numberToAddSpinner.getValue();
297        }
298        if ( numberOfBlocks >= 65 // limited by JSpinnerModel to 100
299            && JmriJOptionPane.showConfirmDialog(addFrame,
300                Bundle.getMessage("WarnExcessBeans", Bundle.getMessage("Blocks"), numberOfBlocks),
301                Bundle.getMessage("WarningTitle"),
302                JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION) {
303            return;
304        }
305        String user = NamedBean.normalizeUserName(userName.getText());
306        if (user == null || user.isEmpty()) {
307            user = null;
308        }
309        String uName = user; // keep result separate to prevent recursive manipulation
310        String system = "";
311
312        if (!_autoSystemNameCheckBox.isSelected()) {
313            system = InstanceManager.getDefault(jmri.BlockManager.class).makeSystemName(sysName.getText());
314        }
315        String sName = system; // keep result separate to prevent recursive manipulation
316        // initial check for empty entry using the raw name
317        if (sName.length() < 3 && !_autoSystemNameCheckBox.isSelected()) {  // Using 3 to catch a plain IB
318            statusBar.setText(Bundle.getMessage("WarningSysNameEmpty"));
319            statusBar.setForeground(Color.red);
320            sysName.setBackground(Color.red);
321            return;
322        } else {
323            sysName.setBackground(Color.white);
324        }
325
326        // Add some entry pattern checking, before assembling sName and handing it to the blockManager
327        StringBuilder statusMessage = new StringBuilder(
328            Bundle.getMessage("ItemCreateFeedback", Bundle.getMessage("BeanNameBlock")));
329
330        for (int x = 0; x < numberOfBlocks; x++) {
331            if (x != 0) { // start at 2nd Block
332                if (!_autoSystemNameCheckBox.isSelected()) {
333                    // Find first block with unused system name
334                    while (true) {
335                        system = nextName(system);
336                        log.debug("Trying sys {}", system);
337                        Block blk = InstanceManager.getDefault(BlockManager.class).getBySystemName(system);
338                        if (blk == null) {
339                            sName = system;
340                            break;
341                        }
342                    }
343                }
344                if (user != null) {
345                    // Find first block with unused user name
346                    while (true) {
347                        user = nextName(user);
348                        log.debug("Trying user {}", user);
349                        Block blk = InstanceManager.getDefault(BlockManager.class).getByUserName(user);
350                        if (blk == null) {
351                            uName = user;
352                            break;
353                        }
354                    }
355                }
356            }
357            Block blk;
358            String xName = "";
359            try {
360                if (_autoSystemNameCheckBox.isSelected()) {
361                    blk = InstanceManager.getDefault(BlockManager.class).createNewBlock(uName);
362                    if (blk == null) {
363                        xName = uName;
364                        throw new java.lang.IllegalArgumentException();
365                    }
366                } else {
367                    blk = InstanceManager.getDefault(BlockManager.class).createNewBlock(sName, uName);
368                    if (blk == null) {
369                        xName = sName;
370                        throw new java.lang.IllegalArgumentException();
371                    }
372                }
373            } catch (IllegalArgumentException ex) {
374                // user input no good
375                handleCreateException(xName);
376                statusBar.setText(Bundle.getMessage("ErrorAddFailedCheck"));
377                statusBar.setForeground(Color.red);
378                return; // without creating
379            }
380
381            // add first and last names to statusMessage user feedback string
382            if (x == 0 || x == numberOfBlocks - 1) {
383                statusMessage.append(" ").append(sName).append(" (").append(user).append(")");
384            }
385            if (x == numberOfBlocks - 2) {
386                statusMessage.append(" ").append(Bundle.getMessage("ItemCreateUpTo")).append(" ");
387            }
388            // only mention first and last of addRangeCheckBox added
389        } // end of for loop creating addRangeCheckBox of Blocks
390
391        // provide feedback to user
392        statusBar.setText(statusMessage.toString());
393        statusBar.setForeground(Color.gray);
394
395        InstanceManager.getDefault(UserPreferencesManager.class)
396            .setSimplePreferenceState(systemNameAuto, _autoSystemNameCheckBox.isSelected());
397    }
398
399    void handleCreateException(String sysName) {
400        JmriJOptionPane.showMessageDialog(addFrame,
401                Bundle.getMessage("ErrorBlockAddFailed", sysName) + "\n" + Bundle.getMessage("ErrorAddFailedCheck"),
402                Bundle.getMessage("ErrorTitle"),
403                JmriJOptionPane.ERROR_MESSAGE);
404    }
405
406    void deletePaths(JmriJFrame f) {
407        // Set option to prevent the path information from being saved.
408
409        Object[] options = {Bundle.getMessage("ButtonRemove"),
410            Bundle.getMessage("ButtonKeep")};
411
412        int retval = JmriJOptionPane.showOptionDialog(f,
413                Bundle.getMessage("BlockPathMessage"),
414                Bundle.getMessage("BlockPathSaveTitle"),
415                JmriJOptionPane.YES_NO_OPTION,
416                JmriJOptionPane.QUESTION_MESSAGE, null, options, options[1]);
417        if (retval != 0) {
418            InstanceManager.getDefault(BlockManager.class).setSavedPathInfo(true);
419            log.info("Requested to save path information via Block Menu.");
420        } else {
421            InstanceManager.getDefault(BlockManager.class).setSavedPathInfo(false);
422            log.info("Requested not to save path information via Block Menu.");
423        }
424    }
425
426    @Override
427    public String getClassDescription() {
428        return Bundle.getMessage("TitleBlockTable");
429    }
430
431    @Override
432    protected String getClassName() {
433        return BlockTableAction.class.getName();
434    }
435
436    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BlockTableAction.class);
437
438}