001package jmri.jmrit.ctc.editor.code;
002
003import java.awt.Component;
004import java.awt.event.ActionEvent;
005import java.awt.event.ActionListener;
006import java.io.IOException;
007import java.util.ArrayList;
008import java.util.Collections;
009import java.util.Enumeration;
010import java.util.Vector;
011
012import javax.swing.AbstractButton;
013import javax.swing.ButtonGroup;
014import javax.swing.DefaultComboBoxModel;
015import javax.swing.JComboBox;
016import javax.swing.JFormattedTextField;
017import javax.swing.JTextField;
018import javax.swing.table.DefaultTableModel;
019import javax.swing.text.NumberFormatter;
020
021import jmri.InstanceManager;
022import jmri.BlockManager;
023import jmri.SensorManager;
024import jmri.SignalHeadManager;
025import jmri.SignalMastManager;
026import jmri.TurnoutManager;
027import jmri.jmrit.ctc.CtcManager;
028import jmri.jmrit.ctc.NBHSensor;
029import jmri.jmrit.ctc.NBHSignal;
030import jmri.jmrit.ctc.NBHTurnout;
031import jmri.jmrit.ctc.ctcserialdata.CTCSerialData;
032import jmri.jmrit.ctc.ctcserialdata.CodeButtonHandlerData;
033import jmri.jmrit.ctc.ctcserialdata.ProjectsCommonSubs;
034import jmri.util.swing.JmriJOptionPane;
035
036import org.apache.commons.csv.CSVFormat;
037import org.apache.commons.csv.CSVPrinter;
038
039/**
040 *
041 * @author Gregory J. Bedlek Copyright (C) 2018, 2019
042 */
043public class CommonSubs {
044
045//  For GUI editor routines that need this:
046    public static void setMillisecondsEdit(JFormattedTextField formattedTextField) {
047        NumberFormatter numberFormatter = (NumberFormatter) formattedTextField.getFormatter();
048        numberFormatter.setMinimum(0);
049        numberFormatter.setMaximum(120000);
050        numberFormatter.setAllowsInvalid(false);
051    }
052
053//  This routine will return 0 if error encountered parsing string.  Since all GUI int fields that use this
054//  routine already should have a NumberFormatter attached to it already (see "setMillisecondsEdit" above),
055//  technically these should NEVER throw!
056    public static int getIntFromJTextFieldNoThrow(JTextField textField) { return ProjectsCommonSubs.getIntFromStringNoThrow(textField.getText(), 0); }
057
058    public static boolean allowClose(Component parentComponent, boolean dataChanged) {
059        if (dataChanged) {
060            return JmriJOptionPane.showConfirmDialog(parentComponent, Bundle.getMessage("CommonSubsDataModified"),
061                    Bundle.getMessage("WarningTitle"), JmriJOptionPane.YES_NO_OPTION) == JmriJOptionPane.YES_OPTION;  // NOI18N
062        }
063        return true;    // NO change, ok to exit
064    }
065
066    /**
067     * Add a standard help menu, including the window specific help item.
068     *
069     * @param frame  The frame receiving the help menu.
070     * @param ref    JHelp reference for the desired window-specific help page
071     * @param direct true if the help main-menu item goes directly to the help system,
072     *               such as when there are no items in the help menu
073     */
074    public static void addHelpMenu(javax.swing.JFrame frame, String ref, boolean direct) {
075        javax.swing.JMenuBar bar = frame.getJMenuBar();
076        if (bar == null) {
077            bar = new javax.swing.JMenuBar();
078        }
079        jmri.util.HelpUtil.helpMenu(bar, ref, direct);
080        frame.setJMenuBar(bar);
081    }
082
083//  If the table model value is null, that is the same as "".  This also "compacts"
084//  the entries also (i.e. blank line(s) between entries are removed):
085    public static String getCSVStringFromDefaultTableModel(DefaultTableModel defaultTableModel) {
086        ArrayList<String> entries = new ArrayList<>();
087        for (int sourceIndex = 0; sourceIndex < defaultTableModel.getRowCount(); sourceIndex++) {
088            Object object = defaultTableModel.getValueAt(sourceIndex, 0);
089            if (object != null) {
090                String entry = object.toString().trim();
091                if (!entry.isEmpty()) { // Do a "compact" on the fly:
092                    entries.add(entry);
093                }
094            }
095        }
096        try (CSVPrinter printer = new CSVPrinter(new StringBuilder(),
097                CSVFormat.Builder.create(CSVFormat.DEFAULT)
098                        .setQuote(null).setRecordSeparator(null).build())) {
099            printer.printRecord(entries);
100            return printer.getOut().toString();
101        } catch (IOException ex) {
102            log.error("Unable to create CSV", ex);
103            return "";
104        }
105    }
106
107//  If the table model value is null, that is the same as "".  This also "compacts"
108//  the entries also (i.e. blank line(s) between entries are removed):
109    public static ArrayList<String> getStringArrayFromDefaultTableModel(DefaultTableModel defaultTableModel) {
110        ArrayList<String> entries = new ArrayList<>();
111        for (int sourceIndex = 0; sourceIndex < defaultTableModel.getRowCount(); sourceIndex++) {
112            Object object = defaultTableModel.getValueAt(sourceIndex, 0);
113            if (object != null) {
114                String entry = object.toString().trim();
115                if (!entry.isEmpty()) { // Do a "compact" on the fly:
116                    entries.add(entry);
117                }
118            }
119        }
120        return entries;
121    }
122
123    public static int compactDefaultTableModel(DefaultTableModel defaultTableModel) {
124        int destIndex = 0;
125        int lastSourceIndexNonEmpty = -1;   // Indicate none found
126        int count = 0;
127        for (int sourceIndex = 0; sourceIndex < defaultTableModel.getRowCount(); sourceIndex++) {
128            Object object = defaultTableModel.getValueAt(sourceIndex, 0);
129            if (object != null) {
130                String entry = object.toString().trim();
131                entry = entry.trim();
132                if (!entry.isEmpty()) {
133                    lastSourceIndexNonEmpty = sourceIndex;
134                    defaultTableModel.setValueAt(entry, destIndex++, 0);
135                    count++;
136                }
137            }
138        }
139        if (-1 != lastSourceIndexNonEmpty) { // Something in table, MAY need to clear out rows at end:
140            while (destIndex <= lastSourceIndexNonEmpty) {
141                defaultTableModel.setValueAt("", destIndex++, 0);
142            }
143        }
144        return count; // Return number of entries encountered.
145    }
146
147//  This creates a sorted ArrayList (so that we can easily load it into a "DefaultComboBoxModel") containing all
148//  Switch Direction Indicators:
149    public static ArrayList<String> getArrayListOfSelectableSwitchDirectionIndicators(ArrayList<CodeButtonHandlerData> codeButtonHandlerDataList) {
150        ArrayList<String> returnValue = new ArrayList<>();
151        for (CodeButtonHandlerData codeButtonHandlerData : codeButtonHandlerDataList) {
152            if (!codeButtonHandlerData._mSWDI_NormalInternalSensor.getHandleName().isEmpty()) {
153                returnValue.add(codeButtonHandlerData._mSWDI_NormalInternalSensor.getHandleName());
154            }
155            if (!codeButtonHandlerData._mSWDI_ReversedInternalSensor.getHandleName().isEmpty()) {
156                returnValue.add(codeButtonHandlerData._mSWDI_ReversedInternalSensor.getHandleName());
157            }
158        }
159//      Collections.sort(returnValue);
160        return returnValue;
161    }
162
163    public static ArrayList<Integer> getArrayListOfSelectableOSSectionUniqueIDs(ArrayList<CodeButtonHandlerData> codeButtonHandlerDataList) {
164        ArrayList<Integer> returnValue = new ArrayList<>();
165        for (CodeButtonHandlerData codeButtonHandlerData : codeButtonHandlerDataList) {
166            returnValue.add(codeButtonHandlerData._mUniqueID);
167        }
168        return returnValue;
169    }
170
171    public static void populateJComboBoxWithColumnDescriptionsAndSelectViaUniqueID(JComboBox<String> jComboBox, CTCSerialData ctcSerialData, int uniqueID) {
172        populateJComboBoxWithColumnDescriptions(jComboBox, ctcSerialData);
173        setSelectedIndexOfJComboBoxViaUniqueID(jComboBox, ctcSerialData, uniqueID);
174    }
175
176//  A blank entry will ALWAYS appear as the first selection.
177    public static void populateJComboBoxWithColumnDescriptions(JComboBox<String> jComboBox, CTCSerialData ctcSerialData) {
178        ArrayList<String> userDescriptions = new ArrayList<>();
179        userDescriptions.add("");   // None can be specified.
180        ArrayList<Integer> arrayListOfSelectableOSSectionUniqueIDs = getArrayListOfSelectableOSSectionUniqueIDs(ctcSerialData.getCodeButtonHandlerDataArrayList());
181        for (Integer uniqueID : arrayListOfSelectableOSSectionUniqueIDs) {
182            userDescriptions.add(ctcSerialData.getMyShortStringNoCommaViaUniqueID(uniqueID));
183        }
184//      Collections.sort(userDescriptions);
185        jComboBox.setModel(new DefaultComboBoxModel<>(new Vector<>(userDescriptions)));
186    }
187
188//  NO blank entry as the first selection, returns true if any in list, else false.
189//  Also populates "uniqueIDS" with corresponding values.
190    public static boolean populateJComboBoxWithColumnDescriptionsExceptOurs(JComboBox<String> jComboBox, CTCSerialData ctcSerialData, int ourUniqueID, ArrayList<Integer> uniqueIDS) {
191        ArrayList<String> userDescriptions = new ArrayList<>();
192        uniqueIDS.clear();
193        ArrayList<Integer> arrayListOfSelectableOSSectionUniqueIDs = getArrayListOfSelectableOSSectionUniqueIDs(ctcSerialData.getCodeButtonHandlerDataArrayList());
194        for (Integer uniqueID : arrayListOfSelectableOSSectionUniqueIDs) {
195            if (ourUniqueID != uniqueID) {
196                userDescriptions.add(ctcSerialData.getMyShortStringNoCommaViaUniqueID(uniqueID));
197                uniqueIDS.add(uniqueID);
198            }
199        }
200//      Collections.sort(userDescriptions);
201        jComboBox.setModel(new DefaultComboBoxModel<>(new Vector<>(userDescriptions)));
202        return !userDescriptions.isEmpty();
203    }
204
205    /**
206     * Populate a combo box with bean names using getDisplayName().
207     * <p>
208     * If a panel xml file has not been loaded, the combo box will behave as a
209     * text field (editable), otherwise it will behave as standard combo box (not editable).
210     * @param jComboBox The string based combo box to be populated.
211     * @param beanType The bean type to be loaded.  It has to be in the switch list.
212     * @param currentSelection The current item to be selected, none if null.
213     * @param firstRowBlank True to create a blank row. If the selection is null or empty, the blank row will be selected.
214     */
215    public static void populateJComboBoxWithBeans(JComboBox<String> jComboBox, String beanType, String currentSelection, boolean firstRowBlank) {
216        jComboBox.removeAllItems();
217        jComboBox.setEditable(false);
218        ArrayList<String> list = new ArrayList<>();
219        switch (beanType) {
220            case "Sensor":  // NOI18N
221                InstanceManager.getDefault(SensorManager.class).getNamedBeanSet().forEach((s) -> {
222                    list.add(s.getDisplayName());
223                });
224                break;
225            case "Turnout": // NOI18N
226                InstanceManager.getDefault(TurnoutManager.class).getNamedBeanSet().forEach((t) -> {
227                    list.add(t.getDisplayName());
228                });
229                break;
230            case "SignalHead":  // NOI18N
231                InstanceManager.getDefault(SignalHeadManager.class).getNamedBeanSet().forEach((h) -> {
232                    list.add(h.getDisplayName());
233                });
234                break;
235            case "SignalMast":  // NOI18N
236                InstanceManager.getDefault(SignalMastManager.class).getNamedBeanSet().forEach((m) -> {
237                    list.add(m.getDisplayName());
238                });
239                break;
240            case "Block":   // NOI18N
241                InstanceManager.getDefault(BlockManager.class).getNamedBeanSet().forEach((b) -> {
242                    list.add(b.getDisplayName());
243                });
244                break;
245            default:
246                log.error("Unhandled, {}", Bundle.getMessage("CommonSubsBeanType", beanType));   // NOI18N
247        }
248        list.sort(new jmri.util.AlphanumComparator());
249        list.forEach((item) -> {
250            jComboBox.addItem(item);
251        });
252        jmri.util.swing.JComboBoxUtil.setupComboBoxMaxRows(jComboBox);
253        jComboBox.setSelectedItem(currentSelection);
254        if (firstRowBlank) {
255            jComboBox.insertItemAt("", 0);
256            if (currentSelection == null || currentSelection.isEmpty()) {
257                jComboBox.setSelectedIndex(0);
258            }
259        }
260    }
261
262    public static void setSelectedIndexOfJComboBoxViaUniqueID(JComboBox<String> jComboBox, CTCSerialData ctcSerialData, int uniqueID) {
263        int index = ctcSerialData.getIndexOfUniqueID(uniqueID) + 1;   // Can be -1 if not found, index becomes 0, which is spaces!
264        jComboBox.setSelectedIndex(index);
265    }
266
267/*  Someday I'll create a better "ButtonGroup" than provided.  Until then:
268
269    Cheat: We know that the implementation of ButtonGroup uses a Vector when elements
270    are added to it.  Therefore the order is guaranteed.  Check your individual order
271    by searching for all X.add (where X is the ButtonGroup variable name).
272    This routine will "number" each button in order using the "setActionCommand".
273    Then you can switch on either this string by doing:
274    switch (getButtonSelectedString(X))
275        case "0":
276            break;
277        case "1":
278    ....
279    Or faster CPU wise:
280    switch (getButtonSelectedInt(X))
281        case 0:
282    ....
283*/
284    public static void numberButtonGroup(ButtonGroup buttonGroup) {
285        int entry = 0;
286        Enumeration<AbstractButton> buttons = buttonGroup.getElements();
287        while (buttons.hasMoreElements()) {
288            AbstractButton button = buttons.nextElement();
289            button.setActionCommand(Integer.toString(entry++));
290        }
291    }
292
293    public static void setButtonSelected(ButtonGroup buttonGroup, int selected) {
294        ArrayList<AbstractButton> buttons = Collections.list(buttonGroup.getElements());
295        if (buttons.isEmpty()) return;    // Safety: The moron forgot to put radio buttons into this group!  Don't select any!
296        if (selected < 0 || selected >= buttons.size()) selected = 0;   // Default is zero if you pass an out of range value.
297        AbstractButton buttonSelected = buttons.get(selected);
298        buttonSelected.setSelected(true);
299//  Be consistent, when set, do this also:
300        ActionEvent actionEvent = new ActionEvent(buttonSelected, ActionEvent.ACTION_PERFORMED, buttonSelected.getActionCommand());
301        for (ActionListener actionListener : buttonSelected.getActionListeners()) {
302            actionListener.actionPerformed(actionEvent);
303        }
304    }
305
306//  If the passed errors array has entries, put up a dialog and return true, if not no dialog, and return false.
307    public static boolean missingFieldsErrorDialogDisplayed(Component parentComponent, ArrayList<String> errors, boolean isCancel) {
308        if (errors.isEmpty()) return false;
309        StringBuilder stringBuffer = new StringBuilder(errors.size() > 1 ? Bundle.getMessage("CommonSubsFieldsPlural") : Bundle.getMessage("CommonSubsFieldSingular"));     // NOI18N
310        errors.forEach(error -> stringBuffer.append(error).append("\n")); // NOI18N
311        if (!isCancel) {
312            stringBuffer.append(Bundle.getMessage("CommonSubsPleaseFix1")); // NOI18N
313        } else {
314            stringBuffer.append(Bundle.getMessage("CommonSubsPleaseFix2")); // NOI18N
315        }
316        JmriJOptionPane.showMessageDialog(parentComponent, stringBuffer.toString(),
317                Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);   // NOI18N
318        return true;
319    }
320
321//  Simple sub to see if field is not empty.  If empty, then it takes the prompt text and adds it to the end of the errors array.
322    public static void checkJTextFieldNotEmpty(javax.swing.JTextField field, javax.swing.JLabel promptName, ArrayList<String> errors) {
323        if (!isJTextFieldNotEmpty(field)) errors.add(promptName.getText());
324    }
325
326    public static boolean isJTextFieldNotEmpty(javax.swing.JTextField field) {
327        return !(field.getText().trim().isEmpty());
328    }
329
330//  Simple sub to see if combo selection is not empty.  If empty, then it takes the prompt text and adds it to the end of the errors array.
331    public static void checkJComboBoxNotEmpty(javax.swing.JComboBox<String> combo, javax.swing.JLabel promptName, ArrayList<String> errors) {
332        if (!isJComboBoxNotEmpty(combo)) errors.add(promptName.getText());
333    }
334
335    public static boolean isJComboBoxNotEmpty(javax.swing.JComboBox<String> combo) {
336        return !((String) combo.getSelectedItem()).trim().isEmpty();
337    }
338
339    /**
340     * Get a NBHSensor from the CtcManager NBHSensor map or create a new one.
341     * @param newName The new name to be retrieved from the map or created.
342     * @param isInternal True if an internal sensor is being requested.  Internal will create the sensor if necessary using provide(String).
343     * @return a NBHSensor or null.
344     */
345    public static NBHSensor getNBHSensor(String newName, boolean isInternal) {
346        NBHSensor sensor = null;
347        if (!ProjectsCommonSubs.isNullOrEmptyString(newName)) {
348            sensor = InstanceManager.getDefault(CtcManager.class).getNBHSensor(newName);
349            if (sensor == null) {
350                if (isInternal) {
351                    sensor = new NBHSensor("CommonSubs", "new sensor = ", newName, newName);
352                } else {
353                    sensor = new NBHSensor("CommonSubs", "new sensor = ", newName, newName, false);
354                }
355            }
356        }
357        return sensor;
358    }
359
360    /**
361     * Get a NBHTurnout from the CtcManager NBHTurnout map or create a new one.
362     * @param newName The new name to be retrieved from the map or created.
363     * @param feedbackDifferent The feedback different state.
364     * @return a valid NBHTurnout or an empty NBHTurnout.
365     */
366    public static NBHTurnout getNBHTurnout(String newName, boolean feedbackDifferent) {
367        NBHTurnout turnout = null;
368        if (!ProjectsCommonSubs.isNullOrEmptyString(newName)) {
369            turnout = InstanceManager.getDefault(CtcManager.class).getNBHTurnout(newName);
370            if (turnout == null) {
371                turnout = new NBHTurnout("CommonSubs", "new turnout = ", newName, newName, feedbackDifferent);
372            }
373        }
374        if (turnout == null) {
375            // Create a dummy NBHTurnout
376            turnout = new NBHTurnout("CommonSubs", "Empty turnout", "");
377        }
378        return turnout;
379    }
380
381    /**
382     * Get a NBHSignal from the CtcManager NBHSignal map or create a new one.
383     * @param newName The new name to be retrieved from the map or created.
384     * @return a valid NBHSignal or null.
385     */
386    public static NBHSignal getNBHSignal(String newName) {
387        NBHSignal signal = null;
388        if (!ProjectsCommonSubs.isNullOrEmptyString(newName)) {
389            signal = InstanceManager.getDefault(CtcManager.class).getNBHSignal(newName);
390            if (signal == null) {
391                signal = new NBHSignal(newName);
392            }
393        }
394        return signal;
395    }
396
397    /**
398     * Add a valid NBHSensor entry to an ArrayList.  The sensor name has to match an existing
399     * sensor in the JMRI sensor table.
400     * @param list The NBHSensor array list.
401     * @param sensorName The proposed sensor name.
402     */
403    public static void addSensorToSensorList(ArrayList<NBHSensor> list, String sensorName) {
404        NBHSensor sensor = getNBHSensor(sensorName, false);
405        if (sensor != null && sensor.valid()) list.add(sensor);
406    }
407
408    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CommonSubs.class);
409}