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