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.InstanceManager;
011import jmri.Manager;
012import jmri.Sensor;
013import jmri.SensorManager;
014import jmri.swing.ManagerComboBox;
015import jmri.swing.SystemNameValidator;
016import jmri.jmrit.beantable.sensor.SensorTableDataModel;
017import jmri.util.JmriJFrame;
018import jmri.util.swing.TriStateJCheckBox;
019import jmri.util.swing.JmriJOptionPane;
020
021/**
022 * Swing action to create and register a SensorTable GUI.
023 *
024 * @author Bob Jacobsen Copyright (C) 2003, 2009
025 */
026public class SensorTableAction extends AbstractTableAction<Sensor> {
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 title of the action
035     */
036    public SensorTableAction(String actionName) {
037        super(actionName);
038
039        // disable ourself if there is no primary sensor manager available
040        if (sensorManager == null) {
041            super.setEnabled(false);
042        }
043    }
044
045    public SensorTableAction() {
046        this(Bundle.getMessage("TitleSensorTable"));
047    }
048
049    protected SensorManager sensorManager = InstanceManager.getDefault(SensorManager.class);
050
051    /**
052     * {@inheritDoc}
053     */
054    @Override
055    public void setManager(@Nonnull Manager<Sensor> s) {
056        if (s instanceof SensorManager) {
057            log.debug("setting manager of ST Action{} to {}",this,s.getClass());
058            sensorManager = (SensorManager) s;
059            if (m != null) {
060                m.setManager(sensorManager);
061            }
062        }
063    }
064
065    /**
066     * Create the JTable DataModel, along with the changes for the specific case
067     * of Sensors.
068     */
069    @Override
070    protected void createModel() {
071        m = new jmri.jmrit.beantable.sensor.SensorTableDataModel(sensorManager);
072    }
073
074    /**
075     * {@inheritDoc}
076     */
077    @Override
078    protected void setTitle() {
079        f.setTitle(Bundle.getMessage("TitleSensorTable"));
080    }
081
082    /**
083     * {@inheritDoc}
084     */
085    @Override
086    protected String helpTarget() {
087        return "package.jmri.jmrit.beantable.SensorTable";
088    }
089
090    JmriJFrame addFrame = null;
091
092    JTextField hardwareAddressTextField = new JTextField(20);
093    // initially allow any 20 char string, updated by prefixBox selection
094    JTextField userNameField = new JTextField(40);
095    ManagerComboBox<Sensor> prefixBox = new ManagerComboBox<>();
096    SpinnerNumberModel rangeSpinner = new SpinnerNumberModel(1, 1, 100, 1); // maximum 100 items
097    JSpinner numberToAddSpinner = new JSpinner(rangeSpinner);
098    JCheckBox rangeBox = new JCheckBox(Bundle.getMessage("AddRangeBox"));
099    JLabel hwAddressLabel = new JLabel(Bundle.getMessage("LabelHardwareAddress"));
100    JLabel userNameLabel = new JLabel(Bundle.getMessage("LabelUserName"));
101    String systemSelectionCombo = this.getClass().getName() + ".SystemSelected";
102    JButton addButton;
103    JLabel statusBarLabel = new JLabel(Bundle.getMessage("HardwareAddStatusEnter"), JLabel.LEADING);
104    jmri.UserPreferencesManager p;
105    Manager<Sensor> connectionChoice = null;
106    SystemNameValidator hardwareAddressValidator;
107
108    /**
109     * {@inheritDoc}
110     */
111    @Override
112    protected void addPressed(ActionEvent e) {
113        p = InstanceManager.getDefault(jmri.UserPreferencesManager.class);
114
115        if (addFrame == null) {
116            addFrame = new JmriJFrame(Bundle.getMessage("TitleAddSensor"));
117            addFrame.addHelpMenu("package.jmri.jmrit.beantable.SensorAddEdit", true);
118            addFrame.getContentPane().setLayout(new BoxLayout(addFrame.getContentPane(), BoxLayout.Y_AXIS));
119
120            ActionListener createListener = this::createPressed;
121            ActionListener cancelListener = this::cancelPressed;
122            ActionListener rangeListener = this::canAddRange;
123            configureManagerComboBox(prefixBox, sensorManager, SensorManager.class);
124            userNameField.setName("userName"); // NOI18N
125            prefixBox.setName("prefixBox"); // NOI18N
126            addButton = new JButton(Bundle.getMessage("ButtonCreate"));
127            addButton.addActionListener(createListener);
128
129            log.debug("add frame hwAddValidator is {} prefix box is {}",hardwareAddressValidator, prefixBox.getSelectedItem());
130            if (hardwareAddressValidator==null){
131                hardwareAddressValidator = new SystemNameValidator(hardwareAddressTextField, prefixBox.getSelectedItem(), true);
132            } else {
133                hardwareAddressValidator.setManager(prefixBox.getSelectedItem());
134            }
135
136            // create panel
137            addFrame.add(new AddNewHardwareDevicePanel(hardwareAddressTextField, hardwareAddressValidator, userNameField, prefixBox,
138                    numberToAddSpinner, rangeBox, addButton, cancelListener, rangeListener, statusBarLabel));
139            // tooltip for hwAddressTextField will be assigned later by canAddRange()
140            canAddRange(null);
141
142            addFrame.setEscapeKeyClosesWindow(true);
143            addFrame.getRootPane().setDefaultButton(addButton);
144
145        }
146        hardwareAddressTextField.setName("hwAddressTextField"); // for GUI test NOI18N
147        addButton.setName("createButton"); // for GUI test NOI18N
148        // reset statusBarLabel text
149        statusBarLabel.setText(Bundle.getMessage("HardwareAddStatusEnter"));
150        statusBarLabel.setForeground(Color.gray);
151
152        addFrame.pack();
153        addFrame.setVisible(true);
154    }
155
156    void cancelPressed(ActionEvent e) {
157        removePrefixBoxListener(prefixBox);
158        addFrame.setVisible(false);
159        addFrame.dispose();
160        addFrame = null;
161    }
162
163    /**
164     * Respond to Create new item button pressed on Add Sensor pane.
165     *
166     * @param e the click event
167     */
168    void createPressed(ActionEvent e) {
169
170        int numberOfSensors = 1;
171
172        if (rangeBox.isSelected()) {
173            numberOfSensors = (Integer) numberToAddSpinner.getValue();
174        }
175        if (numberOfSensors >= 65 // number beyond which to warn and ask permission; limited by JSpinnerModel to 100
176            && JmriJOptionPane.showConfirmDialog(addFrame,
177                Bundle.getMessage("WarnExcessBeans", Bundle.getMessage("Sensors"), numberOfSensors),
178                Bundle.getMessage("WarningTitle"),
179                JmriJOptionPane.YES_NO_OPTION ) != JmriJOptionPane.YES_OPTION ) {
180            return;
181        }
182        String sensorPrefix = prefixBox.getSelectedItem().getSystemPrefix();
183        String sName;
184        String uName = userNameField.getText();
185        String curAddress = hardwareAddressTextField.getText();
186
187        // initial check for empty entry
188        if (curAddress.length() < 1) {
189            statusBarLabel.setText(Bundle.getMessage("WarningEmptyHardwareAddress"));
190            statusBarLabel.setForeground(Color.red);
191            hardwareAddressTextField.setBackground(Color.red);
192            return;
193        } else {
194            hardwareAddressTextField.setBackground(Color.white);
195        }
196
197        // Add some entry pattern checking, before assembling sName and handing it to the SensorManager
198        StringBuilder statusMessage = new StringBuilder(Bundle.getMessage("ItemCreateFeedback", Bundle.getMessage("BeanNameSensor")));
199
200        // Compose the first proposed system name from parts:
201        sName = sensorPrefix + InstanceManager.getDefault(SensorManager.class).typeLetter() + curAddress;
202
203        for (int x = 0; x < numberOfSensors; x++) {
204            log.debug("b4 next valid addr for prefix {} system name {} conn choice mgr {}",sensorPrefix,sName, connectionChoice);
205
206            // create the sensor
207            Sensor s;
208            try {
209                s = InstanceManager.getDefault(SensorManager.class).provideSensor(sName);
210            } catch (IllegalArgumentException ex) {
211                // user input no good
212                handleCreateException(ex, sName);
213                return;   // return without creating
214            }
215
216            // handle setting user name
217            if (!uName.isEmpty()) {
218                if (InstanceManager.getDefault(SensorManager.class).getByUserName(uName) == null) {
219                    s.setUserName(uName);
220                } else {
221                    InstanceManager.getDefault(jmri.UserPreferencesManager.class).
222                            showErrorMessage(Bundle.getMessage("ErrorTitle"),
223                                    Bundle.getMessage("ErrorDuplicateUserName", uName),
224                                    getClassName(), "duplicateUserName", false, true);
225                }
226            }
227
228            // add first and last names to statusMessage uName feedback string
229            // only mention first and last of rangeBox added
230            if (x == 0 || x == numberOfSensors - 1) {
231                statusMessage.append(" ").append(sName).append(" (").append(uName).append(")");
232            }
233            if (x == numberOfSensors - 2) {
234                statusMessage.append(" ").append(Bundle.getMessage("ItemCreateUpTo")).append(" ");
235            }
236
237            // except on last pass
238            if (x < numberOfSensors-1) {
239                // bump system name
240                try {
241                    sName = InstanceManager.getDefault(SensorManager.class).getNextValidSystemName(s);
242                } catch (jmri.JmriException ex) {
243                    displayHwError(s.getSystemName(), ex);
244                    // directly add to statusBarLabel (but never called?)
245                    statusBarLabel.setText(Bundle.getMessage("ErrorConvertHW", sName));
246                    statusBarLabel.setForeground(Color.red);
247                    return;
248                }
249
250                // bump user name
251                if (!uName.isEmpty()) {
252                    uName = nextName(uName);
253                }
254            }
255            // end of for loop creating rangeBox of Sensors
256        }
257
258        // provide success feedback to user
259        statusBarLabel.setText(statusMessage.toString());
260        statusBarLabel.setForeground(Color.gray);
261
262        p.setComboBoxLastSelection(systemSelectionCombo, prefixBox.getSelectedItem().getMemo().getUserName());
263        removePrefixBoxListener(prefixBox);
264        addFrame.setVisible(false);
265        addFrame.dispose();
266        addFrame = null;
267    }
268
269    private String addEntryToolTip;
270
271    /**
272     * Activate Add a rangeBox option if manager accepts adding more than 1
273     * Sensor and set a manager specific tooltip on the AddNewHardwareDevice
274     * pane.
275     */
276    private void canAddRange(ActionEvent e) {
277        rangeBox.setEnabled(false);
278        rangeBox.setSelected(false);
279        if (prefixBox.getSelectedIndex() == -1) {
280            prefixBox.setSelectedIndex(0);
281        }
282        connectionChoice = prefixBox.getSelectedItem(); // store in Field for CheckedTextField
283        String systemPrefix = connectionChoice.getSystemPrefix();
284        rangeBox.setEnabled(((SensorManager) connectionChoice).allowMultipleAdditions(systemPrefix));
285        addEntryToolTip = connectionChoice.getEntryToolTip();
286        // show hwAddressTextField field tooltip in the Add Sensor pane that matches system connection selected from combobox
287        hardwareAddressTextField.setToolTipText(
288                Bundle.getMessage("AddEntryToolTipLine1",
289                        connectionChoice.getMemo().getUserName(),
290                        Bundle.getMessage("Sensors"),
291                        addEntryToolTip));
292        hardwareAddressValidator.setToolTipText(hardwareAddressTextField.getToolTipText());
293        hardwareAddressValidator.verify(hardwareAddressTextField);
294    }
295
296    void handleCreateException(Exception ex, String hwAddress) {
297        statusBarLabel.setText(ex.getLocalizedMessage());
298        String err = Bundle.getMessage("ErrorBeanCreateFailed",
299            InstanceManager.getDefault(SensorManager.class).getBeanTypeHandled(),hwAddress);
300        JmriJOptionPane.showMessageDialog(addFrame, err + "\n" + ex.getLocalizedMessage(),
301                err, JmriJOptionPane.ERROR_MESSAGE);
302    }
303
304    protected void setDefaultDebounce(JFrame _who) {
305        SpinnerNumberModel activeSpinnerModel = new SpinnerNumberModel((Long)sensorManager.getDefaultSensorDebounceGoingActive(), (Long)0L, Sensor.MAX_DEBOUNCE, (Long)1L); // MAX_DEBOUNCE is a Long; casts are to force needed signature
306        JSpinner activeSpinner = new JSpinner(activeSpinnerModel);
307        activeSpinner.setPreferredSize(new JTextField(Long.toString(Sensor.MAX_DEBOUNCE).length()+1).getPreferredSize());
308        SpinnerNumberModel inActiveSpinnerModel = new SpinnerNumberModel((Long)sensorManager.getDefaultSensorDebounceGoingInActive(), (Long)0L, Sensor.MAX_DEBOUNCE, (Long)1L); // MAX_DEBOUNCE is a Long; casts are to force needed signature
309        JSpinner inActiveSpinner = new JSpinner(inActiveSpinnerModel);
310        inActiveSpinner.setPreferredSize(new JTextField(Long.toString(Sensor.MAX_DEBOUNCE).length()+1).getPreferredSize());
311
312        JPanel input = new JPanel(); // panel to hold formatted input for dialog
313        input.setLayout(new BoxLayout(input, BoxLayout.Y_AXIS));
314
315        JTextArea message = new JTextArea(Bundle.getMessage("SensorGlobalDebounceMessageBox")); // multi line
316        message.setEditable(false);
317        message.setOpaque(false);
318        input.add(message);
319
320        JPanel active = new JPanel();
321        active.add(new JLabel(Bundle.getMessage("SensorActiveTimer")));
322        active.add(activeSpinner);
323        input.add(active);
324
325        JPanel inActive = new JPanel();
326        inActive.add(new JLabel(Bundle.getMessage("SensorInactiveTimer")));
327        inActive.add(inActiveSpinner);
328        input.add(inActive);
329
330        int retval = JmriJOptionPane.showOptionDialog(_who,
331                input, Bundle.getMessage("SensorGlobalDebounceMessageTitle"),
332                0, JOptionPane.INFORMATION_MESSAGE, null,
333                new Object[]{Bundle.getMessage("ButtonOK"), Bundle.getMessage("ButtonCancel")},
334                Bundle.getMessage("ButtonCancel"));
335        log.debug("dialog retval={}", retval);
336        if (retval != 0) { // array position 0, ButtonOK
337            return;
338        }
339
340        // Allow the sensor manager to handle checking if the values have changed
341        sensorManager.setDefaultSensorDebounceGoingActive((Long) activeSpinner.getValue());
342        sensorManager.setDefaultSensorDebounceGoingInActive((Long) inActiveSpinner.getValue());
343        m.fireTableDataChanged();
344    }
345
346    protected void setDefaultState(JFrame _who) {
347        String[] sensorStates = new String[]{Bundle.getMessage("BeanStateUnknown"), Bundle.getMessage("SensorStateInactive"), Bundle.getMessage("SensorStateActive"), Bundle.getMessage("BeanStateInconsistent")};
348        JComboBox<String> stateCombo = new JComboBox<>(sensorStates);
349        switch (jmri.jmrix.internal.InternalSensorManager.getDefaultStateForNewSensors()) {
350            case jmri.Sensor.ACTIVE:
351                stateCombo.setSelectedItem(Bundle.getMessage("SensorStateActive"));
352                break;
353            case jmri.Sensor.INACTIVE:
354                stateCombo.setSelectedItem(Bundle.getMessage("SensorStateInactive"));
355                break;
356            case jmri.Sensor.INCONSISTENT:
357                stateCombo.setSelectedItem(Bundle.getMessage("BeanStateInconsistent"));
358                break;
359            default:
360                stateCombo.setSelectedItem(Bundle.getMessage("BeanStateUnknown"));
361        }
362
363        JPanel input = new JPanel(); // panel to hold formatted input for dialog
364        input.add(new JLabel(Bundle.getMessage("SensorInitialStateMessageBox")));
365        JPanel stateBoxPane = new JPanel();
366        stateBoxPane.add(stateCombo);
367        input.add(stateBoxPane);
368
369        int retval = JmriJOptionPane.showConfirmDialog(_who,
370                input, Bundle.getMessage("InitialSensorState"),
371                JmriJOptionPane.OK_CANCEL_OPTION, JOptionPane.INFORMATION_MESSAGE);
372        if (retval != JmriJOptionPane.OK_OPTION) {
373            return;
374        }
375        int defaultState = jmri.Sensor.UNKNOWN;
376        String selectedState = (String) stateCombo.getSelectedItem();
377        if (selectedState.equals(Bundle.getMessage("SensorStateActive"))) {
378            defaultState = jmri.Sensor.ACTIVE;
379        } else if (selectedState.equals(Bundle.getMessage("SensorStateInactive"))) {
380            defaultState = jmri.Sensor.INACTIVE;
381        } else if (selectedState.equals(Bundle.getMessage("BeanStateInconsistent"))) {
382            defaultState = jmri.Sensor.INCONSISTENT;
383        }
384
385        jmri.jmrix.internal.InternalSensorManager.setDefaultStateForNewSensors(defaultState);
386    }
387
388    /**
389     * Insert a table specific Defaults menu. Account for the Window and Help
390     * menus, which are already added to the menu bar as part of the creation of
391     * the JFrame, by adding the Tools menu 2 places earlier unless the table is
392     * part of the ListedTableFrame, that adds the Help menu later on.
393     *
394     * @param f the JFrame of this table
395     */
396    @Override
397    public void setMenuBar(BeanTableFrame<Sensor> f) {
398        final jmri.util.JmriJFrame finalF = f; // needed for anonymous ActionListener class
399        JMenuBar menuBar = f.getJMenuBar();
400        // check for menu
401        boolean menuAbsent = true;
402        for (int i = 0; i < menuBar.getMenuCount(); ++i) {
403            String name = menuBar.getMenu(i).getAccessibleContext().getAccessibleName();
404            if (name.equals(Bundle.getMessage("MenuDefaults"))) {
405                // using first menu for check, should be identical to next JMenu Bundle
406                menuAbsent = false;
407                break;
408            }
409        }
410        if (menuAbsent) { // create it
411            JMenu optionsMenu = new JMenu(Bundle.getMessage("MenuDefaults"));
412            JMenuItem item = new JMenuItem(Bundle.getMessage("GlobalDebounce"));
413            optionsMenu.add(item);
414            item.addActionListener((ActionEvent e) -> {
415                setDefaultDebounce(finalF);
416            });
417            item = new JMenuItem(Bundle.getMessage("InitialSensorState"));
418            optionsMenu.add(item);
419            item.addActionListener((ActionEvent e) -> {
420                setDefaultState(finalF);
421            });
422            int pos = menuBar.getMenuCount() - 1; // count the number of menus to insert the TableMenus before 'Window' and 'Help'
423            int offset = 1;
424            log.debug("setMenuBar number of menu items = {}", pos);
425            for (int i = 0; i <= pos; i++) {
426                if (menuBar.getComponent(i) instanceof JMenu) {
427                    if (((AbstractButton) menuBar.getComponent(i)).getText().equals(Bundle.getMessage("MenuHelp"))) {
428                        offset = -1; // correct for use as part of ListedTableAction where the Help Menu is not yet present
429                    }
430                }
431            }
432            menuBar.add(optionsMenu, pos + offset);
433        }
434    }
435
436    @Override
437    protected void configureTable(JTable table){
438        super.configureTable(table);
439        showDebounceBox.addActionListener((ActionEvent e) -> { ((SensorTableDataModel)m).showDebounce(showDebounceBox.isSelected(), table); });
440        showPullUpBox.addActionListener((ActionEvent e) -> { ((SensorTableDataModel)m).showPullUp(showPullUpBox.isSelected(), table); });
441        showStateForgetAndQueryBox.addActionListener((ActionEvent e) -> { ((SensorTableDataModel)m).showStateForgetAndQuery(showStateForgetAndQueryBox.isSelected(), table); });
442    }
443
444    private final TriStateJCheckBox showDebounceBox = new TriStateJCheckBox(Bundle.getMessage("SensorDebounceCheckBox"));
445    private final TriStateJCheckBox showPullUpBox = new TriStateJCheckBox(Bundle.getMessage("SensorPullUpCheckBox"));
446    private final TriStateJCheckBox showStateForgetAndQueryBox = new TriStateJCheckBox(Bundle.getMessage("ShowStateForgetAndQuery"));
447
448    /**
449     * {@inheritDoc}
450     */
451    @Override
452    public void addToFrame(BeanTableFrame<Sensor> f) {
453        f.addToBottomBox(showDebounceBox, this.getClass().getName());
454        showDebounceBox.setToolTipText(Bundle.getMessage("SensorDebounceToolTip"));
455        f.addToBottomBox(showPullUpBox, this.getClass().getName());
456        showPullUpBox.setToolTipText(Bundle.getMessage("SensorPullUpToolTip"));
457        f.addToBottomBox(showStateForgetAndQueryBox, this.getClass().getName());
458        showStateForgetAndQueryBox.setToolTipText(Bundle.getMessage("StateForgetAndQueryBoxToolTip"));
459    }
460
461    /**
462     * Override to update showDebounceBox, showPullUpBox, showStateForgetAndQueryBox.
463     * {@inheritDoc}
464     */
465    @Override
466    protected void columnsVisibleUpdated(boolean[] colsVisible){
467        log.debug("columns updated {}",colsVisible);
468        showDebounceBox.setState(new boolean[]{
469            colsVisible[SensorTableDataModel.ACTIVEDELAY],
470            colsVisible[SensorTableDataModel.INACTIVEDELAY],
471            colsVisible[SensorTableDataModel.USEGLOBALDELAY] });
472        showPullUpBox.setState(new boolean[]{
473            colsVisible[SensorTableDataModel.PULLUPCOL]});
474        showStateForgetAndQueryBox.setState(new boolean[]{
475            colsVisible[SensorTableDataModel.FORGETCOL],
476            colsVisible[SensorTableDataModel.QUERYCOL] });
477    }
478
479    /**
480     * {@inheritDoc}
481     */
482    @Override
483    public void addToPanel(AbstractTableTabAction<Sensor> f) {
484        String connectionName = sensorManager.getMemo().getUserName();
485
486        if (sensorManager.getClass().getName().contains("ProxySensorManager")) {
487            connectionName = "All";
488        }
489        f.addToBottomBox(showDebounceBox, connectionName);
490        showDebounceBox.setToolTipText(Bundle.getMessage("SensorDebounceToolTip"));
491        f.addToBottomBox(showPullUpBox, connectionName);
492        showPullUpBox.setToolTipText(Bundle.getMessage("SensorPullUpToolTip"));
493        f.addToBottomBox(showStateForgetAndQueryBox, connectionName);
494        showStateForgetAndQueryBox.setToolTipText(Bundle.getMessage("StateForgetAndQueryBoxToolTip"));
495    }
496
497    /**
498     * {@inheritDoc}
499     */
500    @Override
501    public void setMessagePreferencesDetails() {
502        InstanceManager.getDefault(jmri.UserPreferencesManager.class).setPreferenceItemDetails(getClassName(), "duplicateUserName", Bundle.getMessage("DuplicateUserNameWarn"));
503        super.setMessagePreferencesDetails();
504    }
505
506    /**
507     * {@inheritDoc}
508     */
509    @Override
510    protected String getClassName() {
511        return SensorTableAction.class.getName();
512    }
513
514    /**
515     * {@inheritDoc}
516     */
517    @Override
518    public String getClassDescription() {
519        return Bundle.getMessage("TitleSensorTable");
520    }
521
522    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SensorTableAction.class);
523
524}