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}