001package jmri.jmrit.beantable.signalmast; 002 003import java.awt.*; 004import java.awt.event.*; 005import java.io.File; 006import java.net.URISyntaxException; 007import java.net.URL; 008import java.util.*; 009import java.util.List; 010 011import javax.swing.*; 012 013import jmri.*; 014import jmri.implementation.*; 015import jmri.jmrit.XmlFile; 016import jmri.util.*; 017import jmri.util.swing.JComboBoxUtil; 018import jmri.util.swing.JmriJOptionPane; 019 020import org.jdom2.Element; 021 022/** 023 * JPanel to create a new Signal Mast. 024 * 025 * "Driver" refers to a particular class of SignalMast implementation that's to be configured. 026 * 027 * @author Bob Jacobsen Copyright (C) 2009, 2010, 2016 028 * @author Egbert Broerse Copyright (C) 2016 029 */ 030public class AddSignalMastPanel extends JPanel { 031 032 // head matter 033 JTextField userName = new JTextField(20); 034 JComboBox<String> sigSysBox = new JComboBox<>(); // the basic signal system 035 JComboBox<String> mastBox = new JComboBox<>(new String[]{Bundle.getMessage("MastEmpty")}); // the mast within the system NOI18N 036 boolean mastBoxPassive = false; // if true, mastBox doesn't process updates 037 JComboBox<String> signalMastDriver; // the specific SignalMast class type 038 039 List<SignalMastAddPane> panes = new ArrayList<>(); 040 041 // center pane, which holds the specific display 042 JPanel centerPanel = new JPanel(); 043 CardLayout cl = new CardLayout(); 044 SignalMastAddPane currentPane; 045 046 // rest of structure 047 JPanel signalHeadPanel = new JPanel(); 048 JButton cancel = new JButton(Bundle.getMessage("ButtonCancel")); // NOI18N 049 JButton apply = new JButton(Bundle.getMessage("ButtonApply")); // NOI18N 050 JButton create = new JButton(Bundle.getMessage("ButtonCreate")); // NOI18N 051 052 // connection to preferences 053 private UserPreferencesManager prefs = InstanceManager.getDefault(UserPreferencesManager.class); 054 private String systemSelectionCombo = this.getClass().getName() + ".SignallingSystemSelected"; // NOI18N 055 private String mastSelectionCombo = this.getClass().getName() + ".SignallingMastSelected"; // NOI18N 056 private String driverSelectionCombo = this.getClass().getName() + ".SignallingDriverSelected"; // NOI18N 057 058 /** 059 * Constructor providing a blank panel to configure a new signal mast after 060 * pressing 'Add...' on the Signal Mast Table. 061 * <p> 062 * Responds to choice of signal system, mast type and driver 063 * {@link #updateSelectedDriver()} 064 */ 065 public AddSignalMastPanel() { 066 log.debug("AddSignalMastPanel()"); 067 // get the list of possible signal types (as shown by panes) 068 SignalMastAddPane.SignalMastAddPaneProvider.getInstancesCollection().forEach( 069 (provider)-> { 070 if (provider.isAvailable()) { 071 panes.add(provider.getNewPane()); 072 } 073 } 074 ); 075 076 // scoping for temporary variables 077 String[] tempMastNamesArray = new String[panes.size()]; 078 int i = 0; 079 for (SignalMastAddPane pane : panes) { 080 tempMastNamesArray[i++] = pane.getPaneName(); 081 } 082 signalMastDriver = new JComboBox<>(tempMastNamesArray); 083 init(); 084 } 085 086 final void init() { 087 088 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); 089 090 JPanel p; 091 p = new JPanel(); 092 p.setLayout(new jmri.util.javaworld.GridLayout2(5, 2)); 093 094 JLabel l = new JLabel(Bundle.getMessage("LabelUserName")); // NOI18N 095 p.add(l); 096 p.add(userName); 097 098 l = new JLabel(Bundle.getMessage("SigSys") + ": "); // NOI18N 099 p.add(l); 100 p.add(sigSysBox); 101 JComboBoxUtil.setupComboBoxMaxRows(sigSysBox); 102 103 l = new JLabel(Bundle.getMessage("MastType") + ": "); // NOI18N 104 p.add(l); 105 p.add(mastBox); 106 JComboBoxUtil.setupComboBoxMaxRows(mastBox); 107 108 l = new JLabel(Bundle.getMessage("DriverType") + ": "); // NOI18N 109 p.add(l); 110 p.add(signalMastDriver); 111 JComboBoxUtil.setupComboBoxMaxRows(signalMastDriver); 112 113 add(p); 114 115 // central region 116 centerPanel.setLayout(cl); 117 for (SignalMastAddPane pane : panes) { 118 centerPanel.add(pane, pane.getPaneName()); // assumes names are systemwide-unique 119 } 120 add(centerPanel); 121 signalMastDriver.addItemListener((ItemEvent evt) -> { 122 log.trace("about to call selection() from signalMastDriver itemStateChanged"); 123 selection((String)evt.getItem()); 124 }); 125 126 // button region 127 JPanel buttonHolder = new JPanel(); 128 buttonHolder.setLayout(new FlowLayout(FlowLayout.TRAILING)); 129 cancel.setVisible(true); 130 buttonHolder.add(cancel); 131 cancel.addActionListener((ActionEvent e) -> { 132 cancelPressed(); 133 } // Cancel button 134 ); 135 cancel.setVisible(true); 136 buttonHolder.add(create); // Create button on add new mast pane 137 create.addActionListener((ActionEvent e) -> { 138 okPressed(); 139 }); 140 create.setVisible(true); 141 buttonHolder.add(apply); // Apply button on Edit existing mast pane 142 apply.addActionListener((ActionEvent e) -> { 143 okPressed(); 144 }); 145 apply.setVisible(false); 146 add(buttonHolder); // add bottom row of buttons (to me) 147 148 // default to 1st pane 149 currentPane = panes.get(0); 150 151 // load the list of signal systems 152 SignalSystemManager man = InstanceManager.getDefault(SignalSystemManager.class); 153 SortedSet<SignalSystem> systems = man.getNamedBeanSet(); 154 for (SignalSystem system : systems) { 155 sigSysBox.addItem(system.getUserName()); 156 } 157 158 if (prefs.getComboBoxLastSelection(systemSelectionCombo) != null) { 159 sigSysBox.setSelectedItem(prefs.getComboBoxLastSelection(systemSelectionCombo)); 160 } 161 log.trace(" preferences set {} into sigSysBox", sigSysBox.getSelectedItem()); 162 163 loadMastDefinitions(); 164 165 // select the 1st one 166 selection(panes.get(0).getPaneName()); // there has to be at least one, so we can do the update 167 168 // set a remembered signalmast type, if present 169 if (prefs.getComboBoxLastSelection(driverSelectionCombo) != null) { 170 signalMastDriver.setSelectedItem(prefs.getComboBoxLastSelection(driverSelectionCombo)); 171 } 172 173 sigSysBox.addItemListener((ItemEvent e) -> { 174 loadMastDefinitions(); 175 updateSelectedDriver(); 176 }); 177 } 178 179 /** 180 * Select a particular signal implementation to display. 181 * @param view The signal implementation pane name to display 182 */ 183 final void selection(String view) { 184 log.trace(" selection({}) start", view); 185 // find the new pane 186 for (SignalMastAddPane pane : panes) { 187 if (pane.getPaneName().equals(view)) { 188 currentPane = pane; 189 } 190 } 191 192 // update that selected pane before display. 193 updateSelectedDriver(); 194 195 // and show 196 cl.show(centerPanel, view); 197 log.trace(" selection({}) end", view); 198 } 199 200 /** 201 * Build a panel filled in for existing mast after pressing 'Edit' in the 202 * Signal Mast table. 203 * 204 * @param mast {@code NamedBeanHandle<SignalMast> } for the signal mast to 205 * be retrieved 206 * @see #AddSignalMastPanel() 207 */ 208 public AddSignalMastPanel(SignalMast mast) { 209 this(); // calls the above method to build the base for an edit panel 210 log.debug("AddSignalMastPanel({}) start", mast); 211 212 // switch buttons 213 apply.setVisible(true); 214 create.setVisible(false); 215 216 // can't change some things from original settings 217 sigSysBox.setEnabled(false); 218 mastBox.setEnabled(false); 219 signalMastDriver.setEnabled(false); 220 userName.setEnabled(false); 221 222 //load prior content 223 userName.setText(mast.getUserName()); 224 log.trace("Prior content system name: {} mast type: {}", mast.getSignalSystem().getUserName(), mast.getMastType()); 225 if (mast.getMastType() == null) log.error("MastType was null, and never should be"); 226 sigSysBox.setSelectedItem(mast.getSignalSystem().getUserName()); // signal system 227 228 // select and show 229 for (SignalMastAddPane pane : panes) { 230 if (pane.canHandleMast(mast)) { 231 currentPane = pane; 232 // set the driver combobox 233 signalMastDriver.setSelectedItem(pane.getPaneName()); 234 log.trace("About to call selection() from SignalMastAddPane loop in AddSignalMastPanel(SignalMast mast)"); 235 selection(pane.getPaneName()); 236 237 // Ensure that the mast type is set 238 mastBoxPassive = false; 239 if (mapTypeToName.get(mast.getMastType()) == null ) { 240 log.error("About to set mast to null, which shouldn't happen. mast.getMastType() is {}", mast.getMastType(), 241 new Exception("Traceback Exception")); // NOI18N 242 } 243 log.trace("set mastBox to \"{}\" from \"{}\"", mapTypeToName.get(mast.getMastType()), mast.getMastType()); // NOI18N 244 mastBox.setSelectedItem(mapTypeToName.get(mast.getMastType())); 245 246 pane.setMast(mast); 247 break; 248 } 249 } 250 251 // set mast type, suppress notification 252 mastBoxPassive = true; 253 String newMastType = mapTypeToName.get(mast.getMastType()); 254 log.debug("Setting type to {}", newMastType); // NOI18N 255 mastBox.setSelectedItem(newMastType); 256 mastBoxPassive = false; 257 258 log.debug("AddSignalMastPanel({}) end", mast); 259 } 260 261 // signal system definition variables 262 private String sigsysname; 263 private ArrayList<File> mastFiles = new ArrayList<>(); // signal system definition files 264 private LinkedHashMap<String, Integer> mapNameToShowSize = new LinkedHashMap<>(); 265 private LinkedHashMap<String, String> mapTypeToName = new LinkedHashMap<>(); 266 267 /** 268 * Load the mast definitions from the selected signal system. 269 */ 270 void loadMastDefinitions() { 271 log.trace(" loadMastDefinitions() start"); 272 // need to remove itemListener before addItem() or item event will occur 273 if (mastBox.getItemListeners().length > 0) { // should this be a while loop? 274 mastBox.removeItemListener(mastBox.getItemListeners()[0]); 275 } 276 mastBox.removeAllItems(); 277 try { 278 mastFiles = new ArrayList<>(); 279 SignalSystemManager man = InstanceManager.getDefault(SignalSystemManager.class); 280 281 // get the signals system name from the user name in combo box 282 String u = (String) sigSysBox.getSelectedItem(); 283 SignalSystem sig = man.getByUserName(u); 284 if (sig==null){ 285 log.error("Signal System Not found for Username {}",u); 286 return; 287 } 288 sigsysname = sig.getSystemName(); 289 log.trace(" loadMastDefinitions with sigsysname {}", sigsysname); // NOI18N 290 mapNameToShowSize = new LinkedHashMap<>(); 291 mapTypeToName = new LinkedHashMap<>(); 292 293 // do file IO to get all the appearances 294 // gather all the appearance files 295 // Look for the default system defined ones first 296 File[] programDirArray = new File[0]; 297 URL pathProgramDir = FileUtil.findURL("xml/signals/" + sigsysname, FileUtil.Location.INSTALLED); // NOI18N 298 if (pathProgramDir != null) programDirArray = new File(pathProgramDir.toURI()).listFiles(); 299 if (programDirArray == null) programDirArray = new File[0]; 300 301 File[] profileDirArray = new File[0]; 302 URL pathProfileDir = FileUtil.findURL("resources/signals/" + sigsysname, FileUtil.Location.USER); // NOI18N 303 if (pathProfileDir != null) profileDirArray = new File(pathProfileDir.toURI()).listFiles(); 304 if (profileDirArray == null) profileDirArray = new File[0]; 305 306 // create a composite list of files 307 File[] apps = Arrays.copyOf(programDirArray, programDirArray.length + profileDirArray.length); 308 System.arraycopy(profileDirArray, 0, apps, programDirArray.length, profileDirArray.length); 309 310 if (apps !=null) { 311 for (File app : apps) { 312 if (app.getName().startsWith("appearance") && app.getName().endsWith(".xml")) { // NOI18N 313 log.debug(" found file: {}", app.getName()); // NOI18N 314 // load it and get name 315 mastFiles.add(app); 316 XmlFile xf = new XmlFile() { 317 }; 318 Element root = xf.rootFromFile(app); 319 String name = root.getChild("name").getText(); 320 log.trace("mastNames adding \"{}\" mastBox adding \"{}\" ", app, name); // NOI18N 321 mastBox.addItem(name); 322 log.trace("mapTypeToName adding key \"{}\" value \"{}\"", app.getName().substring(11, app.getName().indexOf(".xml")), name); // NOI18N 323 mapTypeToName.put(app.getName().substring(11, app.getName().indexOf(".xml")), name); // NOI18N 324 mapNameToShowSize.put(name, root.getChild("appearances") // NOI18N 325 .getChild("appearance") // NOI18N 326 .getChildren("show") // NOI18N 327 .size()); 328 329 } 330 } 331 } else { 332 log.error("Unexpected null list of signal definition files"); // NOI18N 333 } 334 335 } catch (org.jdom2.JDOMException e) { 336 mastBox.addItem(Bundle.getMessage("ErrorSignalMastBox1")); // NOI18N 337 log.warn("in loadMastDefinitions", e); // NOI18N 338 } catch (java.io.IOException | URISyntaxException e) { 339 mastBox.addItem(Bundle.getMessage("ErrorSignalMastBox2")); // NOI18N 340 log.warn("in loadMastDefinitions", e); // NOI18N 341 } 342 343 try { 344 URL path = FileUtil.findURL("signals/" + sigsysname, FileUtil.Location.USER, "xml", "resources"); // NOI18N 345 if (path != null) { 346 File[] apps = new File(path.toURI()).listFiles(); 347 if (apps != null) { 348 for (File app : apps) { 349 if (app.getName().startsWith("appearance") && app.getName().endsWith(".xml")) { // NOI18N 350 log.debug(" found file: {}", app.getName()); // NOI18N 351 // load it and get name 352 // If the mast file name already exists no point in re-adding it 353 if (!mastFiles.contains(app)) { 354 mastFiles.add(app); 355 XmlFile xf = new XmlFile() { 356 }; 357 Element root = xf.rootFromFile(app); 358 String name = root.getChild("name").getText(); 359 //if the mast name already exist no point in readding it. 360 if (!mapNameToShowSize.containsKey(name)) { 361 mastBox.addItem(name); 362 mapNameToShowSize.put(name, root.getChild("appearances") // NOI18N 363 .getChild("appearance") // NOI18N 364 .getChildren("show") // NOI18N 365 .size()); 366 } 367 } 368 } 369 } 370 } else { 371 log.warn("No mast definition files found"); 372 } 373 } 374 } catch (org.jdom2.JDOMException | java.io.IOException | URISyntaxException e) { 375 log.warn("in loadMastDefinitions", e); // NOI18N 376 } 377 mastBox.addItemListener((ItemEvent e) -> { 378 if (!mastBoxPassive) updateSelectedDriver(); 379 }); 380 updateSelectedDriver(); 381 382 if (prefs.getComboBoxLastSelection(mastSelectionCombo + ":" + ((String) sigSysBox.getSelectedItem())) != null) { // NOI18N 383 mastBox.setSelectedItem(prefs.getComboBoxLastSelection(mastSelectionCombo + ":" + ((String) sigSysBox.getSelectedItem()))); 384 } 385 log.trace(" loadMastDefinitions() end"); 386 } 387 388 /** 389 * Update contents of Add/Edit mast panel appropriate for chosen Driver 390 * type. 391 * <p> 392 * Invoked when selecting a Signal Mast Driver in {@link #loadMastDefinitions} 393 */ 394 protected void updateSelectedDriver() { 395 log.trace(" updateSelectedDriver() start"); 396 397 if (mastBox.getSelectedIndex() < 0) return; // no mast selected yet 398 String mastFile = mastFiles.get(mastBox.getSelectedIndex()).getName(); 399 String mastType = mastFile.substring(11, mastFile.indexOf(".xml")); 400 DefaultSignalAppearanceMap sigMap = DefaultSignalAppearanceMap.getMap(sigsysname, mastType); 401 SignalSystem sigsys = InstanceManager.getDefault(SignalSystemManager.class).getSystem(sigsysname); 402 if (sigsys == null){ 403 log.error("Signalling System for {} Not Found",sigsysname); 404 } else { 405 currentPane.setAspectNames(sigMap, sigsys); 406 } 407 // clear mast info 408 currentPane.setMast(null); 409 410 currentPane.revalidate(); 411 412 java.awt.Container ancestor = getTopLevelAncestor(); 413 if ((ancestor instanceof JmriJFrame)) { 414 ((JmriJFrame) ancestor).pack(); 415 } else { 416 log.debug("Can't call pack() on {}", ancestor); 417 } 418 log.trace(" updateSelectedDriver() end"); 419 } 420 421 /** 422 * Check of user name done when creating new SignalMast. 423 * In case of error, it looks a message and (if not headless) shows a dialog. 424 * 425 * @param nam User name to be checked 426 * @return true if OK to proceed 427 */ 428 boolean checkUserName(String nam) { 429 if (!((nam == null) || (nam.isEmpty()))) { 430 // user name provided, check if that name already exists 431 NamedBean nB = InstanceManager.getDefault(SignalMastManager.class).getByUserName(nam); 432 if (nB != null) { 433 issueWarningUserName(nam); 434 return false; 435 } 436 // Check to ensure that the username doesn't exist as a systemname. 437 nB = InstanceManager.getDefault(SignalMastManager.class).getBySystemName(nam); 438 if (nB != null) { 439 issueWarningUserNameAsSystem(nam); 440 return false; 441 } 442 } 443 return true; 444 } 445 446 void issueWarningUserName(String nam) { 447 log.error("User Name \"{}\" is already in use", nam); // NOI18N 448 if (!GraphicsEnvironment.isHeadless()) { 449 String msg = Bundle.getMessage("WarningUserName", new Object[]{("" + nam)}); // NOI18N 450 JmriJOptionPane.showMessageDialog(this, msg, 451 Bundle.getMessage("WarningTitle"), // NOI18N 452 JmriJOptionPane.ERROR_MESSAGE); 453 } 454 } 455 456 void issueWarningUserNameAsSystem(String nam) { 457 log.error("User Name \"{}\" already exists as a System name", nam); 458 if (!GraphicsEnvironment.isHeadless()) { 459 String msg = Bundle.getMessage("WarningUserNameAsSystem", new Object[]{("" + nam)}); 460 JmriJOptionPane.showMessageDialog(this, msg, 461 Bundle.getMessage("WarningTitle"), 462 JmriJOptionPane.ERROR_MESSAGE); 463 } 464 } 465 466 /** 467 * Store user input for a signal mast definition in new or existing mast 468 * object. 469 * <p> 470 * Invoked from Apply/Create button. 471 */ 472 private void okPressed() { 473 log.trace(" okPressed() start"); 474 boolean success; 475 476 // get and validate entered global information 477 if ( (mastBox.getSelectedIndex() < 0) || ( mastFiles.get(mastBox.getSelectedIndex()) == null) ) { 478 issueDialogFailMessage(new RuntimeException("There's something wrong with the mast type selection")); 479 return; 480 } 481 String mastname = mastFiles.get(mastBox.getSelectedIndex()).getName(); 482 String tmpUserName = NamedBean.normalizeUserName(userName.getText()); 483 String user = ( tmpUserName != null ? tmpUserName : ""); // NOI18N 484 if (!GraphicsEnvironment.isHeadless()) { 485 if (user.isEmpty()) { 486 int i = issueNoUserNameGiven(); 487 if (i != 0) { 488 return; 489 } 490 } 491 } 492 493 // ask top-most pane to make a signal 494 try { 495 success = currentPane.createMast(sigsysname, mastname, user); 496 } catch (RuntimeException ex) { 497 issueDialogFailMessage(ex); 498 return; // without clearing the panel, so user can try again 499 } 500 if (!success) { 501 // should have already provided user feedback via dialog 502 return; 503 } 504 505 clearPanel(); 506 log.trace(" okPressed() end"); 507 } 508 509 int issueNoUserNameGiven() { 510 return JmriJOptionPane.showConfirmDialog(this, Bundle.getMessage("SignalMastEmptyUserNameDialog"), // NOI18N 511 Bundle.getMessage("SignalMastEmptyUserNameDialogTitle"), // NOI18N 512 JmriJOptionPane.YES_NO_OPTION); 513 } 514 515 void issueDialogFailMessage(RuntimeException ex) { 516 // This is intrinsically swing, so pop a dialog 517 log.error("Failed during createMast", ex); // NOI18N 518 JmriJOptionPane.showMessageDialog(this, 519 Bundle.getMessage("DialogFailMessage", ex.toString()), // NOI18N 520 Bundle.getMessage("DialogFailTitle"), // title of box // NOI18N 521 JmriJOptionPane.ERROR_MESSAGE); 522 } 523 524 /** 525 * Called when an already-initialized AddSignalMastPanel is being 526 * displayed again, right before it's set visible. 527 */ 528 public void refresh() { 529 log.trace(" refresh() start"); 530 // add new cards (new panes) 531 centerPanel.removeAll(); 532 for (SignalMastAddPane pane : panes) { 533 centerPanel.add(pane, pane.getPaneName()); // assumes names are systemwide-unique 534 } 535 536 // select pane to match current combobox 537 log.trace("about to call selection from refresh"); 538 selection(signalMastDriver.getItemAt(signalMastDriver.getSelectedIndex())); 539 log.trace(" refresh() end"); 540 } 541 542 /** 543 * Respond to the Cancel button. 544 */ 545 private void cancelPressed() { 546 log.trace(" cancelPressed() start"); 547 clearPanel(); 548 log.trace(" cancelPressed() end"); 549 } 550 551 /** 552 * Close and dispose() panel. 553 * <p> 554 * Called at end of okPressed() and from Cancel 555 */ 556 private void clearPanel() { 557 log.trace(" clearPanel() start"); 558 java.awt.Container ancestor = getTopLevelAncestor(); 559 if ((ancestor instanceof JmriJFrame)) { 560 ((JmriJFrame) ancestor).dispose(); 561 } else { 562 log.warn("Unexpected top level ancestor: {}", ancestor); // NOI18N 563 } 564 userName.setText(""); // clear user name 565 log.trace(" clearPanel() end"); 566 } 567 568 569 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AddSignalMastPanel.class); 570 571}