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