001package jmri.jmrit.operations.rollingstock;
002
003import java.awt.Dimension;
004import java.awt.GridBagLayout;
005import java.text.MessageFormat;
006
007import javax.swing.*;
008
009import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
010import jmri.InstanceManager;
011import jmri.jmrit.operations.OperationsFrame;
012import jmri.jmrit.operations.OperationsXml;
013import jmri.jmrit.operations.rollingstock.cars.*;
014import jmri.jmrit.operations.setup.Control;
015import jmri.jmrit.operations.setup.Setup;
016import jmri.jmrit.operations.trains.TrainCommon;
017import jmri.util.swing.JmriJOptionPane;
018
019/**
020 * Frame for editing a rolling stock attribute.
021 *
022 * @author Daniel Boudreau Copyright (C) 2020
023 */
024public abstract class RollingStockAttributeEditFrame extends OperationsFrame implements java.beans.PropertyChangeListener {
025
026    // labels
027    public JLabel textAttribute = new JLabel();
028    JLabel textSep = new JLabel();
029    public JLabel quanity = new JLabel("0");
030
031    // major buttons
032    public JButton addButton = new JButton(Bundle.getMessage("Add"));
033    public JButton deleteButton = new JButton(Bundle.getMessage("ButtonDelete"));
034    public JButton replaceButton = new JButton(Bundle.getMessage("Replace"));
035
036    // combo box
037    public JComboBox<String> comboBox;
038
039    // text box
040    public JTextField addTextBox = new JTextField(Control.max_len_string_attibute);
041
042    // ROAD and OWNER are the only two attributes shared between Cars and Engines
043    public static final String ROAD = "Road";
044    public static final String OWNER = "Owner";
045    public static final String TYPE = "Type"; // cars and engines have different types
046    public static final String LENGTH = "Length"; // cars and engines have different lengths
047
048    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings("MS_CANNOT_BE_FINAL") // needs access in subpackage
049    protected static boolean showDialogBox = true;
050    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings("MS_CANNOT_BE_FINAL") // needs access in subpackage
051    protected static boolean showQuanity = false;
052
053    // property change
054    public static final String DISPOSE = "dispose"; // NOI18N
055
056    public RollingStockAttributeEditFrame() {
057    }
058
059    public String _attribute; // used to determine which attribute is being edited
060
061    public void initComponents(String attribute, String name) {
062        getContentPane().removeAll();
063
064        // track which attribute is being edited
065        _attribute = attribute;
066        loadCombobox();
067        comboBox.setSelectedItem(name);
068
069        // general GUI config
070        getContentPane().setLayout(new GridBagLayout());
071
072        textAttribute.setText(attribute);
073        quanity.setVisible(showQuanity);
074
075        // row 1
076        addItem(textAttribute, 2, 1);
077        // row 2
078        addItem(addTextBox, 2, 2);
079        addItem(addButton, 3, 2);
080
081        // row 3
082        addItem(quanity, 1, 3);
083        addItem(comboBox, 2, 3);
084        addItem(deleteButton, 3, 3);
085
086        // row 4
087        addItem(replaceButton, 3, 4);
088
089        addButtonAction(addButton);
090        addButtonAction(deleteButton);
091        addButtonAction(replaceButton);
092        
093        addComboBoxAction(comboBox);
094        
095        updateAttributeQuanity();
096
097        deleteButton.setToolTipText(
098                Bundle.getMessage("TipDeleteAttributeName", attribute));
099        addButton.setToolTipText(
100                Bundle.getMessage("TipAddAttributeName", attribute));
101        replaceButton.setToolTipText(
102                Bundle.getMessage("TipReplaceAttributeName", attribute));
103
104        initMinimumSize(new Dimension(Control.panelWidth400, Control.panelHeight250));
105    }
106
107    // add, delete, or replace button
108    @Override
109    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", justification = "GUI ease of use")
110    public void buttonActionPerformed(java.awt.event.ActionEvent ae) {
111        log.debug("edit frame button activated");
112        if (ae.getSource() == addButton) {
113            if (!checkItemName(Bundle.getMessage("canNotAdd"))) {
114                return;
115            }
116            addAttributeName(addTextBox.getText().trim());
117        }
118        if (ae.getSource() == deleteButton) {
119            deleteAttributeName((String) comboBox.getSelectedItem());
120        }
121        if (ae.getSource() == replaceButton) {
122            if (!checkItemName(Bundle.getMessage("canNotReplace"))) {
123                return;
124            }
125            String newItem = addTextBox.getText().trim();
126            String oldItem = (String) comboBox.getSelectedItem();
127            if (JmriJOptionPane.showConfirmDialog(this,
128                    Bundle.getMessage("replaceMsg", oldItem, newItem),
129                    Bundle.getMessage("replaceAll"), JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION) {
130                return;
131            }
132            if (newItem.equals(oldItem)) {
133                return;
134            }
135            // don't show dialog, save current state
136            boolean oldShow = showDialogBox;
137            showDialogBox = false;
138            addAttributeName(newItem);
139            showDialogBox = oldShow;
140            replaceItem(oldItem, newItem);
141            deleteAttributeName(oldItem);
142        }
143    }
144
145    protected boolean checkItemName(String errorMessage) {
146        String itemName = addTextBox.getText().trim();
147        if (itemName.isEmpty()) {
148            return false;
149        }
150        // hyphen feature needs at least one character to work properly
151        if (itemName.contains(TrainCommon.HYPHEN)) {
152            String[] s = itemName.split(TrainCommon.HYPHEN);
153            if (s.length == 0) {
154                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("HyphenFeature"),
155                        MessageFormat.format(errorMessage, new Object[] { _attribute }), JmriJOptionPane.ERROR_MESSAGE);
156                return false;
157            }
158        }
159        if (_attribute.equals(LENGTH)) {
160            if (convertLength(itemName).equals(FAILED)) {
161                return false;
162            }
163        }
164        if (_attribute.equals(ROAD)) {
165            if (!OperationsXml.checkFileName(itemName)) { // NOI18N
166                JmriJOptionPane.showMessageDialog(this,
167                        Bundle.getMessage("NameResChar") + NEW_LINE + Bundle.getMessage("ReservedChar"),
168                        MessageFormat.format(errorMessage, new Object[] { _attribute }), JmriJOptionPane.ERROR_MESSAGE);
169                return false;
170            }
171        }
172        String[] item = { itemName };
173        if (_attribute.equals(TYPE)) {
174            // can't have the " & " as part of the type name
175            if (itemName.contains(CarLoad.SPLIT_CHAR)) {
176                JmriJOptionPane.showMessageDialog(this,
177                        Bundle.getMessage("carNameNoAndChar",
178                                CarLoad.SPLIT_CHAR),
179                        MessageFormat.format(errorMessage, new Object[] { _attribute }), JmriJOptionPane.ERROR_MESSAGE);
180                return false;
181            }
182            item = itemName.split(TrainCommon.HYPHEN);
183        }
184        if (item[0].length() > Control.max_len_string_attibute) {
185            JmriJOptionPane.showMessageDialog(this,
186                    Bundle.getMessage("rsAttributeName",
187                            Control.max_len_string_attibute),
188                    MessageFormat.format(errorMessage, new Object[] { _attribute }), JmriJOptionPane.ERROR_MESSAGE);
189            return false;
190        }
191        return true;
192    }
193
194    protected void deleteAttributeName(String deleteItem) {
195        log.debug("delete attribute {}", deleteItem);
196        if (_attribute.equals(ROAD)) {
197            // purge train and locations by using replace
198            InstanceManager.getDefault(CarRoads.class).replaceName(deleteItem, null);
199        }
200        if (_attribute.equals(OWNER)) {
201            InstanceManager.getDefault(CarOwners.class).deleteName(deleteItem);
202        }
203    }
204
205    protected void addAttributeName(String addItem) {
206        if (_attribute.equals(ROAD)) {
207            InstanceManager.getDefault(CarRoads.class).addName(addItem);
208        }
209        if (_attribute.equals(OWNER)) {
210            InstanceManager.getDefault(CarOwners.class).addName(addItem);
211        }
212    }
213
214    protected void replaceItem(String oldItem, String newItem) {
215        if (_attribute.equals(ROAD)) {
216            InstanceManager.getDefault(CarRoads.class).replaceName(oldItem, newItem);
217        }
218        if (_attribute.equals(OWNER)) {
219            InstanceManager.getDefault(CarOwners.class).replaceName(oldItem, newItem);
220        }
221    }
222
223    protected void loadCombobox() {
224        if (_attribute.equals(ROAD)) {
225            comboBox = InstanceManager.getDefault(CarRoads.class).getComboBox();
226            InstanceManager.getDefault(CarRoads.class).addPropertyChangeListener(this);
227        }
228        if (_attribute.equals(OWNER)) {
229            comboBox = InstanceManager.getDefault(CarOwners.class).getComboBox();
230            InstanceManager.getDefault(CarOwners.class).addPropertyChangeListener(this);
231        }
232    }
233
234    public static final String FAILED = "failed";
235
236    public String convertLength(String addItem) {
237        // convert from inches to feet if needed
238        if (addItem.endsWith("\"")) { // NOI18N
239            addItem = addItem.substring(0, addItem.length() - 1);
240            try {
241                double inches = Double.parseDouble(addItem);
242                int feet = (int) (inches * Setup.getScaleRatio() / 12);
243                addItem = Integer.toString(feet);
244            } catch (NumberFormatException e) {
245                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("CanNotConvertFeet"),
246                        Bundle.getMessage("ErrorRsLength"), JmriJOptionPane.ERROR_MESSAGE);
247                return FAILED;
248            }
249            addTextBox.setText(addItem);
250        }
251        if (addItem.endsWith("cm")) { // NOI18N
252            addItem = addItem.substring(0, addItem.length() - 2);
253            try {
254                double cm = Double.parseDouble(addItem);
255                int meter = (int) (cm * Setup.getScaleRatio() / 100);
256                addItem = Integer.toString(meter);
257            } catch (NumberFormatException e) {
258                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("CanNotConvertMeter"),
259                        Bundle.getMessage("ErrorRsLength"), JmriJOptionPane.ERROR_MESSAGE);
260                return FAILED;
261            }
262            addTextBox.setText(addItem);
263        }
264        // confirm that length is a number and less than 10000 feet
265        try {
266            int length = Integer.parseInt(addItem);
267            if (length < 0) {
268                log.error("length ({}) has to be a positive number", addItem);
269                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorRsLength"),
270                        Bundle.getMessage("canNotAdd", _attribute),
271                        JmriJOptionPane.ERROR_MESSAGE);
272                return FAILED;
273            }
274            if (addItem.length() > Control.max_len_string_length_name) {
275                JmriJOptionPane.showMessageDialog(this,
276                        Bundle.getMessage("RsAttribute",
277                                Control.max_len_string_length_name),
278                        Bundle.getMessage("canNotAdd", _attribute),
279                        JmriJOptionPane.ERROR_MESSAGE);
280                return FAILED;
281            }
282        } catch (NumberFormatException e) {
283            log.error("length ({}) is not an integer", addItem);
284            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorRsLength"),
285                    Bundle.getMessage("canNotAdd",  _attribute),
286                    JmriJOptionPane.ERROR_MESSAGE);
287            return FAILED;
288        }
289        return addItem;
290    }
291    
292    @Override
293    protected void comboBoxActionPerformed(java.awt.event.ActionEvent ae) {
294        log.debug("Combo box action");
295        updateAttributeQuanity();
296    }
297    
298    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", justification = "GUI ease of use")
299    public void toggleShowQuanity() {
300        showQuanity = !showQuanity;
301        quanity.setVisible(showQuanity);
302        updateAttributeQuanity();
303    }
304    
305    protected boolean deleteUnused = false;
306    protected boolean cancel = false;
307
308    public void deleteUnusedAttributes() {
309        if (!showQuanity) {
310            toggleShowQuanity();
311        }
312        deleteUnused = true;
313        cancel = false;
314        int items = comboBox.getItemCount() - 1;
315        for (int i = items; i >= 0; i--) {
316            comboBox.setSelectedIndex(i);
317        }
318        deleteUnused = false; // done
319        comboBox.setSelectedIndex(0);
320    }
321    
322    protected void confirmDelete(String item) {
323        if (!cancel) {
324            int results = JmriJOptionPane.showOptionDialog(this,
325                    MessageFormat
326                            .format(Bundle.getMessage("ConfirmDeleteAttribute"), new Object[] { _attribute, item }),
327                    Bundle.getMessage("DeleteAttribute?"), JmriJOptionPane.DEFAULT_OPTION,
328                    JmriJOptionPane.QUESTION_MESSAGE, null, new Object[] { Bundle.getMessage("ButtonYes"),
329                            Bundle.getMessage("ButtonNo"), Bundle.getMessage("ButtonCancel") },
330                    Bundle.getMessage("ButtonYes"));
331            if (results == 0 ) { // array position 0
332                deleteAttributeName((String) comboBox.getSelectedItem());
333            }
334            if (results == 2 || results == JmriJOptionPane.CLOSED_OPTION) { // array position 2 or Dialog closed
335                cancel = true;
336            }
337        }
338    }
339    
340    protected abstract void updateAttributeQuanity();
341
342    @Override
343    public void dispose() {
344        InstanceManager.getDefault(CarRoads.class).removePropertyChangeListener(this);
345        InstanceManager.getDefault(CarOwners.class).removePropertyChangeListener(this);
346        firePropertyChange(DISPOSE, _attribute, null);
347        super.dispose();
348    }
349
350    @Override
351    public void propertyChange(java.beans.PropertyChangeEvent e) {
352        if (e.getPropertyName().equals(CarRoads.CARROADS_CHANGED_PROPERTY)) {
353            InstanceManager.getDefault(CarRoads.class).updateComboBox(comboBox);
354        }
355        if (e.getPropertyName().equals(CarOwners.CAROWNERS_CHANGED_PROPERTY)) {
356            InstanceManager.getDefault(CarOwners.class).updateComboBox(comboBox);
357        }
358        comboBox.setSelectedItem(addTextBox.getText().trim()); // has to be the last line for propertyChange
359    }
360
361    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RollingStockAttributeEditFrame.class);
362}