001package jmri.jmrit.beantable; 002 003import java.awt.*; 004import java.awt.event.ActionEvent; 005import java.awt.event.ActionListener; 006import java.beans.PropertyChangeEvent; 007import java.beans.PropertyChangeListener; 008import javax.annotation.Nonnull; 009import javax.swing.*; 010 011import jmri.*; 012import jmri.jmrit.beantable.oblock.*; 013import jmri.jmrit.logix.OBlock; 014import jmri.jmrit.logix.OBlockManager; 015import jmri.jmrit.logix.PortalManager; 016import jmri.util.JmriJFrame; 017import jmri.util.gui.GuiLafPreferencesManager; 018import jmri.util.swing.JmriJOptionPane; 019 020/** 021 * GUI to define OBlocks, OPaths and Portals. Overrides some of the AbstractTableAction methods as this is a hybrid pane. 022 * Relies on {@link jmri.jmrit.beantable.oblock.TableFrames}. 023 * 024 * @author Pete Cressman (C) 2009, 2010 025 * @author Egbert Broerse (C) 2020 026 */ 027public class OBlockTableAction extends AbstractTableAction<OBlock> implements PropertyChangeListener { 028 029 // for tabs or desktop interface 030 protected boolean _tabbed = false; // updated from prefs 031 protected JPanel dataPanel; 032 protected JTabbedPane dataTabs; 033 protected boolean init = false; 034 035 // basic table models 036 OBlockTableModel oblocks; 037 PortalTableModel portals; 038 SignalTableModel signals; 039 BlockPortalTableModel blockportals; 040 // tables created on demand inside TableFrames: 041 // - BlockPathTable(block) 042 // - PathTurnoutTable(block) 043 044 @Nonnull 045 protected OBlockManager oblockManager = InstanceManager.getDefault(OBlockManager.class); 046 @Nonnull 047 protected PortalManager portalManager = InstanceManager.getDefault(PortalManager.class); 048 049 TableFrames tf; 050 OBlockTableFrame otf; 051 OBlockTablePanel otp; 052 053 // edit frames 054 //OBlockEditFrame oblockFrame; // instead we use NewBean util + Edit 055 PortalEditFrame portalFrame; 056 SignalEditFrame signalFrame; 057 // on demand frames 058 // PathTurnoutFrame ptFrame; created from TableFrames 059 // BlockPathEditFrame bpFrame; created from TableFrames 060 061 /** 062 * Create an action with a specific title. 063 * <p> 064 * Note that the argument is the Action title, not the title of the 065 * resulting frame. Perhaps this should be changed? 066 * 067 * @param actionName title of the action 068 */ 069 public OBlockTableAction(String actionName) { 070 super(actionName); 071 includeAddButton = false; // not required per se as we override the actionPerformed method 072 } 073 074 /** 075 * Default constructor 076 */ 077 public OBlockTableAction() { 078 this(Bundle.getMessage("TitleOBlockTable")); 079 } 080 081 /** 082 * Configure managers for all tabs on OBlocks table pane. 083 * @param om the manager to assign 084 */ 085 @Override 086 public void setManager(@Nonnull Manager<OBlock> om) { 087 oblockManager.removePropertyChangeListener(this); 088 if (om instanceof OBlockManager) { 089 oblockManager = (OBlockManager) om; 090 if (m != null) { // model 091 m.setManager(oblockManager); 092 } 093 } 094 oblockManager.addPropertyChangeListener(this); 095 } 096 097 // add the 3 buttons to add new OBlock, Portal, Signal 098 @Override 099 public void addToFrame(@Nonnull BeanTableFrame<OBlock> f) { 100 JButton addOblockButton = new JButton(Bundle.getMessage("ButtonAddOBlock")); 101 otp.addToBottomBox(addOblockButton); 102 addOblockButton.addActionListener(this::addOBlockPressed); 103 104 JButton addPortalButton = new JButton(Bundle.getMessage("ButtonAddPortal")); 105 otp.addToBottomBox(addPortalButton); 106 addPortalButton.addActionListener(this::addPortalPressed); 107 108 JButton addSignalButton = new JButton(Bundle.getMessage("ButtonAddSignal")); 109 otp.addToBottomBox(addSignalButton); 110 addSignalButton.addActionListener(this::addSignalPressed); 111 } 112 113 /** 114 * Open OBlock tables action handler. 115 * @see jmri.jmrit.beantable.oblock.TableFrames 116 * @param e menu action 117 */ 118 @Override 119 public void actionPerformed(ActionEvent e) { 120 _tabbed = InstanceManager.getDefault(GuiLafPreferencesManager.class).isOblockEditTabbed(); 121 initTableFrames(); 122 } 123 124 private void initTableFrames() { 125 // initialise core OBlock Edit functionality 126 tf = new TableFrames(); // tf contains OBlock Edit methods and links to tableDataModels, is a JmriJFrame that must be hidden 127 128 if (_tabbed) { // add the tables on a JTabbedPane, choose in Preferences > Display > GUI 129 log.debug("Tabbed starting"); 130 // create the JTable model, with changes for specific NamedBean 131 createModel(); 132 // create the frame 133 otf = new OBlockTableFrame(otp, helpTarget()) { 134 135 /** 136 * Include "Add OBlock..." and "Add XYZ..." buttons 137 */ 138 @Override 139 void extras() { 140 addToFrame(this); //creates multiple sets, wrong level to call 141 } 142 }; 143 setTitle(); 144 145 //tf.setParentFrame(otf); // needed? 146 //tf.makePrivateWindow(); // prevents tf "OBlock Tables..." to show up in the Windows menu 147 //tf.setVisible(false); // hide the TableFrames container when _tabbed 148 149 otf.pack(); 150 otf.setVisible(true); 151 } else { 152 tf.initComponents(); 153 // original simulated desktop interface is created in tf.initComponents() and takes care of itself if !_tabbed 154 // only required for _desktop, creates InternalFrames 155 //tf.setVisible(true); 156 } 157 } 158 159 /** 160 * Create the JTable DataModel, along with the extra stuff for this specific NamedBean type. 161 * Is directly called to prepare the Tables > OBlock Table entry in the left sidebar list, bypassing actionPerformed(a) 162 */ 163 @Override 164 protected void createModel() { // Tabbed 165 if (tf == null) { 166 initTableFrames(); 167 } 168 oblocks = tf.getOblockTableModel(); 169 portals = tf.getPortalTableModel(); 170 signals = tf.getSignalTableModel(); 171 blockportals = tf.getPortalXRefTableModel(); 172 173 otp = new OBlockTablePanel(oblocks, portals, signals, blockportals, tf, helpTarget()); 174 175// if (f == null) { 176// f = new OBlockTableFrame(otp, helpTarget()); 177// } 178// setMenuBar(f); // comes after the Help menu is added by f = new 179 // BeanTableFrame(etc.) handled in stand alone application 180// setTitle(); // TODO see if some of this is required or should be turned off to prevent/hide the stray JFrame that opens 181// addToFrame(f); 182// f.pack(); 183// f.setVisible(true); <--- another empty pane! 184 185 init = true; 186 } 187 188 @Override 189 public JPanel getPanel() { 190 createModel(); 191 return otp; 192 } 193 194 /** 195 * Include the correct title. 196 */ 197 @Override 198 protected void setTitle() { 199 if (_tabbed && otf != null) { 200 otf.setTitle(Bundle.getMessage("TitleOBlocksTabbedFrame")); 201 } 202 } 203 204 @Override 205 public void setMenuBar(BeanTableFrame<OBlock> f) { 206 if (_tabbed) { 207 //final JmriJFrame finalF = f; // needed for anonymous ActionListener class on dialogs, see TurnoutTableAction ? 208 JMenuBar menuBar = f.getJMenuBar(); 209 if (menuBar == null) { 210 log.debug("NULL MenuBar"); 211 return; 212 } 213 MenuElement[] subElements; 214 JMenu fileMenu = null; 215 for (int i = 0; i < menuBar.getMenuCount(); i++) { 216 if (menuBar.getComponent(i) instanceof JMenu) { 217 if (((JMenu) menuBar.getComponent(i)).getText().equals(Bundle.getMessage("MenuFile"))) { 218 fileMenu = menuBar.getMenu(i); 219 } 220 } 221 } 222 if (fileMenu == null) { 223 log.debug("NULL FileMenu"); 224 return; 225 } 226 subElements = fileMenu.getSubElements(); 227 for (MenuElement subElement : subElements) { 228 MenuElement[] popsubElements = subElement.getSubElements(); 229 for (MenuElement popsubElement : popsubElements) { 230 if (popsubElement instanceof JMenuItem) { 231 if (((JMenuItem) popsubElement).getText().equals(Bundle.getMessage("PrintTable"))) { 232 JMenuItem printMenu = (JMenuItem) popsubElement; 233 fileMenu.remove(printMenu); 234 break; 235 } 236 } 237 } 238 } 239 fileMenu.add(otp.getPrintItem()); 240 241 menuBar.add(otp.getOptionMenu()); 242 menuBar.add(otp.getTablesMenu()); 243 log.debug("setMenuBar for OBLOCKS"); 244 245 // check for menu (copied from TurnoutTableAction) 246// boolean menuAbsent = true; 247// for (int m = 0; m < menuBar.getMenuCount(); ++m) { 248// String name = menuBar.getMenu(m).getAccessibleContext().getAccessibleName(); 249// if (name.equals(Bundle.getMessage("MenuOptions"))) { 250// // using first menu for check, should be identical to next JMenu Bundle 251// menuAbsent = false; 252// break; 253// } 254// } 255// if (menuAbsent) { // create it 256// int pos = menuBar.getMenuCount() - 1; // count the number of menus to insert the TableMenu before 'Window' and 'Help' 257// int offset = 1; 258// log.debug("setMenuBar number of menu items = {}", pos); 259// for (int i = 0; i <= pos; i++) { 260// if (menuBar.getComponent(i) instanceof JMenu) { 261// if (((JMenu) menuBar.getComponent(i)).getText().equals(Bundle.getMessage("MenuHelp"))) { 262// offset = -1; // correct for use as part of ListedTableAction where the Help Menu is not yet present 263// } 264// } 265// } 266 // add separate items, actionhandlers? next 2 menuItem examples copied from TurnoutTableAction 267 268 // JMenuItem item = new JMenuItem(Bundle.getMessage("TurnoutAutomationMenuItemEdit")); 269 // opsMenu.add(item); 270 // item.addActionListener(new ActionListener() { 271 // @Override 272 // public void actionPerformed(ActionEvent e) { 273 // new TurnoutOperationFrame(finalF); 274 // } 275 // }); 276 // menuBar.add(opsMenu, pos + offset); // test 277 // 278 // JMenu speedMenu = new JMenu(Bundle.getMessage("SpeedsMenu")); 279 // item = new JMenuItem(Bundle.getMessage("SpeedsMenuItemDefaults")); 280 // speedMenu.add(item); 281 // item.addActionListener(new ActionListener() { 282 // @Override 283 // public void actionPerformed(ActionEvent e) { 284 // //setDefaultSpeeds(finalF); 285 // } 286 // }); 287 // menuBar.add(speedMenu, pos + offset + 1); // add this menu to the right of the previous 288 // } 289 f.addHelpMenu("package.jmri.jmrit.beantable.OBlockTable", true); 290 } 291 } 292 293 JTextField startAddress = new JTextField(10); 294 JTextField userName = new JTextField(40); 295 SpinnerNumberModel rangeSpinner = new SpinnerNumberModel(1, 1, 100, 1); // maximum 100 items 296 JSpinner numberToAddSpinner = new JSpinner(rangeSpinner); 297 JCheckBox rangeBox = new JCheckBox(Bundle.getMessage("AddRangeBox")); 298 JCheckBox autoSystemNameBox = new JCheckBox(Bundle.getMessage("LabelAutoSysName")); 299 JLabel statusBar = new JLabel(Bundle.getMessage("HardwareAddStatusEnter"), JLabel.LEADING); 300 jmri.UserPreferencesManager pref; 301 JmriJFrame addOBlockFrame = null; 302 // for prefs persistence 303 String systemNameAuto = this.getClass().getName() + ".AutoSystemName"; 304 305 // Three [Addx...] buttons on tabbed bottom box handlers 306 307 @Override 308 protected void addPressed(ActionEvent e) { 309 log.warn("This should not have happened"); 310 } 311 312 protected void addOBlockPressed(ActionEvent e) { 313 pref = jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class); 314 315 if (addOBlockFrame == null) { 316 addOBlockFrame = new JmriJFrame(Bundle.getMessage("TitleAddOBlock"), false, true); 317 addOBlockFrame.addHelpMenu("package.jmri.jmrit.beantable.OBlockTable", true); 318 addOBlockFrame.getContentPane().setLayout(new BoxLayout(addOBlockFrame.getContentPane(), BoxLayout.Y_AXIS)); 319 320 ActionListener okListener = this::createObPressed; 321 ActionListener cancelListener = this::cancelObPressed; 322 323 AddNewBeanPanel anbp = new AddNewBeanPanel(startAddress, userName, numberToAddSpinner, rangeBox, autoSystemNameBox, "ButtonCreate", okListener, cancelListener, statusBar); 324 addOBlockFrame.add(anbp); 325 addOBlockFrame.getRootPane().setDefaultButton(anbp.ok); 326 addOBlockFrame.setEscapeKeyClosesWindow(true); 327 startAddress.setToolTipText(Bundle.getMessage("SysNameToolTip", "OB")); // override tooltip with bean specific letter 328 } 329 startAddress.setBackground(Color.white); 330 // reset status bar text 331 status(Bundle.getMessage("AddBeanStatusEnter"), false); 332 if (pref.getSimplePreferenceState(systemNameAuto)) { 333 autoSystemNameBox.setSelected(true); 334 } 335 addOBlockFrame.pack(); 336 addOBlockFrame.setVisible(true); 337 } 338 339 void cancelObPressed(ActionEvent e) { 340 addOBlockFrame.setVisible(false); 341 addOBlockFrame.dispose(); 342 addOBlockFrame = null; 343 } 344 345 /** 346 * Respond to Create new OBlock button pressed on Add OBlock pane. 347 * Adapted from {@link MemoryTableAction#addPressed(ActionEvent)} 348 * 349 * @param e the click event 350 */ 351 void createObPressed(ActionEvent e) { 352 int numberOfOblocks = 1; 353 354 if (rangeBox.isSelected()) { 355 numberOfOblocks = (Integer) numberToAddSpinner.getValue(); 356 } 357 358 if (numberOfOblocks >= 65 // limited by JSpinnerModel to 100 359 && JmriJOptionPane.showConfirmDialog(addOBlockFrame, 360 Bundle.getMessage("WarnExcessBeans", Bundle.getMessage("OBlocks"), numberOfOblocks), 361 Bundle.getMessage("WarningTitle"), 362 JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION ) { 363 return; 364 } 365 366 String uName = NamedBean.normalizeUserName(userName.getText()); 367 if (uName != null && uName.isEmpty()) { 368 uName = null; 369 } 370 String sName = startAddress.getText().trim(); 371 // initial check for empty entries 372 if (autoSystemNameBox.isSelected()) { 373 startAddress.setBackground(Color.white); 374 } else if (sName.equals("")) { 375 status(Bundle.getMessage("WarningSysNameEmpty"), true); 376 startAddress.setBackground(Color.red); 377 return; 378 } else if (!sName.startsWith("OB")) { 379 sName = "OB" + sName; 380 } 381 // Add some entry pattern checking, before assembling sName and handing it to the OBlockManager 382 StringBuilder statusMessage = new StringBuilder(Bundle.getMessage("ItemCreateFeedback", Bundle.getMessage("BeanNameOBlock"))); 383 String errorMessage = null; 384 for (int x = 0; x < numberOfOblocks; x++) { 385 if (uName != null && !uName.isEmpty() && oblockManager.getByUserName(uName) != null && !pref.getPreferenceState(getClassName(), "duplicateUserName")) { 386 jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class). 387 showErrorMessage(Bundle.getMessage("ErrorTitle"), Bundle.getMessage("ErrorDuplicateUserName", uName), getClassName(), "duplicateUserName", false, true); 388 // show in status bar 389 errorMessage = Bundle.getMessage("ErrorDuplicateUserName", uName); 390 status(errorMessage, true); 391 uName = null; // new OBlock objects always receive a valid system name using the next free index, but uName names must not be in use so use none in that case 392 } 393 if (!sName.isEmpty() && oblockManager.getBySystemName(sName) != null && !pref.getPreferenceState(getClassName(), "duplicateSystemName")) { 394 jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class). 395 showErrorMessage(Bundle.getMessage("ErrorTitle"), Bundle.getMessage("ErrorDuplicateSystemName", sName), getClassName(), "duplicateSystemName", false, true); 396 // show in status bar 397 errorMessage = Bundle.getMessage("ErrorDuplicateSystemName", sName); 398 status(errorMessage, true); 399 return; // new OBlock objects are always valid, but system names must not be in use so skip in that case 400 } 401 OBlock oblk; 402 String xName = ""; 403 try { 404 if (autoSystemNameBox.isSelected()) { 405 assert uName != null; 406 oblk = oblockManager.createNewOBlock(uName); 407 if (oblk == null) { 408 xName = uName; 409 throw new java.lang.IllegalArgumentException(); 410 } 411 } else { 412 oblk = oblockManager.createNewOBlock(sName, uName); 413 if (oblk == null) { 414 xName = sName; 415 throw new java.lang.IllegalArgumentException(); 416 } 417 } 418 } catch (IllegalArgumentException ex) { 419 // uName input no good 420 handleCreateException(xName); 421 errorMessage = Bundle.getMessage("ErrorAddFailedCheck"); 422 status(errorMessage, true); 423 return; // without creating 424 } 425 426 // add first and last names to statusMessage uName feedback string 427 // only mention first and last of rangeBox added 428 if (x == 0 || x == numberOfOblocks - 1) { 429 statusMessage.append(" ").append(sName).append(" (").append(uName).append(")"); 430 } 431 if (x == numberOfOblocks - 2) { 432 statusMessage.append(" ").append(Bundle.getMessage("ItemCreateUpTo")).append(" "); 433 } 434 435 // bump system & uName names 436 if (!autoSystemNameBox.isSelected()) { 437 sName = nextName(sName); 438 } 439 if (uName != null) { 440 uName = nextName(uName); 441 } 442 } // end of for loop creating rangeBox of OBlocks 443 444 // provide feedback to uName 445 if (errorMessage == null) { 446 status(statusMessage.toString(), false); 447 // statusBar.setForeground(Color.red); handled when errorMassage is set to differentiate urgency 448 } 449 450 pref.setSimplePreferenceState(systemNameAuto, autoSystemNameBox.isSelected()); 451 // Notify changes 452 oblocks.fireTableDataChanged(); 453 } 454 455 void addPortalPressed(ActionEvent e) { 456 if (portalFrame == null) { 457 portalFrame = new PortalEditFrame(Bundle.getMessage("TitleAddPortal"), null, portals); 458 } 459 //portalFrame.updatePortalList(); 460 portalFrame.resetFrame(); 461 portalFrame.pack(); 462 portalFrame.setVisible(true); 463 } 464 465 void addSignalPressed(ActionEvent e) { 466 if (!signals.editMode()) { 467 signals.setEditMode(true); 468 if (signalFrame == null) { 469 signalFrame = new SignalEditFrame(Bundle.getMessage("TitleAddSignal"), null, null, signals); 470 } 471 //signalFrame.updateSignalList(); 472 signalFrame.resetFrame(); 473 signalFrame.pack(); 474 signalFrame.setVisible(true); 475 } 476 } 477 478 void handleCreateException(String sysName) { 479 JmriJOptionPane.showMessageDialog(addOBlockFrame, 480 Bundle.getMessage("ErrorOBlockAddFailed", sysName) + "\n" + Bundle.getMessage("ErrorAddFailedCheck"), 481 Bundle.getMessage("ErrorTitle"), 482 JmriJOptionPane.ERROR_MESSAGE); 483 } 484 485 /** 486 * Create or update the blockPathTableModel. Used in EditBlockPath pane. 487 * 488// * @param block to build a table for 489 */ 490// private void setBlockPathTableModel(OBlock block) { 491// BlockPathTableModel blockPathTableModel = tf.getBlockPathTableModel(block); 492// } 493 494// @Override // loops with ListedTableItem.dispose() 495// public void dispose() { 496// //jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class).setSimplePreferenceState(getClassName() + ":LengthUnitMetric", centimeterBox.isSelected()); 497// f.dispose(); 498// super.dispose(); 499// } 500 501 @Override 502 protected String getClassName() { 503 return OBlockTableAction.class.getName(); 504 } 505 506 @Override 507 public String getClassDescription() { 508 return Bundle.getMessage("TitleOBlockTable"); 509 } 510 511 @Override 512 public void setMessagePreferencesDetails() { 513 InstanceManager.getDefault(jmri.UserPreferencesManager.class). 514 setPreferenceItemDetails(getClassName(), "duplicateUserName", Bundle.getMessage("HideDupUserError")); // NOI18N 515 InstanceManager.getDefault(jmri.UserPreferencesManager.class). 516 setPreferenceItemDetails(getClassName(), "duplicateSystemName", Bundle.getMessage("HideDupSysError")); // NOI18N 517 super.setMessagePreferencesDetails(); 518 } 519 520// @Override 521// public void addToPanel(AbstractTableTabAction<OBlock> f) { 522// // not used (checkboxes etc.) 523// } 524 525 /** 526 * {@inheritDoc} 527 */ 528 @Override 529 public void propertyChange(PropertyChangeEvent e) { 530 String property = e.getPropertyName(); 531 if (log.isDebugEnabled()) { 532 log.debug("PropertyChangeEvent property = {} source= {}", property, e.getSource().getClass().getName()); 533 } 534 switch (property) { 535 case "StateStored": 536 //isStateStored.setSelected(oblockManager.isStateStored()); 537 break; 538 case "UseFastClock": 539 default: 540 //isFastClockUsed.setSelected(portalManager.isFastClockUsed()); 541 break; 542 } 543 } 544 545 void status(String message, boolean warn){ 546 statusBar.setText(message); 547 statusBar.setForeground(warn ? Color.red : Color.gray); 548 } 549 550 @Override 551 protected String helpTarget() { 552 return "package.jmri.jmrit.beantable.OBlockTable"; 553 } 554 555 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OBlockTableAction.class); 556 557}