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        menuBar.add(valuesMenu, pos + offset + 2); // put it to the right of the Speed menu
164    
165    }
166
167    /**
168     * Save the restore rule selection. Called by menu item change events.
169     *
170     * @param newRule The RestoreRule enum constant
171     */
172    void setRestoreRule(RestoreRule newRule) {
173        _restoreRule = newRule;
174        InstanceManager.getDefault(jmri.UserPreferencesManager.class).
175                setProperty(getClassName(), "Restore Rule", newRule.name());  // NOI18N
176    }
177
178    /**
179     * Retrieve the restore rule selection from user preferences
180     *
181     * @return restoreRule 
182     */
183    public static RestoreRule getRestoreRule() {
184        RestoreRule rr = RestoreRule.RESTOREONLYIFALLOCCUPIED; //default to previous JMRI behavior 
185        Object rro = InstanceManager.getDefault(jmri.UserPreferencesManager.class).
186                getProperty("jmri.jmrit.beantable.BlockTableAction", "Restore Rule");   // NOI18N
187        if (rro != null) {
188            try {
189                rr = RestoreRule.valueOf(rro.toString());
190            } catch (IllegalArgumentException ignored) {
191                log.warn("Invalid Block Restore Rule value '{}' ignored", rro);  // NOI18N
192            }
193        }
194        return rr;
195    }
196
197    private void metricSelectionChanged() {
198        InstanceManager.getDefault(UserPreferencesManager.class)
199            .setSimplePreferenceState(BLOCK_METRIC_PREF, centimeterBox.isSelected());
200        ((BlockTableDataModel)m).setMetric(centimeterBox.isSelected());
201    }
202
203    @Override
204    protected String helpTarget() {
205        return "package.jmri.jmrit.beantable.BlockTable";
206    }
207
208    private JmriJFrame addFrame = null;
209    private final JTextField sysName = new JTextField(20);
210    private final JTextField userName = new JTextField(20);
211
212    private final SpinnerNumberModel numberToAddSpinnerNumberModel =
213        new SpinnerNumberModel(1, 1, 100, 1); // maximum 100 items
214    private final JSpinner numberToAddSpinner = new JSpinner(numberToAddSpinnerNumberModel);
215    private final JCheckBox addRangeCheckBox = new JCheckBox(Bundle.getMessage("AddRangeBox"));
216    private final JCheckBox _autoSystemNameCheckBox = new JCheckBox(Bundle.getMessage("LabelAutoSysName"));
217    private final JLabel statusBar = new JLabel(Bundle.getMessage("AddBeanStatusEnter"), SwingConstants.LEADING);
218    private JButton newButton = null;
219
220    /**
221     * Rules for restoring block values     *
222     */
223    public enum RestoreRule {
224        RESTOREALWAYS,
225        RESTOREOCCUPIEDONLY,
226        RESTOREONLYIFALLOCCUPIED;
227    }
228
229    private RestoreRule _restoreRule;
230
231    @Override
232    protected void addPressed(ActionEvent e) {
233        if (addFrame == null) {
234            addFrame = new JmriJFrame(Bundle.getMessage("TitleAddBlock"), false, true);
235            addFrame.setEscapeKeyClosesWindow(true);
236            addFrame.addHelpMenu("package.jmri.jmrit.beantable.BlockAddEdit", true); // NOI18N
237            addFrame.getContentPane().setLayout(new BoxLayout(addFrame.getContentPane(), BoxLayout.Y_AXIS));
238            ActionListener oklistener = this::okPressed;
239            ActionListener cancellistener = this::cancelPressed;
240            
241            AddNewBeanPanel anbp = new AddNewBeanPanel(sysName, userName,
242                numberToAddSpinner, addRangeCheckBox, _autoSystemNameCheckBox,
243                "ButtonCreate", oklistener, cancellistener, statusBar);
244            addFrame.add(anbp);
245            newButton = anbp.ok;
246            sysName.setToolTipText(Bundle.getMessage("SysNameToolTip", "B"));
247        }
248        sysName.setBackground(Color.white);
249        // reset statusBar text
250        statusBar.setText(Bundle.getMessage("AddBeanStatusEnter"));
251        statusBar.setForeground(Color.gray);
252        if (InstanceManager.getDefault(jmri.UserPreferencesManager.class).getSimplePreferenceState(systemNameAuto)) {
253            _autoSystemNameCheckBox.setSelected(true);
254        }
255        if (newButton!=null){
256            addFrame.getRootPane().setDefaultButton(newButton);
257        }
258        addRangeCheckBox.setSelected(false);
259        addFrame.pack();
260        addFrame.setVisible(true);
261    }
262
263    private final String systemNameAuto = this.getClass().getName() + ".AutoSystemName";
264
265    void cancelPressed(ActionEvent e) {
266        addFrame.setVisible(false);
267        addFrame.dispose();
268        addFrame = null;
269    }
270
271    /**
272     * Respond to Create new item pressed on Add Block pane.
273     *
274     * @param e the click event
275     */
276    void okPressed(ActionEvent e) {
277
278        int numberOfBlocks = 1;
279
280        if (addRangeCheckBox.isSelected()) {
281            numberOfBlocks = (Integer) numberToAddSpinner.getValue();
282        }
283        if ( numberOfBlocks >= 65 // limited by JSpinnerModel to 100
284            && JmriJOptionPane.showConfirmDialog(addFrame,
285                Bundle.getMessage("WarnExcessBeans", Bundle.getMessage("Blocks"), numberOfBlocks),
286                Bundle.getMessage("WarningTitle"),
287                JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION) {
288            return;
289        }
290        String user = NamedBean.normalizeUserName(userName.getText());
291        if (user == null || user.isEmpty()) {
292            user = null;
293        }
294        String uName = user; // keep result separate to prevent recursive manipulation
295        String system = "";
296
297        if (!_autoSystemNameCheckBox.isSelected()) {
298            system = InstanceManager.getDefault(jmri.BlockManager.class).makeSystemName(sysName.getText());
299        }
300        String sName = system; // keep result separate to prevent recursive manipulation
301        // initial check for empty entry using the raw name
302        if (sName.length() < 3 && !_autoSystemNameCheckBox.isSelected()) {  // Using 3 to catch a plain IB
303            statusBar.setText(Bundle.getMessage("WarningSysNameEmpty"));
304            statusBar.setForeground(Color.red);
305            sysName.setBackground(Color.red);
306            return;
307        } else {
308            sysName.setBackground(Color.white);
309        }
310
311        // Add some entry pattern checking, before assembling sName and handing it to the blockManager
312        StringBuilder statusMessage = new StringBuilder(
313            Bundle.getMessage("ItemCreateFeedback", Bundle.getMessage("BeanNameBlock")));
314
315        for (int x = 0; x < numberOfBlocks; x++) {
316            if (x != 0) { // start at 2nd Block
317                if (!_autoSystemNameCheckBox.isSelected()) {
318                    // Find first block with unused system name
319                    while (true) {
320                        system = nextName(system);
321                        log.debug("Trying sys {}", system);
322                        Block blk = InstanceManager.getDefault(BlockManager.class).getBySystemName(system);
323                        if (blk == null) {
324                            sName = system;
325                            break;
326                        }
327                    }
328                }
329                if (user != null) {
330                    // Find first block with unused user name
331                    while (true) {
332                        user = nextName(user);
333                        log.debug("Trying user {}", user);
334                        Block blk = InstanceManager.getDefault(BlockManager.class).getByUserName(user);
335                        if (blk == null) {
336                            uName = user;
337                            break;
338                        }
339                    }
340                }
341            }
342            Block blk;
343            String xName = "";
344            try {
345                if (_autoSystemNameCheckBox.isSelected()) {
346                    blk = InstanceManager.getDefault(BlockManager.class).createNewBlock(uName);
347                    if (blk == null) {
348                        xName = uName;
349                        throw new java.lang.IllegalArgumentException();
350                    }
351                } else {
352                    blk = InstanceManager.getDefault(BlockManager.class).createNewBlock(sName, uName);
353                    if (blk == null) {
354                        xName = sName;
355                        throw new java.lang.IllegalArgumentException();
356                    }
357                }
358            } catch (IllegalArgumentException ex) {
359                // user input no good
360                handleCreateException(xName);
361                statusBar.setText(Bundle.getMessage("ErrorAddFailedCheck"));
362                statusBar.setForeground(Color.red);
363                return; // without creating
364            }
365            
366            // add first and last names to statusMessage user feedback string
367            if (x == 0 || x == numberOfBlocks - 1) {
368                statusMessage.append(" ").append(sName).append(" (").append(user).append(")");
369            }
370            if (x == numberOfBlocks - 2) {
371                statusMessage.append(" ").append(Bundle.getMessage("ItemCreateUpTo")).append(" ");
372            }
373            // only mention first and last of addRangeCheckBox added
374        } // end of for loop creating addRangeCheckBox of Blocks
375
376        // provide feedback to user
377        statusBar.setText(statusMessage.toString());
378        statusBar.setForeground(Color.gray);
379
380        InstanceManager.getDefault(UserPreferencesManager.class)
381            .setSimplePreferenceState(systemNameAuto, _autoSystemNameCheckBox.isSelected());
382    }
383
384    void handleCreateException(String sysName) {
385        JmriJOptionPane.showMessageDialog(addFrame,
386                Bundle.getMessage("ErrorBlockAddFailed", sysName) + "\n" + Bundle.getMessage("ErrorAddFailedCheck"),
387                Bundle.getMessage("ErrorTitle"),
388                JmriJOptionPane.ERROR_MESSAGE);
389    }
390
391    void deletePaths(JmriJFrame f) {
392        // Set option to prevent the path information from being saved.
393
394        Object[] options = {Bundle.getMessage("ButtonRemove"),
395            Bundle.getMessage("ButtonKeep")};
396
397        int retval = JmriJOptionPane.showOptionDialog(f,
398                Bundle.getMessage("BlockPathMessage"),
399                Bundle.getMessage("BlockPathSaveTitle"),
400                JmriJOptionPane.YES_NO_OPTION,
401                JmriJOptionPane.QUESTION_MESSAGE, null, options, options[1]);
402        if (retval != 0) {
403            InstanceManager.getDefault(BlockManager.class).setSavedPathInfo(true);
404            log.info("Requested to save path information via Block Menu.");
405        } else {
406            InstanceManager.getDefault(BlockManager.class).setSavedPathInfo(false);
407            log.info("Requested not to save path information via Block Menu.");
408        }
409    }
410
411    @Override
412    public String getClassDescription() {
413        return Bundle.getMessage("TitleBlockTable");
414    }
415
416    @Override
417    protected String getClassName() {
418        return BlockTableAction.class.getName();
419    }
420
421    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BlockTableAction.class);
422
423}