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.trainbuilder.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        String[] item = { itemName };
165        if (_attribute.equals(ROAD)) {
166            if (!OperationsXml.checkFileName(itemName)) { // NOI18N
167                JmriJOptionPane.showMessageDialog(this,
168                        Bundle.getMessage("NameResChar") + NEW_LINE + Bundle.getMessage("ReservedChar"),
169                        MessageFormat.format(errorMessage, new Object[] { _attribute }), JmriJOptionPane.ERROR_MESSAGE);
170                return false;
171            }
172            item = itemName.split(TrainCommon.HYPHEN);
173        }
174        if (_attribute.equals(TYPE)) {
175            // can't have the " & " as part of the type name
176            if (itemName.contains(CarLoad.SPLIT_CHAR)) {
177                JmriJOptionPane.showMessageDialog(this,
178                        Bundle.getMessage("carNameNoAndChar",
179                                CarLoad.SPLIT_CHAR),
180                        MessageFormat.format(errorMessage, new Object[] { _attribute }), JmriJOptionPane.ERROR_MESSAGE);
181                return false;
182            }
183            item = itemName.split(TrainCommon.HYPHEN);
184        }
185        if (item[0].length() > Control.max_len_string_attibute) {
186            JmriJOptionPane.showMessageDialog(this,
187                    Bundle.getMessage("rsAttributeName",
188                            Control.max_len_string_attibute),
189                    MessageFormat.format(errorMessage, new Object[] { _attribute }), JmriJOptionPane.ERROR_MESSAGE);
190            return false;
191        }
192        return true;
193    }
194
195    protected void deleteAttributeName(String deleteItem) {
196        log.debug("delete attribute {}", deleteItem);
197        if (_attribute.equals(ROAD)) {
198            // purge train and locations by using replace
199            InstanceManager.getDefault(CarRoads.class).replaceName(deleteItem, null);
200        }
201        if (_attribute.equals(OWNER)) {
202            InstanceManager.getDefault(CarOwners.class).deleteName(deleteItem);
203        }
204    }
205
206    protected void addAttributeName(String addItem) {
207        if (_attribute.equals(ROAD)) {
208            InstanceManager.getDefault(CarRoads.class).addName(addItem);
209        }
210        if (_attribute.equals(OWNER)) {
211            InstanceManager.getDefault(CarOwners.class).addName(addItem);
212        }
213    }
214
215    protected void replaceItem(String oldItem, String newItem) {
216        if (_attribute.equals(ROAD)) {
217            InstanceManager.getDefault(CarRoads.class).replaceName(oldItem, newItem);
218        }
219        if (_attribute.equals(OWNER)) {
220            InstanceManager.getDefault(CarOwners.class).replaceName(oldItem, newItem);
221        }
222    }
223
224    protected void loadCombobox() {
225        if (_attribute.equals(ROAD)) {
226            comboBox = InstanceManager.getDefault(CarRoads.class).getComboBox();
227            InstanceManager.getDefault(CarRoads.class).addPropertyChangeListener(this);
228        }
229        if (_attribute.equals(OWNER)) {
230            comboBox = InstanceManager.getDefault(CarOwners.class).getComboBox();
231            InstanceManager.getDefault(CarOwners.class).addPropertyChangeListener(this);
232        }
233    }
234
235    public static final String FAILED = "failed";
236
237    public String convertLength(String addItem) {
238        // convert from inches to feet if needed
239        if (addItem.endsWith("\"")) { // NOI18N
240            addItem = addItem.substring(0, addItem.length() - 1);
241            try {
242                double inches = Double.parseDouble(addItem);
243                int feet = (int) (inches * Setup.getScaleRatio() / 12);
244                addItem = Integer.toString(feet);
245            } catch (NumberFormatException e) {
246                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("CanNotConvertFeet"),
247                        Bundle.getMessage("ErrorRsLength"), JmriJOptionPane.ERROR_MESSAGE);
248                return FAILED;
249            }
250            addTextBox.setText(addItem);
251        }
252        if (addItem.endsWith("cm")) { // NOI18N
253            addItem = addItem.substring(0, addItem.length() - 2);
254            try {
255                double cm = Double.parseDouble(addItem);
256                int meter = (int) (cm * Setup.getScaleRatio() / 100);
257                addItem = Integer.toString(meter);
258            } catch (NumberFormatException e) {
259                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("CanNotConvertMeter"),
260                        Bundle.getMessage("ErrorRsLength"), JmriJOptionPane.ERROR_MESSAGE);
261                return FAILED;
262            }
263            addTextBox.setText(addItem);
264        }
265        // confirm that length is a number and less than 10000 feet
266        try {
267            int length = Integer.parseInt(addItem);
268            if (length < 0) {
269                log.error("length ({}) has to be a positive number", addItem);
270                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorRsLength"),
271                        Bundle.getMessage("canNotAdd", _attribute),
272                        JmriJOptionPane.ERROR_MESSAGE);
273                return FAILED;
274            }
275            if (addItem.length() > Control.max_len_string_length_name) {
276                JmriJOptionPane.showMessageDialog(this,
277                        Bundle.getMessage("RsAttribute",
278                                Control.max_len_string_length_name),
279                        Bundle.getMessage("canNotAdd", _attribute),
280                        JmriJOptionPane.ERROR_MESSAGE);
281                return FAILED;
282            }
283        } catch (NumberFormatException e) {
284            log.error("length ({}) is not an integer", addItem);
285            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorRsLength"),
286                    Bundle.getMessage("canNotAdd",  _attribute),
287                    JmriJOptionPane.ERROR_MESSAGE);
288            return FAILED;
289        }
290        return addItem;
291    }
292    
293    @Override
294    protected void comboBoxActionPerformed(java.awt.event.ActionEvent ae) {
295        log.debug("Combo box action");
296        updateAttributeQuanity();
297    }
298    
299    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", justification = "GUI ease of use")
300    public void toggleShowQuanity() {
301        showQuanity = !showQuanity;
302        quanity.setVisible(showQuanity);
303        updateAttributeQuanity();
304    }
305    
306    protected boolean deleteUnused = false;
307    protected boolean cancel = false;
308
309    public void deleteUnusedAttributes() {
310        if (!showQuanity) {
311            toggleShowQuanity();
312        }
313        deleteUnused = true;
314        cancel = false;
315        int items = comboBox.getItemCount() - 1;
316        for (int i = items; i >= 0; i--) {
317            comboBox.setSelectedIndex(i);
318        }
319        deleteUnused = false; // done
320        comboBox.setSelectedIndex(0);
321    }
322    
323    protected void confirmDelete(String item) {
324        if (!cancel) {
325            int results = JmriJOptionPane.showOptionDialog(this,
326                    MessageFormat
327                            .format(Bundle.getMessage("ConfirmDeleteAttribute"), new Object[] { _attribute, item }),
328                    Bundle.getMessage("DeleteAttribute?"), JmriJOptionPane.DEFAULT_OPTION,
329                    JmriJOptionPane.QUESTION_MESSAGE, null, new Object[] { Bundle.getMessage("ButtonYes"),
330                            Bundle.getMessage("ButtonNo"), Bundle.getMessage("ButtonCancel") },
331                    Bundle.getMessage("ButtonYes"));
332            if (results == 0 ) { // array position 0
333                deleteAttributeName((String) comboBox.getSelectedItem());
334            }
335            if (results == 2 || results == JmriJOptionPane.CLOSED_OPTION) { // array position 2 or Dialog closed
336                cancel = true;
337            }
338        }
339    }
340    
341    protected abstract void updateAttributeQuanity();
342
343    @Override
344    public void dispose() {
345        InstanceManager.getDefault(CarRoads.class).removePropertyChangeListener(this);
346        InstanceManager.getDefault(CarOwners.class).removePropertyChangeListener(this);
347        firePropertyChange(DISPOSE, _attribute, null);
348        super.dispose();
349    }
350
351    @Override
352    public void propertyChange(java.beans.PropertyChangeEvent e) {
353        if (e.getPropertyName().equals(CarRoads.CARROADS_CHANGED_PROPERTY)) {
354            InstanceManager.getDefault(CarRoads.class).updateComboBox(comboBox);
355        }
356        if (e.getPropertyName().equals(CarOwners.CAROWNERS_CHANGED_PROPERTY)) {
357            InstanceManager.getDefault(CarOwners.class).updateComboBox(comboBox);
358        }
359        comboBox.setSelectedItem(addTextBox.getText().trim()); // has to be the last line for propertyChange
360    }
361
362    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RollingStockAttributeEditFrame.class);
363}