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;
018
019import org.slf4j.Logger;
020import org.slf4j.LoggerFactory;
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 (jmri.InstanceManager.getNullableDefault(jmri.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 jmri.util.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
131        JMenu pathMenu = new JMenu(Bundle.getMessage("MenuPaths"));
132        JMenuItem item = new JMenuItem(Bundle.getMessage("MenuItemDeletePaths"));
133        pathMenu.add(item);
134        item.addActionListener((ActionEvent e) -> {
135            deletePaths(finalF);
136        });
137        menuBar.add(pathMenu, pos + offset);
138
139        JMenu speedMenu = new JMenu(Bundle.getMessage("SpeedsMenu"));
140        item = new JMenuItem(Bundle.getMessage("SpeedsMenuItemDefaults"));
141        speedMenu.add(item);
142        item.addActionListener((ActionEvent e) -> {
143            ((BlockTableDataModel)m).setDefaultSpeeds(finalF);
144        });
145        menuBar.add(speedMenu, pos + offset + 1); // put it to the right of the Paths menu
146    }
147    
148    private void metricSelectionChanged(ActionEvent e) {
149        InstanceManager.getDefault(UserPreferencesManager.class)
150            .setSimplePreferenceState(BLOCK_METRIC_PREF, centimeterBox.isSelected());
151        ((BlockTableDataModel)m).setMetric(centimeterBox.isSelected());
152    }
153
154    @Override
155    protected String helpTarget() {
156        return "package.jmri.jmrit.beantable.BlockTable";
157    }
158
159    JmriJFrame addFrame = null;
160    JTextField sysName = new JTextField(20);
161    JTextField userName = new JTextField(20);
162    JLabel sysNameLabel = new JLabel(Bundle.getMessage("LabelSystemName"));
163    JLabel userNameLabel = new JLabel(Bundle.getMessage("LabelUserName"));
164
165    SpinnerNumberModel numberToAddSpinnerNumberModel = new SpinnerNumberModel(1, 1, 100, 1); // maximum 100 items
166    JSpinner numberToAddSpinner = new JSpinner(numberToAddSpinnerNumberModel);
167    JCheckBox addRangeCheckBox = new JCheckBox(Bundle.getMessage("AddRangeBox"));
168    JCheckBox _autoSystemNameCheckBox = new JCheckBox(Bundle.getMessage("LabelAutoSysName"));
169    JLabel statusBar = new JLabel(Bundle.getMessage("AddBeanStatusEnter"), JLabel.LEADING);
170    private JButton newButton = null;
171
172    @Override
173    protected void addPressed(ActionEvent e) {
174        if (addFrame == null) {
175            addFrame = new JmriJFrame(Bundle.getMessage("TitleAddBlock"), false, true);
176            addFrame.setEscapeKeyClosesWindow(true);
177            addFrame.addHelpMenu("package.jmri.jmrit.beantable.BlockAddEdit", true); // NOI18N
178            addFrame.getContentPane().setLayout(new BoxLayout(addFrame.getContentPane(), BoxLayout.Y_AXIS));
179            ActionListener oklistener = this::okPressed;
180            ActionListener cancellistener = this::cancelPressed;
181            
182            AddNewBeanPanel anbp = new AddNewBeanPanel(sysName, userName, numberToAddSpinner, addRangeCheckBox, _autoSystemNameCheckBox, "ButtonCreate", oklistener, cancellistener, statusBar); 
183            addFrame.add(anbp);
184            newButton = anbp.ok;
185            sysName.setToolTipText(Bundle.getMessage("SysNameToolTip", "B")); // override tooltip with bean specific letter
186        }
187        sysName.setBackground(Color.white);
188        // reset statusBar text
189        statusBar.setText(Bundle.getMessage("AddBeanStatusEnter"));
190        statusBar.setForeground(Color.gray);
191        if (InstanceManager.getDefault(jmri.UserPreferencesManager.class).getSimplePreferenceState(systemNameAuto)) {
192            _autoSystemNameCheckBox.setSelected(true);
193        }
194        if (newButton!=null){
195            addFrame.getRootPane().setDefaultButton(newButton);
196        }
197        addRangeCheckBox.setSelected(false);
198        addFrame.pack();
199        addFrame.setVisible(true);
200    }
201
202    String systemNameAuto = this.getClass().getName() + ".AutoSystemName";
203
204    void cancelPressed(ActionEvent e) {
205        addFrame.setVisible(false);
206        addFrame.dispose();
207        addFrame = null;
208    }
209
210    /**
211     * Respond to Create new item pressed on Add Block pane.
212     *
213     * @param e the click event
214     */
215    void okPressed(ActionEvent e) {
216
217        int numberOfBlocks = 1;
218
219        if (addRangeCheckBox.isSelected()) {
220            numberOfBlocks = (Integer) numberToAddSpinner.getValue();
221        }
222        if (numberOfBlocks >= 65) { // limited by JSpinnerModel to 100
223            if (JOptionPane.showConfirmDialog(addFrame,
224                    Bundle.getMessage("WarnExcessBeans", Bundle.getMessage("Blocks"), numberOfBlocks),
225                    Bundle.getMessage("WarningTitle"),
226                    JOptionPane.YES_NO_OPTION) == 1) {
227                return;
228            }
229        }
230        String user = NamedBean.normalizeUserName(userName.getText());
231        if (user == null || user.isEmpty()) {
232            user = null;
233        }
234        String uName = user; // keep result separate to prevent recursive manipulation
235        String system = "";
236
237        if (!_autoSystemNameCheckBox.isSelected()) {
238            system = InstanceManager.getDefault(jmri.BlockManager.class).makeSystemName(sysName.getText());
239        }
240        String sName = system; // keep result separate to prevent recursive manipulation
241        // initial check for empty entry using the raw name
242        if (sName.length() < 3 && !_autoSystemNameCheckBox.isSelected()) {  // Using 3 to catch a plain IB
243            statusBar.setText(Bundle.getMessage("WarningSysNameEmpty"));
244            statusBar.setForeground(Color.red);
245            sysName.setBackground(Color.red);
246            return;
247        } else {
248            sysName.setBackground(Color.white);
249        }
250
251        // Add some entry pattern checking, before assembling sName and handing it to the blockManager
252        StringBuilder statusMessage = new StringBuilder(Bundle.getMessage("ItemCreateFeedback", Bundle.getMessage("BeanNameBlock")));
253
254        for (int x = 0; x < numberOfBlocks; x++) {
255            if (x != 0) { // start at 2nd Block
256                if (!_autoSystemNameCheckBox.isSelected()) {
257                    // Find first block with unused system name
258                    while (true) {
259                        system = nextName(system);
260                        // log.warn("Trying " + system);
261                        Block blk = InstanceManager.getDefault(jmri.BlockManager.class).getBySystemName(system);
262                        if (blk == null) {
263                            sName = system;
264                            break;
265                        }
266                    }
267                }
268                if (user != null) {
269                    // Find first block with unused user name
270                    while (true) {
271                        user = nextName(user);
272                        //log.warn("Trying " + user);
273                        Block blk = InstanceManager.getDefault(jmri.BlockManager.class).getByUserName(user);
274                        if (blk == null) {
275                            uName = user;
276                            break;
277                        }
278                    }
279                }
280            }
281            Block blk;
282            String xName = "";
283            try {
284                if (_autoSystemNameCheckBox.isSelected()) {
285                    blk = InstanceManager.getDefault(jmri.BlockManager.class).createNewBlock(uName);
286                    if (blk == null) {
287                        xName = uName;
288                        throw new java.lang.IllegalArgumentException();
289                    }
290                } else {
291                    blk = InstanceManager.getDefault(jmri.BlockManager.class).createNewBlock(sName, uName);
292                    if (blk == null) {
293                        xName = sName;
294                        throw new java.lang.IllegalArgumentException();
295                    }
296                }
297            } catch (IllegalArgumentException ex) {
298                // user input no good
299                handleCreateException(xName);
300                statusBar.setText(Bundle.getMessage("ErrorAddFailedCheck"));
301                statusBar.setForeground(Color.red);
302                return; // without creating
303            }
304            
305            // add first and last names to statusMessage user feedback string
306            if (x == 0 || x == numberOfBlocks - 1) {
307                statusMessage.append(" ").append(sName).append(" (").append(user).append(")");
308            }
309            if (x == numberOfBlocks - 2) {
310                statusMessage.append(" ").append(Bundle.getMessage("ItemCreateUpTo")).append(" ");
311            }
312            // only mention first and last of addRangeCheckBox added
313        } // end of for loop creating addRangeCheckBox of Blocks
314
315        // provide feedback to user
316        statusBar.setText(statusMessage.toString());
317        statusBar.setForeground(Color.gray);
318
319        InstanceManager.getDefault(UserPreferencesManager.class)
320            .setSimplePreferenceState(systemNameAuto, _autoSystemNameCheckBox.isSelected());
321    }
322
323    void handleCreateException(String sysName) {
324        JOptionPane.showMessageDialog(addFrame,
325                Bundle.getMessage("ErrorBlockAddFailed", sysName) + "\n" + Bundle.getMessage("ErrorAddFailedCheck"),
326                Bundle.getMessage("ErrorTitle"),
327                JOptionPane.ERROR_MESSAGE);
328    }
329    //private boolean noWarn = false;
330
331    void deletePaths(jmri.util.JmriJFrame f) {
332        // Set option to prevent the path information from being saved.
333
334        Object[] options = {Bundle.getMessage("ButtonRemove"),
335            Bundle.getMessage("ButtonKeep")};
336
337        int retval = JOptionPane.showOptionDialog(f,
338                Bundle.getMessage("BlockPathMessage"),
339                Bundle.getMessage("BlockPathSaveTitle"),
340                JOptionPane.YES_NO_OPTION,
341                JOptionPane.QUESTION_MESSAGE, null, options, options[1]);
342        if (retval != 0) {
343            InstanceManager.getDefault(jmri.BlockManager.class).setSavedPathInfo(true);
344            log.info("Requested to save path information via Block Menu.");
345        } else {
346            InstanceManager.getDefault(jmri.BlockManager.class).setSavedPathInfo(false);
347            log.info("Requested not to save path information via Block Menu.");
348        }
349    }
350
351    @Override
352    public String getClassDescription() {
353        return Bundle.getMessage("TitleBlockTable");
354    }
355
356    @Override
357    protected String getClassName() {
358        return BlockTableAction.class.getName();
359    }
360
361    private final static Logger log = LoggerFactory.getLogger(BlockTableAction.class);
362
363}