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