001package jmri.jmrit.beantable;
002
003import java.awt.Color;
004import java.awt.event.ActionEvent;
005import java.awt.event.ActionListener;
006import javax.annotation.Nonnull;
007import javax.swing.*;
008
009import jmri.InstanceManager;
010import jmri.Manager;
011import jmri.StringIO;
012import jmri.StringIOManager;
013import jmri.UserPreferencesManager;
014import jmri.swing.ManagerComboBox;
015import jmri.swing.SystemNameValidator;
016import jmri.util.JmriJFrame;
017import jmri.util.swing.JmriJOptionPane;
018
019/**
020 * Swing action to create and register a StringIOTable GUI.
021 *
022 * @author Bob Jacobsen Copyright (C) 2024
023 */
024public class StringIOTableAction extends AbstractTableAction<StringIO> {
025
026    /**
027     * Create an action with a specific title.
028     * <p>
029     * Note that the argument is the Action title, not the title of the
030     * resulting frame. Perhaps this should be changed?
031     *
032     * @param actionName title of the action
033     */
034    public StringIOTableAction(String actionName) {
035        super(actionName);
036
037        // disable ourself if there is no primary StringIO manager available
038        if (stringIOManager == null) {
039            super.setEnabled(false);
040        }
041    }
042
043    public StringIOTableAction() {
044        this(Bundle.getMessage("TitleStringIOTable"));
045    }
046
047    protected StringIOManager stringIOManager = InstanceManager.getDefault(StringIOManager.class);
048
049    /**
050     * {@inheritDoc}
051     */
052    @Override
053    public void setManager(@Nonnull Manager<StringIO> man) {
054        if (man instanceof StringIOManager) {
055            stringIOManager = (StringIOManager) man;
056        }
057    }
058
059
060    /**
061     * Create the JTable DataModel, along with the changes for the specific case
062     * of StringIOs.
063     */
064    @Override
065    protected void createModel() {
066        m = new StringIOTableDataModel(stringIOManager);
067    }
068
069    /**
070     * {@inheritDoc}
071     */
072    @Override
073    protected void setTitle() {
074        f.setTitle(Bundle.getMessage("TitleStringIOTable"));
075    }
076
077    /**
078     * {@inheritDoc}
079     */
080    @Override
081    protected String helpTarget() {
082        return "package.jmri.jmrit.beantable.StringIOTable";
083    }
084
085    private JmriJFrame addFrame = null;
086    private final JTextField hardwareAddressTextField = new JTextField(20);
087    private final JTextField userNameTextField = new JTextField(20);
088    
089    private final ManagerComboBox<StringIO> prefixBox = new ManagerComboBox<>();
090    
091    
092    private final SpinnerNumberModel rangeSpinner = new SpinnerNumberModel(1, 1, 100, 1); // maximum 100 items
093    private final JSpinner numberToAddSpinner = new JSpinner(rangeSpinner);
094    private final JCheckBox rangeCheckBox = new JCheckBox(Bundle.getMessage("AddRangeBox"));
095    private final String systemSelectionCombo = this.getClass().getName() + ".SystemSelected";
096    private JButton addButton;
097    private final JLabel statusBarLabel = new JLabel(Bundle.getMessage("HardwareAddStatusEnter"), JLabel.LEADING);
098    private Manager<StringIO> connectionChoice = null;
099    private UserPreferencesManager pref;
100    private SystemNameValidator hardwareAddressValidator;
101
102    /**
103     * {@inheritDoc}
104     */
105    @Override
106    protected void addPressed(ActionEvent e) {
107        pref = InstanceManager.getDefault(UserPreferencesManager.class);
108        if (addFrame == null) {
109            addFrame = new JmriJFrame(Bundle.getMessage("TitleAddStringIO"), false, true);
110            addFrame.addHelpMenu("package.jmri.jmrit.beantable.StringIOAddEdit", true);
111            ActionListener createListener = this::createPressed;
112            ActionListener cancelListener = this::cancelPressed;
113            ActionListener rangeListener = this::canAddRange;
114            configureManagerComboBox(prefixBox, stringIOManager, StringIOManager.class);
115            userNameTextField.setName("userName"); // NOI18N
116            prefixBox.setName("prefixBox"); // NOI18N
117            addButton = new JButton(Bundle.getMessage("ButtonCreate"));
118            addButton.addActionListener(createListener);
119
120            if (hardwareAddressValidator==null){
121                hardwareAddressValidator = new SystemNameValidator(hardwareAddressTextField, java.util.Objects.requireNonNull(prefixBox.getSelectedItem(), "encountered null system selection"), true);
122            } else {
123                log.trace("on add, prefixBox.getSelected is {}", prefixBox.getSelectedItem());
124                hardwareAddressValidator.setManager(prefixBox.getSelectedItem());
125            }
126
127            // create panel
128            addFrame.add(new AddNewHardwareDevicePanel(hardwareAddressTextField, hardwareAddressValidator, userNameTextField, prefixBox,
129                    numberToAddSpinner, rangeCheckBox, addButton, cancelListener, rangeListener, statusBarLabel));
130            // tooltip for hardwareAddressTextField will be assigned next by canAddRange()
131            canAddRange(null);
132        }
133        hardwareAddressTextField.setName("sysName"); // for GUI test NOI18N
134        hardwareAddressTextField.setName("hwAddressTextField"); // for GUI test NOI18N
135        addButton.setName("createButton"); // for GUI test NOI18N
136        // reset statusBarLabel text
137        statusBarLabel.setText(Bundle.getMessage("HardwareAddStatusEnter"));
138        statusBarLabel.setForeground(Color.gray);
139        addFrame.setEscapeKeyClosesWindow(true);
140        addFrame.getRootPane().setDefaultButton(addButton);
141        addFrame.pack();
142        addFrame.setVisible(true);
143    }
144
145    void cancelPressed(ActionEvent e) {
146        removePrefixBoxListener(prefixBox);
147        addFrame.setVisible(false);
148        addFrame.dispose();
149        addFrame = null;
150    }
151
152    /**
153     * Respond to Create new item button pressed on Add StringIO pane.
154     *
155     * @param e the click event
156     */
157    void createPressed(ActionEvent e) {
158
159        int numberOfStringIOs = 1;
160
161        if (rangeCheckBox.isSelected()) {
162            numberOfStringIOs = (Integer) numberToAddSpinner.getValue();
163        }
164        if (numberOfStringIOs >= 65 // limited by JSpinnerModel to 100
165            && JmriJOptionPane.showConfirmDialog(addFrame,
166                Bundle.getMessage("WarnExcessBeans", Bundle.getMessage("StringIOs"), numberOfStringIOs),
167                Bundle.getMessage("WarningTitle"),
168                JmriJOptionPane.YES_NO_OPTION ) != JmriJOptionPane.YES_OPTION ) {
169            return;
170        }
171        String rName;
172        String stringIOPrefix = prefixBox.getSelectedItem().getSystemPrefix(); 
173        String curAddress = hardwareAddressTextField.getText();
174        // initial check for empty entry
175        if (curAddress.isEmpty()) {
176            statusBarLabel.setText(Bundle.getMessage("WarningEmptyHardwareAddress"));
177            statusBarLabel.setForeground(Color.red);
178            hardwareAddressTextField.setBackground(Color.red);
179            return;
180        } else {
181            hardwareAddressTextField.setBackground(Color.white);
182        }
183
184        // Add some entry pattern checking, before assembling sName and handing it to the StringIOManager
185        StringBuilder statusMessage = new StringBuilder(
186            Bundle.getMessage("ItemCreateFeedback", Bundle.getMessage("BeanNameStringIO")));
187        String uName = userNameTextField.getText();
188
189
190        // Compose the proposed system name from parts:
191        rName = stringIOPrefix + stringIOManager.typeLetter() + curAddress;
192        log.trace("will create first with name {}", rName);
193        
194        for (int x = 0; x < numberOfStringIOs; x++) {
195
196            // create the next StringIO
197            StringIO r;
198            try {
199                r = stringIOManager.provideStringIO(rName);
200                log.trace("created {} from {}", r, stringIOManager);
201            } catch (IllegalArgumentException ex) {
202                // user input no good
203                handleCreateException(ex, rName); // displays message dialog to the user
204                return; // without creating
205            }
206
207            // handle setting user name
208            if (!uName.isEmpty()) {
209                if ((stringIOManager.getByUserName(uName) == null)) {
210                    r.setUserName(uName);
211                } else {
212                    pref.showErrorMessage(Bundle.getMessage("ErrorTitle"),
213                            Bundle.getMessage("ErrorDuplicateUserName", uName),
214                            getClassName(), "duplicateUserName", false, true);
215                }
216            }
217
218            // add first and last names to statusMessage user feedback string
219            // only mention first and last of rangeCheckBox added
220            if (x == 0 || x == numberOfStringIOs - 1) {
221                statusMessage.append(" ").append(rName).append(" (").append(uName).append(")");
222            }
223            if (x == numberOfStringIOs - 2) {
224                statusMessage.append(" ").append(Bundle.getMessage("ItemCreateUpTo")).append(" ");
225            }
226
227            // except on last pass
228            if (x < numberOfStringIOs-1) {
229                // bump system name
230                try {
231                    rName = InstanceManager.getDefault(StringIOManager.class).getNextValidSystemName(r);
232                } catch (jmri.JmriException ex) {
233                    displayHwError(r.getSystemName(), ex);
234                    // directly add to statusBarLabel (but never called?)
235                    statusBarLabel.setText(Bundle.getMessage("ErrorConvertHW", rName));
236                    statusBarLabel.setForeground(Color.red);
237                    return;
238                }
239
240                // bump user name
241                if (!uName.isEmpty()) {
242                    uName = nextName(uName);
243                }
244            }
245            // end of for loop creating rangeCheckBox of StringIOs
246        }
247        // provide success feedback to uName
248        statusBarLabel.setText(statusMessage.toString());
249        statusBarLabel.setForeground(Color.gray);
250
251        pref.setComboBoxLastSelection(systemSelectionCombo, prefixBox.getSelectedItem().getMemo().getUserName());
252        removePrefixBoxListener(prefixBox);
253        addFrame.setVisible(false);
254        addFrame.dispose();
255        addFrame = null;
256    }
257
258    private String addEntryToolTip;
259
260    /**
261     * Activate Add a rangeCheckBox option if manager accepts adding more than 1
262     * StringIO and set a manager specific tooltip on the AddNewHardwareDevice
263     * pane.
264     */
265    private void canAddRange(ActionEvent e) {
266        rangeCheckBox.setEnabled(false);
267        rangeCheckBox.setSelected(false);
268        if (prefixBox.getSelectedIndex() == -1) {
269            prefixBox.setSelectedIndex(0);
270        }
271        connectionChoice = prefixBox.getSelectedItem(); // store in Field for CheckedTextField
272        String systemPrefix = connectionChoice.getSystemPrefix();
273        rangeCheckBox.setEnabled(((StringIOManager) connectionChoice).allowMultipleAdditions(systemPrefix));
274        addEntryToolTip = connectionChoice.getEntryToolTip();
275        // show hwAddressTextField field tooltip in the Add StringIO pane that matches system connection selected from combobox
276        hardwareAddressTextField.setToolTipText(
277                Bundle.getMessage("AddEntryToolTipLine1",
278                        connectionChoice.getMemo().getUserName(),
279                        Bundle.getMessage("StringIOs"),
280                        addEntryToolTip));
281        hardwareAddressValidator.setToolTipText(hardwareAddressTextField.getToolTipText());
282        hardwareAddressValidator.verify(hardwareAddressTextField);
283    }
284
285    void handleCreateException(Exception ex, String sysName) {
286        statusBarLabel.setText(ex.getLocalizedMessage());
287        statusBarLabel.setForeground(Color.red);
288        String err = Bundle.getMessage("ErrorBeanCreateFailed",
289            InstanceManager.getDefault(StringIOManager.class).getBeanTypeHandled(),sysName);
290        JmriJOptionPane.showMessageDialog(addFrame, err + "\n" + ex.getLocalizedMessage(),
291                err, JmriJOptionPane.ERROR_MESSAGE);
292    }
293
294    /**
295     * {@inheritDoc}
296     */
297    @Override
298    protected String getClassName() {
299        return StringIOTableAction.class.getName();
300    }
301
302    /**
303     * {@inheritDoc}
304     */
305    @Override
306    public String getClassDescription() {
307        return Bundle.getMessage("TitleStringIOTable");
308    }
309
310    @Override
311    public void setMessagePreferencesDetails() {
312        InstanceManager.getDefault(jmri.UserPreferencesManager.class).
313                setPreferenceItemDetails(getClassName(), "duplicateUserName", Bundle.getMessage("DuplicateUserNameWarn"));  // NOI18N
314        super.setMessagePreferencesDetails();
315    }
316
317    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(StringIOTableAction.class);
318
319}