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