001package jmri.jmrit.symbolicprog.tabbedframe; 002 003import java.awt.*; 004import java.awt.event.ActionEvent; 005import java.awt.event.ItemEvent; 006import java.awt.event.ItemListener; 007import java.util.ArrayList; 008import java.util.List; 009import javax.annotation.Nonnull; 010import javax.annotation.OverridingMethodsMustInvokeSuper; 011import javax.swing.*; 012 013import jmri.AddressedProgrammerManager; 014import jmri.GlobalProgrammerManager; 015import jmri.InstanceManager; 016import jmri.InvokeOnAnyThread; 017import jmri.InvokeOnGuiThread; 018import jmri.Programmer; 019import jmri.ProgrammingMode; 020import jmri.ShutDownTask; 021import jmri.UserPreferencesManager; 022import jmri.implementation.swing.SwingShutDownTask; 023import jmri.jmrit.XmlFile; 024import jmri.jmrit.decoderdefn.DecoderFile; 025import jmri.jmrit.decoderdefn.DecoderIndexFile; 026import jmri.jmrit.roster.*; 027import jmri.jmrit.symbolicprog.*; 028import jmri.util.BusyGlassPane; 029import jmri.util.FileUtil; 030import jmri.util.JmriJFrame; 031import jmri.util.ThreadingUtil; 032import jmri.util.swing.JmriJOptionPane; 033 034import org.jdom2.Attribute; 035import org.jdom2.Element; 036 037/** 038 * Frame providing a command station programmer from decoder definition files. 039 * 040 * @author Bob Jacobsen Copyright (C) 2001, 2004, 2005, 2008, 2014, 2018, 2019, 2025 041 * @author D Miller Copyright 2003, 2005 042 * @author Howard G. Penny Copyright (C) 2005 043 */ 044abstract public class PaneProgFrame extends JmriJFrame 045 implements java.beans.PropertyChangeListener, PaneContainer { 046 047 // members to contain working variable, CV values 048 JLabel progStatus = new JLabel(Bundle.getMessage("StateIdle")); 049 CvTableModel cvModel; 050 VariableTableModel variableModel; 051 052 ResetTableModel resetModel; 053 JMenu resetMenu = null; 054 055 ArrayList<ExtraMenuTableModel> extraMenuModelList; 056 ArrayList<JMenu> extraMenuList = new ArrayList<>(); 057 058 Programmer mProgrammer; 059 boolean noDecoder = false; 060 061 JMenuBar menuBar = new JMenuBar(); 062 063 JPanel tempPane; // passed around during construction 064 065 boolean _opsMode; 066 067 boolean maxFnNumDirty = false; 068 String maxFnNumOld = ""; 069 String maxFnNumNew = ""; 070 071 RosterEntry _rosterEntry; 072 RosterEntryPane _rPane = null; 073 RosterPhysicsPane _physicsPane = null; 074 FunctionLabelPane _flPane = null; 075 RosterMediaPane _rMPane = null; 076 String _frameEntryId; 077 078 List<JPanel> paneList = new ArrayList<>(); 079 int paneListIndex; 080 081 List<Element> decoderPaneList; 082 083 BusyGlassPane glassPane; 084 List<JComponent> activeComponents = new ArrayList<>(); 085 086 String filename; 087 String programmerShowEmptyPanes = ""; 088 String decoderShowEmptyPanes = ""; 089 String decoderAllowResetDefaults = ""; 090 String suppressFunctionLabels = ""; 091 String suppressRosterMedia = ""; 092 093 // GUI member declarations 094 JTabbedPane tabPane; 095 JToggleButton readChangesButton = new JToggleButton(Bundle.getMessage("ButtonReadChangesAllSheets")); 096 JToggleButton writeChangesButton = new JToggleButton(Bundle.getMessage("ButtonWriteChangesAllSheets")); 097 JToggleButton readAllButton = new JToggleButton(Bundle.getMessage("ButtonReadAllSheets")); 098 JToggleButton writeAllButton = new JToggleButton(Bundle.getMessage("ButtonWriteAllSheets")); 099 100 ItemListener l1; 101 ItemListener l2; 102 ItemListener l3; 103 ItemListener l4; 104 105 ShutDownTask decoderDirtyTask; 106 ShutDownTask fileDirtyTask; 107 108 // holds a count of incomplete threads launched at ctor time; goes to zero when they're done 109 public java.util.concurrent.atomic.AtomicInteger threadCount = new java.util.concurrent.atomic.AtomicInteger(0); 110 111 public RosterEntryPane getRosterPane() { return _rPane;} 112 public FunctionLabelPane getFnLabelPane() { return _flPane;} 113 114 /** 115 * Abstract method to provide a JPanel setting the programming mode, if 116 * appropriate. 117 * <p> 118 * A null value is ignored (?) 119 * @return new mode panel for inclusion in the GUI 120 */ 121 abstract protected JPanel getModePane(); 122 123 @InvokeOnGuiThread 124 protected void installComponents() { 125 126 String title = " : "+_frameEntryId; 127 128 if (checkDontDetachPanes()) { 129 tabPane = new JTabbedPane(); 130 } else { 131 tabPane = new jmri.util.org.mitre.jawb.swing.DetachableTabbedPane(title); 132 } 133 134 // create ShutDownTasks 135 if (decoderDirtyTask == null) { 136 decoderDirtyTask = new SwingShutDownTask("DecoderPro Decoder Window Check", 137 Bundle.getMessage("PromptQuitWindowNotWrittenDecoder"), null, this) { 138 @Override 139 public boolean checkPromptNeeded() { 140 return !checkDirtyDecoder(); 141 } 142 }; 143 } 144 jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).register(decoderDirtyTask); 145 if (fileDirtyTask == null) { 146 fileDirtyTask = new SwingShutDownTask("DecoderPro Decoder Window Check", 147 Bundle.getMessage("PromptQuitWindowNotWrittenConfig"), 148 Bundle.getMessage("PromptSaveQuit"), this) { 149 @Override 150 public boolean checkPromptNeeded() { 151 return !checkDirtyFile(); 152 } 153 154 @Override 155 public boolean doPrompt() { 156 // storeFile returns false if failed, so abort shutdown 157 return storeFile(); 158 } 159 }; 160 } 161 jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).register(fileDirtyTask); 162 163 // Create a menu bar 164 setJMenuBar(menuBar); 165 166 // add a "File" menu 167 JMenu fileMenu = new JMenu(Bundle.getMessage("MenuFile")); 168 menuBar.add(fileMenu); 169 170 // add a "Factory Reset" menu 171 resetMenu = new JMenu(Bundle.getMessage("MenuReset")); 172 menuBar.add(resetMenu); 173 resetMenu.add(new FactoryResetAction(Bundle.getMessage("MenuFactoryReset"), resetModel, this)); 174 resetMenu.setEnabled(false); 175 176 // Add a save item 177 JMenuItem menuItem = new JMenuItem(Bundle.getMessage("MenuSaveNoDots")); 178 menuItem.addActionListener(e -> storeFile() 179 180 ); 181 menuItem.setAccelerator(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_S, java.awt.event.KeyEvent.META_DOWN_MASK)); 182 fileMenu.add(menuItem); 183 184 JMenu printSubMenu = new JMenu(Bundle.getMessage("MenuPrint")); 185 printSubMenu.add(new PrintAction(Bundle.getMessage("MenuPrintAll"), this, false)); 186 printSubMenu.add(new PrintCvAction(Bundle.getMessage("MenuPrintCVs"), cvModel, this, false, _rosterEntry)); 187 fileMenu.add(printSubMenu); 188 189 JMenu printPreviewSubMenu = new JMenu(Bundle.getMessage("MenuPrintPreview")); 190 printPreviewSubMenu.add(new PrintAction(Bundle.getMessage("MenuPrintPreviewAll"), this, true)); 191 printPreviewSubMenu.add(new PrintCvAction(Bundle.getMessage("MenuPrintPreviewCVs"), cvModel, this, true, _rosterEntry)); 192 fileMenu.add(printPreviewSubMenu); 193 194 // add "Import" submenu; this is hierarchical because 195 // some of the names are so long, and we expect more formats 196 JMenu importSubMenu = new JMenu(Bundle.getMessage("MenuImport")); 197 fileMenu.add(importSubMenu); 198 importSubMenu.add(new CsvImportAction(Bundle.getMessage("MenuImportCSV"), cvModel, this, progStatus)); 199 importSubMenu.add(new Pr1ImportAction(Bundle.getMessage("MenuImportPr1"), cvModel, this, progStatus)); 200 importSubMenu.add(new LokProgImportAction(Bundle.getMessage("MenuImportLokProg"), cvModel, this, progStatus)); 201 importSubMenu.add(new QuantumCvMgrImportAction(Bundle.getMessage("MenuImportQuantumCvMgr"), cvModel, this, progStatus)); 202 importSubMenu.add(new TcsImportAction(Bundle.getMessage("MenuImportTcsFile"), cvModel, variableModel, this, progStatus, _rosterEntry)); 203 if (TcsDownloadAction.willBeEnabled()) { 204 importSubMenu.add(new TcsDownloadAction(Bundle.getMessage("MenuImportTcsCS"), cvModel, variableModel, this, progStatus, _rosterEntry)); 205 } 206 importSubMenu.add(new CsvFunctionImportAction(Bundle.getMessage("MenuImportFunctions"), this)); 207 208 // add "Export" submenu; this is hierarchical because 209 // some of the names are so long, and we expect more formats 210 JMenu exportSubMenu = new JMenu(Bundle.getMessage("MenuExport")); 211 fileMenu.add(exportSubMenu); 212 exportSubMenu.add(new CsvExportAction(Bundle.getMessage("MenuExportCSV"), cvModel, this)); 213 exportSubMenu.add(new CsvExportModifiedAction(Bundle.getMessage("MenuExportCSVModified"), cvModel, this)); 214 exportSubMenu.add(new Pr1ExportAction(Bundle.getMessage("MenuExportPr1DOS"), cvModel, this)); 215 exportSubMenu.add(new Pr1WinExportAction(Bundle.getMessage("MenuExportPr1WIN"), cvModel, this)); 216 exportSubMenu.add(new CsvExportVariablesAction(Bundle.getMessage("MenuExportVariables"), variableModel, this)); 217 exportSubMenu.add(new TcsExportAction(Bundle.getMessage("MenuExportTcsFile"), cvModel, variableModel, _rosterEntry, this)); 218 if (TcsDownloadAction.willBeEnabled()) { 219 exportSubMenu.add(new TcsUploadAction(Bundle.getMessage("MenuExportTcsCS"), cvModel, variableModel, _rosterEntry, this)); 220 } 221 exportSubMenu.add(new CsvFunctionExportAction(Bundle.getMessage("MenuExportFunctions"), this)); 222 223 // Speed table submenu in File menu 224 ThreadingUtil.runOnGUIEventually( ()->{ 225 JMenu speedTableSubMenu = new JMenu(Bundle.getMessage("MenuSpeedTable")); 226 fileMenu.add(speedTableSubMenu); 227 ButtonGroup SpeedTableNumbersGroup = new ButtonGroup(); 228 UserPreferencesManager upm = InstanceManager.getDefault(UserPreferencesManager.class); 229 Object speedTableNumbersSelectionObj = upm.getProperty(SpeedTableNumbers.class.getName(), "selection"); 230 231 SpeedTableNumbers speedTableNumbersSelection = 232 speedTableNumbersSelectionObj != null 233 ? SpeedTableNumbers.valueOf(speedTableNumbersSelectionObj.toString()) 234 : null; 235 236 for (SpeedTableNumbers speedTableNumbers : SpeedTableNumbers.values()) { 237 JRadioButtonMenuItem rbMenuItem = new JRadioButtonMenuItem(speedTableNumbers.toString()); 238 rbMenuItem.addActionListener((ActionEvent event) -> { 239 rbMenuItem.setSelected(true); 240 upm.setProperty(SpeedTableNumbers.class.getName(), "selection", speedTableNumbers.name()); 241 JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("MenuSpeedTable_CloseReopenWindow")); 242 }); 243 rbMenuItem.setSelected(speedTableNumbers == speedTableNumbersSelection); 244 speedTableSubMenu.add(rbMenuItem); 245 SpeedTableNumbersGroup.add(rbMenuItem); 246 } 247 }); 248 249 // to control size, we need to insert a single 250 // JPanel, then have it laid out with BoxLayout 251 JPanel pane = new JPanel(); 252 tempPane = pane; 253 254 // general GUI config 255 pane.setLayout(new BorderLayout()); 256 257 // most of the GUI is done from XML in readConfig() function 258 // which configures the tabPane 259 pane.add(tabPane, BorderLayout.CENTER); 260 261 // and put that pane into the JFrame 262 getContentPane().add(pane); 263 264 // configure GUI buttons 265 ThreadingUtil.runOnGUIEventually( ()->{ 266 configureButtons(); 267 }); 268 269 } 270 271 @InvokeOnGuiThread 272 void configureButtons() { 273 // set read buttons enabled state, tooltips 274 enableReadButtons(); 275 276 readChangesButton.addItemListener(l1 = e -> { 277 if (e.getStateChange() == ItemEvent.SELECTED) { 278 prepGlassPane(readChangesButton); 279 readChangesButton.setText(Bundle.getMessage("ButtonStopReadChangesAll")); 280 readChanges(); 281 } else { 282 if (_programmingPane != null) { 283 _programmingPane.stopProgramming(); 284 } 285 paneListIndex = paneList.size(); 286 readChangesButton.setText(Bundle.getMessage("ButtonReadChangesAllSheets")); 287 } 288 }); 289 290 readAllButton.addItemListener(l3 = e -> { 291 if (e.getStateChange() == ItemEvent.SELECTED) { 292 prepGlassPane(readAllButton); 293 readAllButton.setText(Bundle.getMessage("ButtonStopReadAll")); 294 readAll(); 295 } else { 296 if (_programmingPane != null) { 297 _programmingPane.stopProgramming(); 298 } 299 paneListIndex = paneList.size(); 300 readAllButton.setText(Bundle.getMessage("ButtonReadAllSheets")); 301 } 302 }); 303 304 writeChangesButton.setToolTipText(Bundle.getMessage("TipWriteHighlightedValues")); 305 writeChangesButton.addItemListener(l2 = e -> { 306 if (e.getStateChange() == ItemEvent.SELECTED) { 307 prepGlassPane(writeChangesButton); 308 writeChangesButton.setText(Bundle.getMessage("ButtonStopWriteChangesAll")); 309 writeChanges(); 310 } else { 311 if (_programmingPane != null) { 312 _programmingPane.stopProgramming(); 313 } 314 paneListIndex = paneList.size(); 315 writeChangesButton.setText(Bundle.getMessage("ButtonWriteChangesAllSheets")); 316 } 317 }); 318 319 writeAllButton.setToolTipText(Bundle.getMessage("TipWriteAllValues")); 320 writeAllButton.addItemListener(l4 = e -> { 321 if (e.getStateChange() == ItemEvent.SELECTED) { 322 prepGlassPane(writeAllButton); 323 writeAllButton.setText(Bundle.getMessage("ButtonStopWriteAll")); 324 writeAll(); 325 } else { 326 if (_programmingPane != null) { 327 _programmingPane.stopProgramming(); 328 } 329 paneListIndex = paneList.size(); 330 writeAllButton.setText(Bundle.getMessage("ButtonWriteAllSheets")); 331 } 332 }); 333 } 334 335 void setProgrammingGui(JPanel bottom) { 336 // see if programming mode is available 337 JPanel tempModePane = null; 338 if (!noDecoder) { 339 tempModePane = getModePane(); 340 } 341 if (tempModePane != null) { 342 // if so, configure programming part of GUI 343 // add buttons 344 JPanel bottomButtons = new JPanel(); 345 bottomButtons.setLayout(new BoxLayout(bottomButtons, BoxLayout.X_AXIS)); 346 347 bottomButtons.add(readChangesButton); 348 bottomButtons.add(writeChangesButton); 349 bottomButtons.add(readAllButton); 350 bottomButtons.add(writeAllButton); 351 bottom.add(bottomButtons); 352 353 // add programming mode 354 bottom.add(new JSeparator(javax.swing.SwingConstants.HORIZONTAL)); 355 JPanel temp = new JPanel(); 356 bottom.add(temp); 357 temp.add(tempModePane); 358 } else { 359 // set title to Editing 360 super.setTitle(Bundle.getMessage("TitleEditPane", _frameEntryId)); 361 } 362 363 // add space for (programming) status message 364 bottom.add(new JSeparator(javax.swing.SwingConstants.HORIZONTAL)); 365 progStatus.setAlignmentX(JLabel.CENTER_ALIGNMENT); 366 bottom.add(progStatus); 367 } 368 369 // ================== Search section ================== 370 371 // create and add the Search GUI 372 void setSearchGui(JPanel bottom) { 373 // search field 374 searchBar = new jmri.util.swing.SearchBar(searchForwardTask, searchBackwardTask, searchDoneTask); 375 searchBar.setVisible(false); // start not visible 376 searchBar.configureKeyModifiers(this); 377 bottom.add(searchBar); 378 } 379 380 jmri.util.swing.SearchBar searchBar; 381 static class SearchPair { 382 WatchingLabel label; 383 JPanel tab; 384 SearchPair(WatchingLabel label, @Nonnull JPanel tab) { 385 this.label = label; 386 this.tab = tab; 387 } 388 } 389 390 ArrayList<SearchPair> searchTargetList; 391 int nextSearchTarget = 0; 392 393 // Load the array of search targets 394 protected void loadSearchTargets() { 395 if (searchTargetList != null) return; 396 397 searchTargetList = new ArrayList<>(); 398 399 for (JPanel p : getPaneList()) { 400 for (Component c : p.getComponents()) { 401 loadJPanel(c, p); 402 } 403 } 404 405 // add the panes themselves 406 for (JPanel tab : getPaneList()) { 407 searchTargetList.add( new SearchPair( null, tab )); 408 } 409 } 410 411 // Recursive load of possible search targets 412 protected void loadJPanel(Component c, JPanel tab) { 413 if (c instanceof JPanel) { 414 for (Component d : ((JPanel)c).getComponents()) { 415 loadJPanel(d, tab); 416 } 417 } else if (c instanceof JScrollPane) { 418 loadJPanel( ((JScrollPane)c).getViewport().getView(), tab); 419 } else if (c instanceof WatchingLabel) { 420 searchTargetList.add( new SearchPair( (WatchingLabel)c, tab)); 421 } 422 } 423 424 // Search didn't find anything at all 425 protected void searchDidNotFind() { 426 java.awt.Toolkit.getDefaultToolkit().beep(); 427 } 428 429 // Search succeeded, go to the result 430 protected void searchGoesTo(SearchPair result) { 431 tabPane.setSelectedComponent(result.tab); 432 if (result.label != null) { 433 SwingUtilities.invokeLater(() -> result.label.getWatched().requestFocus()); 434 } else { 435 log.trace("search result set to tab {}", result.tab); 436 } 437 } 438 439 // Check a single case to see if its search match 440 // @return true for matched 441 private boolean checkSearchTarget(int index, String target) { 442 boolean result = false; 443 if (searchTargetList.get(index).label != null ) { 444 // match label text 445 if ( ! searchTargetList.get(index).label.getText().toUpperCase().contains(target.toUpperCase() ) ) { 446 return false; 447 } 448 // only match if showing 449 return searchTargetList.get(index).label.isShowing(); 450 } else { 451 // Match pane label. 452 // Finding the tab requires a search here. 453 // Could have passed a clue along in SwingUtilities 454 for (int i = 0; i < tabPane.getTabCount(); i++) { 455 if (tabPane.getComponentAt(i) == searchTargetList.get(index).tab) { 456 result = tabPane.getTitleAt(i).toUpperCase().contains(target.toUpperCase()); 457 } 458 } 459 } 460 return result; 461 } 462 463 // Invoked by forward search operation 464 private final Runnable searchForwardTask = new Runnable() { 465 @Override 466 public void run() { 467 log.trace("start forward"); 468 loadSearchTargets(); 469 String target = searchBar.getSearchString(); 470 471 nextSearchTarget++; 472 if (nextSearchTarget < 0 ) nextSearchTarget = 0; 473 if (nextSearchTarget >= searchTargetList.size() ) nextSearchTarget = 0; 474 475 int startingSearchTarget = nextSearchTarget; 476 477 while (nextSearchTarget < searchTargetList.size()) { 478 if ( checkSearchTarget(nextSearchTarget, target)) { 479 // hit! 480 searchGoesTo(searchTargetList.get(nextSearchTarget)); 481 return; 482 } 483 nextSearchTarget++; 484 } 485 486 // end reached, wrap 487 nextSearchTarget = 0; 488 while (nextSearchTarget < startingSearchTarget) { 489 if ( checkSearchTarget(nextSearchTarget, target)) { 490 // hit! 491 searchGoesTo(searchTargetList.get(nextSearchTarget)); 492 return; 493 } 494 nextSearchTarget++; 495 } 496 // not found 497 searchDidNotFind(); 498 } 499 }; 500 501 // Invoked by backward search operation 502 private final Runnable searchBackwardTask = new Runnable() { 503 @Override 504 public void run() { 505 log.trace("start backward"); 506 loadSearchTargets(); 507 String target = searchBar.getSearchString(); 508 509 nextSearchTarget--; 510 if (nextSearchTarget < 0 ) nextSearchTarget = searchTargetList.size()-1; 511 if (nextSearchTarget >= searchTargetList.size() ) nextSearchTarget = searchTargetList.size()-1; 512 513 int startingSearchTarget = nextSearchTarget; 514 515 while (nextSearchTarget > 0) { 516 if ( checkSearchTarget(nextSearchTarget, target)) { 517 // hit! 518 searchGoesTo(searchTargetList.get(nextSearchTarget)); 519 return; 520 } 521 nextSearchTarget--; 522 } 523 524 // start reached, wrap 525 nextSearchTarget = searchTargetList.size() - 1; 526 while (nextSearchTarget > startingSearchTarget) { 527 if ( checkSearchTarget(nextSearchTarget, target)) { 528 // hit! 529 searchGoesTo(searchTargetList.get(nextSearchTarget)); 530 return; 531 } 532 nextSearchTarget--; 533 } 534 // not found 535 searchDidNotFind(); 536 } 537 }; 538 539 // Invoked when search bar Done is pressed 540 private final Runnable searchDoneTask = new Runnable() { 541 @Override 542 public void run() { 543 log.debug("done with search bar"); 544 searchBar.setVisible(false); 545 } 546 }; 547 548 // =================== End of search section ================== 549 550 public List<JPanel> getPaneList() { 551 return paneList; 552 } 553 554 void addHelp() { 555 addHelpMenu("package.jmri.jmrit.symbolicprog.tabbedframe.PaneProgFrame", true); 556 } 557 558 @Override 559 public Dimension getPreferredSize() { 560 Dimension screen = getMaximumSize(); 561 int width = Math.min(super.getPreferredSize().width, screen.width); 562 int height = Math.min(super.getPreferredSize().height, screen.height); 563 return new Dimension(width, height); 564 } 565 566 @Override 567 public Dimension getMaximumSize() { 568 Dimension screen = getToolkit().getScreenSize(); 569 return new Dimension(screen.width, screen.height - 35); 570 } 571 572 /** 573 * Enable the [Read all] and [Read changes] buttons if possible. This checks 574 * to make sure this is appropriate, given the attached programmer's 575 * capability. 576 */ 577 void enableReadButtons() { 578 readChangesButton.setToolTipText(Bundle.getMessage("TipReadChanges")); 579 readAllButton.setToolTipText(Bundle.getMessage("TipReadAll")); 580 // check with CVTable programmer to see if read is possible 581 if (cvModel != null && cvModel.getProgrammer() != null 582 && !cvModel.getProgrammer().getCanRead() 583 || noDecoder) { 584 // can't read, disable the button 585 readChangesButton.setEnabled(false); 586 readAllButton.setEnabled(false); 587 readChangesButton.setToolTipText(Bundle.getMessage("TipNoRead")); 588 readAllButton.setToolTipText(Bundle.getMessage("TipNoRead")); 589 } else { 590 readChangesButton.setEnabled(true); 591 readAllButton.setEnabled(true); 592 } 593 } 594 595 /** 596 * Initialization sequence: 597 * <ul> 598 * <li> Ask the RosterEntry to read its contents 599 * <li> If the decoder file is specified, open and load it, otherwise get 600 * the decoder filename from the RosterEntry and load that. Note that we're 601 * assuming the roster entry has the right decoder, at least w.r.t. the loco 602 * file. 603 * <li> Fill CV values from the roster entry 604 * <li> Create the programmer panes 605 * </ul> 606 * 607 * @param pDecoderFile XML file defining the decoder contents; if null, 608 * the decoder definition is found from the 609 * RosterEntry 610 * @param pRosterEntry RosterEntry for information on this locomotive 611 * @param pFrameEntryId Roster ID (entry) loaded into the frame 612 * @param pProgrammerFile Name of the programmer file to use 613 * @param pProg Programmer object to be used to access CVs 614 * @param opsMode true for opsMode, else false. 615 */ 616 public PaneProgFrame(DecoderFile pDecoderFile, @Nonnull RosterEntry pRosterEntry, 617 String pFrameEntryId, String pProgrammerFile, Programmer pProg, boolean opsMode) { 618 super(Bundle.getMessage("TitleProgPane", pFrameEntryId)); 619 620 _rosterEntry = pRosterEntry; 621 _opsMode = opsMode; 622 filename = pProgrammerFile; 623 mProgrammer = pProg; 624 _frameEntryId = pFrameEntryId; 625 626 // create the tables 627 cvModel = new CvTableModel(progStatus, mProgrammer); 628 629 variableModel = new VariableTableModel(progStatus, new String[] {"Name", "Value"}, 630 cvModel); 631 632 resetModel = new ResetTableModel(progStatus, mProgrammer); 633 extraMenuModelList = new ArrayList<>(); 634 635 // handle the roster entry 636 _rosterEntry.setOpen(true); 637 638 installComponents(); 639 640 threadCount.incrementAndGet(); 641 new javax.swing.SwingWorker<Object, Object>(){ 642 @Override 643 public Object doInBackground() { 644 if (_rosterEntry.getFileName() != null) { 645 // set the loco file name in the roster entry 646 _rosterEntry.readFile(); // read, but don't yet process 647 } 648 649 log.trace("starting to load decoderfile"); 650 if (pDecoderFile != null) { 651 loadDecoderFile(pDecoderFile, _rosterEntry); 652 } else { 653 loadDecoderFromLoco(pRosterEntry); 654 } 655 log.trace("end loading decoder file"); 656 return null; 657 } 658 @Override 659 protected void done() { 660 ctorPhase2(); 661 threadCount.decrementAndGet(); 662 } 663 }.execute(); 664 } 665 666 // This is invoked at the end of the 667 // PaneProgFrame constructor, after the roster entry and DecoderFile 668 // have been read in 669 @InvokeOnGuiThread 670 void ctorPhase2() { 671 // save default values 672 saveDefaults(); 673 674 // finally fill the Variable and CV values from the specific loco file 675 if (_rosterEntry.getFileName() != null) { 676 _rosterEntry.loadCvModel(variableModel, cvModel); 677 } 678 679 // mark file state as consistent 680 variableModel.setFileDirty(false); 681 682 // if the Reset Table was used lets enable the menu item 683 if (!_opsMode || resetModel.hasOpsModeReset()) { 684 if (resetModel.getRowCount() > 0) { 685 resetMenu.setEnabled(true); 686 } 687 } 688 689 // if there are extra menus defined, enable them 690 log.trace("enabling {} {}", extraMenuModelList.size(), extraMenuModelList); 691 for (int i = 0; i<extraMenuModelList.size(); i++) { 692 log.trace("enabling {} {}", _opsMode, extraMenuModelList.get(i).hasOpsModeReset()); 693 if ( !_opsMode || extraMenuModelList.get(i).hasOpsModeReset()) { 694 if (extraMenuModelList.get(i).getRowCount() > 0) { 695 extraMenuList.get(i).setEnabled(true); 696 } 697 } 698 } 699 700 // set the programming mode 701 if (mProgrammer != null) { 702 if (InstanceManager.getOptionalDefault(AddressedProgrammerManager.class).isPresent() 703 || InstanceManager.getOptionalDefault(GlobalProgrammerManager.class).isPresent()) { 704 // go through in preference order, trying to find a mode 705 // that exists in both the programmer and decoder. 706 // First, get attributes. If not present, assume that 707 // all modes are usable 708 Element programming = null; 709 if (decoderRoot != null 710 && (programming = decoderRoot.getChild("decoder").getChild("programming")) != null) { 711 712 // add a verify-write facade if configured 713 Programmer pf = mProgrammer; 714 if (getDoConfirmRead()) { 715 pf = new jmri.implementation.VerifyWriteProgrammerFacade(pf); 716 log.debug("adding VerifyWriteProgrammerFacade, new programmer is {}", pf); 717 } 718 // add any facades defined in the decoder file 719 pf = jmri.implementation.ProgrammerFacadeSelector 720 .loadFacadeElements(programming, pf, getCanCacheDefault(), mProgrammer); 721 log.debug("added any other FacadeElements, new programmer is {}", pf); 722 mProgrammer = pf; 723 cvModel.setProgrammer(pf); 724 resetModel.setProgrammer(pf); 725 for (var model : extraMenuModelList) { 726 model.setProgrammer(pf); 727 } 728 log.debug("Found programmer: {}", cvModel.getProgrammer()); 729 } 730 731 // done after setting facades in case new possibilities appear 732 if (programming != null) { 733 pickProgrammerMode(programming); 734 // reset the read buttons if the mode changes 735 enableReadButtons(); 736 if (noDecoder) { 737 writeChangesButton.setEnabled(false); 738 writeAllButton.setEnabled(false); 739 } 740 } else { 741 log.debug("Skipping programmer setup because found no programmer element"); 742 } 743 744 } else { 745 log.error("Can't set programming mode, no programmer instance"); 746 } 747 } 748 749 // and build the GUI (after programmer mode because it depends on what's available) 750 loadProgrammerFile(_rosterEntry); 751 752 // optionally, add extra panes from the decoder file 753 Attribute a; 754 if ((a = programmerRoot.getChild("programmer").getAttribute("decoderFilePanes")) != null 755 && a.getValue().equals("yes")) { 756 if (decoderRoot != null) { 757 if (log.isDebugEnabled()) { 758 log.debug("will process {} pane definitions from decoder file", decoderPaneList.size()); 759 } 760 for (Element element : decoderPaneList) { 761 // load each pane 762 String pname = jmri.util.jdom.LocaleSelector.getAttribute(element, "name"); 763 764 // handle include/exclude 765 if (isIncludedFE(element, modelElem, _rosterEntry, "", "")) { 766 newPane(pname, element, modelElem, true, false); // show even if empty not a programmer pane 767 log.debug("PaneProgFrame init - pane {} added", pname); // these are MISSING in RosterPrint 768 } 769 } 770 } 771 } 772 773 JPanel bottom = new JPanel(); 774 bottom.setLayout(new BoxLayout(bottom, BoxLayout.Y_AXIS)); 775 tempPane.add(bottom, BorderLayout.SOUTH); 776 777 // now that programmer is configured, set the programming GUI 778 setProgrammingGui(bottom); 779 780 // add the search GUI 781 setSearchGui(bottom); 782 783 pack(); 784 785 if (log.isDebugEnabled()) { // because size elements take time 786 log.debug("PaneProgFrame \"{}\" constructed for file {}, unconstrained size is {}, constrained to {}", 787 _frameEntryId, _rosterEntry.getFileName(), super.getPreferredSize(), getPreferredSize()); 788 } 789 } 790 791 /** 792 * Front end to DecoderFile.isIncluded() 793 * <ul> 794 * <li>Retrieves "productID" and "model attributes from the "model" element 795 * and "family" attribute from the roster entry. </li> 796 * <li>Then invokes DecoderFile.isIncluded() with the retrieved values.</li> 797 * <li>Deals gracefully with null or missing elements and 798 * attributes.</li> 799 * </ul> 800 * 801 * @param e XML element with possible "include" and "exclude" 802 * attributes to be checked 803 * @param aModelElement "model" element from the Decoder Index, used to get 804 * "model" and "productID". 805 * @param aRosterEntry The current roster entry, used to get "family". 806 * @param extraIncludes additional "include" terms 807 * @param extraExcludes additional "exclude" terms. 808 * @return true if front ended included, else false. 809 */ 810 public static boolean isIncludedFE(Element e, Element aModelElement, RosterEntry aRosterEntry, String extraIncludes, String extraExcludes) { 811 812 String pID; 813 try { 814 pID = aModelElement.getAttribute("productID").getValue(); 815 } catch (Exception ex) { 816 pID = null; 817 } 818 819 String modelName; 820 try { 821 modelName = aModelElement.getAttribute("model").getValue(); 822 } catch (Exception ex) { 823 modelName = null; 824 } 825 826 String familyName; 827 try { 828 familyName = aRosterEntry.getDecoderFamily(); 829 } catch (Exception ex) { 830 familyName = null; 831 } 832 return DecoderFile.isIncluded(e, pID, modelName, familyName, extraIncludes, extraExcludes); 833 } 834 835 protected void pickProgrammerMode(@Nonnull Element programming) { 836 log.debug("pickProgrammerMode starts"); 837 boolean paged = true; 838 boolean directbit = true; 839 boolean directbyte = true; 840 boolean register = true; 841 842 Attribute a; 843 844 // set the programming attributes for DCC 845 if ((a = programming.getAttribute("nodecoder")) != null) { 846 if (a.getValue().equals("yes")) { 847 noDecoder = true; // No decoder in the loco 848 } 849 } 850 if ((a = programming.getAttribute("paged")) != null) { 851 if (a.getValue().equals("no")) { 852 paged = false; 853 } 854 } 855 if ((a = programming.getAttribute("direct")) != null) { 856 if (a.getValue().equals("no")) { 857 directbit = false; 858 directbyte = false; 859 } else if (a.getValue().equals("bitOnly")) { 860 //directbit = true; 861 directbyte = false; 862 } else if (a.getValue().equals("byteOnly")) { 863 directbit = false; 864 //directbyte = true; 865 //} else { // items already have these values 866 //directbit = true; 867 //directbyte = true; 868 } 869 } 870 if ((a = programming.getAttribute("register")) != null) { 871 if (a.getValue().equals("no")) { 872 register = false; 873 } 874 } 875 876 // find an accepted mode to set it to 877 List<ProgrammingMode> modes = mProgrammer.getSupportedModes(); 878 879 if (log.isDebugEnabled()) { 880 log.debug("XML specifies modes: P {} DBi {} Dby {} R {} now {}", paged, directbit, directbyte, register, mProgrammer.getMode()); 881 log.debug("Programmer supports:"); 882 for (ProgrammingMode m : modes) { 883 log.debug(" mode: {} {}", m.getStandardName(), m); 884 } 885 } 886 887 StringBuilder desiredModes = new StringBuilder(); 888 // first try specified modes 889 for (Element el1 : programming.getChildren("mode")) { 890 String name = el1.getText(); 891 if (desiredModes.length() > 0) desiredModes.append(", "); 892 desiredModes.append(name); 893 log.debug(" mode {} was specified", name); 894 for (ProgrammingMode m : modes) { 895 if (name.equals(m.getStandardName())) { 896 log.debug("Programming mode selected: {} ({})", m, m.getStandardName()); 897 mProgrammer.setMode(m); 898 return; 899 } 900 } 901 } 902 903 // go through historical modes 904 if (modes.contains(ProgrammingMode.DIRECTMODE) && directbit && directbyte) { 905 mProgrammer.setMode(ProgrammingMode.DIRECTMODE); 906 log.debug("Set to DIRECTMODE"); 907 } else if (modes.contains(ProgrammingMode.DIRECTBITMODE) && directbit) { 908 mProgrammer.setMode(ProgrammingMode.DIRECTBITMODE); 909 log.debug("Set to DIRECTBITMODE"); 910 } else if (modes.contains(ProgrammingMode.DIRECTBYTEMODE) && directbyte) { 911 mProgrammer.setMode(ProgrammingMode.DIRECTBYTEMODE); 912 log.debug("Set to DIRECTBYTEMODE"); 913 } else if (modes.contains(ProgrammingMode.PAGEMODE) && paged) { 914 mProgrammer.setMode(ProgrammingMode.PAGEMODE); 915 log.debug("Set to PAGEMODE"); 916 } else if (modes.contains(ProgrammingMode.REGISTERMODE) && register) { 917 mProgrammer.setMode(ProgrammingMode.REGISTERMODE); 918 log.debug("Set to REGISTERMODE"); 919 } else if (noDecoder) { 920 log.debug("No decoder"); 921 } else { 922 JmriJOptionPane.showMessageDialog( 923 this, 924 Bundle.getMessage("ErrorCannotSetMode", desiredModes.toString()), 925 Bundle.getMessage("ErrorCannotSetModeTitle"), 926 JmriJOptionPane.ERROR_MESSAGE); 927 log.warn("No acceptable mode found, leave as found"); 928 } 929 } 930 931 /** 932 * Data element holding the 'model' element representing the decoder type. 933 */ 934 Element modelElem = null; 935 936 Element decoderRoot = null; 937 938 protected void loadDecoderFromLoco(RosterEntry r) { 939 // get a DecoderFile from the locomotive xml 940 String decoderModel = r.getDecoderModel(); 941 String decoderFamily = r.getDecoderFamily(); 942 log.debug("selected loco uses decoder {} {}", decoderFamily, decoderModel); 943 944 // locate a decoder like that. 945 List<DecoderFile> l = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, decoderFamily, null, null, null, decoderModel); 946 log.debug("found {} matches", l.size()); 947 if (l.size() == 0) { 948 log.debug("Loco uses {} {} decoder, but no such decoder defined", decoderFamily, decoderModel); 949 // fall back to use just the decoder name, not family 950 l = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, null, null, null, null, decoderModel); 951 if (log.isDebugEnabled()) { 952 log.debug("found {} matches without family key", l.size()); 953 } 954 } 955 if (l.size() > 0) { 956 DecoderFile d = l.get(0); 957 loadDecoderFile(d, r); 958 } else { 959 if (decoderModel.equals("")) { 960 log.debug("blank decoderModel requested, so nothing loaded"); 961 } else { 962 log.warn("no matching \"{}\" decoder found for loco, no decoder info loaded", decoderModel); 963 } 964 } 965 } 966 967 protected void loadDecoderFile(@Nonnull DecoderFile df, @Nonnull RosterEntry re) { 968 if (log.isDebugEnabled()) { 969 log.debug("loadDecoderFile from {} {}", DecoderFile.fileLocation, df.getFileName()); 970 } 971 972 try { 973 decoderRoot = df.rootFromName(DecoderFile.fileLocation + df.getFileName()); 974 } catch (org.jdom2.JDOMException e) { 975 log.error("Exception while parsing decoder XML file: {}", df.getFileName(), e); 976 return; 977 } catch (java.io.IOException e) { 978 log.error("Exception while reading decoder XML file: {}", df.getFileName(), e); 979 return; 980 } 981 // load variables from decoder tree 982 df.getProductID(); 983 df.loadVariableModel(decoderRoot.getChild("decoder"), variableModel); 984 985 // load reset from decoder tree 986 df.loadResetModel(decoderRoot.getChild("decoder"), resetModel); 987 988 // load extra menus from decoder tree 989 df.loadExtraMenuModel(decoderRoot.getChild("decoder"), extraMenuModelList, progStatus, mProgrammer); 990 991 // add extra menus 992 log.trace("add menus {} {}", extraMenuModelList.size(), extraMenuList); 993 for (int i=0; i < extraMenuModelList.size(); i++ ) { 994 String name = extraMenuModelList.get(i).getName(); 995 JMenu menu = new JMenu(name); 996 extraMenuList.add(i, menu); 997 menuBar.add(menu); 998 menu.add(new ExtraMenuAction(name, extraMenuModelList.get(i), this)); 999 menu.setEnabled(false); 1000 } 1001 1002 // add Window and Help menu items (_after_ the extra menus) 1003 addHelp(); 1004 1005 // load function names from family 1006 re.loadFunctions(decoderRoot.getChild("decoder").getChild("family").getChild("functionlabels"), "family"); 1007 1008 // load sound names from family 1009 re.loadSounds(decoderRoot.getChild("decoder").getChild("family").getChild("soundlabels"), "family"); 1010 1011 // get the showEmptyPanes attribute, if yes/no update our state 1012 if (decoderRoot.getAttribute("showEmptyPanes") != null) { 1013 log.debug("Found in decoder showEmptyPanes={}", decoderRoot.getAttribute("showEmptyPanes").getValue()); 1014 decoderShowEmptyPanes = decoderRoot.getAttribute("showEmptyPanes").getValue(); 1015 } else { 1016 decoderShowEmptyPanes = ""; 1017 } 1018 log.debug("decoderShowEmptyPanes={}", decoderShowEmptyPanes); 1019 1020 // get the suppressFunctionLabels attribute, if yes/no update our state 1021 if (decoderRoot.getAttribute("suppressFunctionLabels") != null) { 1022 log.debug("Found in decoder suppressFunctionLabels={}", decoderRoot.getAttribute("suppressFunctionLabels").getValue()); 1023 suppressFunctionLabels = decoderRoot.getAttribute("suppressFunctionLabels").getValue(); 1024 } else { 1025 suppressFunctionLabels = ""; 1026 } 1027 log.debug("suppressFunctionLabels={}", suppressFunctionLabels); 1028 1029 // get the suppressRosterMedia attribute, if yes/no update our state 1030 if (decoderRoot.getAttribute("suppressRosterMedia") != null) { 1031 log.debug("Found in decoder suppressRosterMedia={}", decoderRoot.getAttribute("suppressRosterMedia").getValue()); 1032 suppressRosterMedia = decoderRoot.getAttribute("suppressRosterMedia").getValue(); 1033 } else { 1034 suppressRosterMedia = ""; 1035 } 1036 log.debug("suppressRosterMedia={}", suppressRosterMedia); 1037 1038 // get the allowResetDefaults attribute, if yes/no update our state 1039 if (decoderRoot.getAttribute("allowResetDefaults") != null) { 1040 log.debug("Found in decoder allowResetDefaults={}", decoderRoot.getAttribute("allowResetDefaults").getValue()); 1041 decoderAllowResetDefaults = decoderRoot.getAttribute("allowResetDefaults").getValue(); 1042 } else { 1043 decoderAllowResetDefaults = "yes"; 1044 } 1045 log.debug("decoderAllowResetDefaults={}", decoderAllowResetDefaults); 1046 1047 // save the pointer to the model element 1048 modelElem = df.getModelElement(); 1049 1050 // load function names from model 1051 re.loadFunctions(modelElem.getChild("functionlabels"), "model"); 1052 1053 // load sound names from model 1054 re.loadSounds(modelElem.getChild("soundlabels"), "model"); 1055 1056 // load maxFnNum from model 1057 Attribute a; 1058 if ((a = modelElem.getAttribute("maxFnNum")) != null) { 1059 maxFnNumOld = re.getMaxFnNum(); 1060 maxFnNumNew = a.getValue(); 1061 if (!maxFnNumOld.equals(maxFnNumNew)) { 1062 if (!re.getId().equals(Bundle.getMessage("LabelNewDecoder"))) { 1063 maxFnNumDirty = true; 1064 log.debug("maxFnNum for \"{}\" changed from {} to {}", re.getId(), maxFnNumOld, maxFnNumNew); 1065 String message = java.text.MessageFormat.format( 1066 SymbolicProgBundle.getMessage("StatusMaxFnNumUpdated"), 1067 re.getDecoderFamily(), re.getDecoderModel(), maxFnNumNew); 1068 progStatus.setText(message); 1069 } 1070 re.setMaxFnNum(maxFnNumNew); 1071 } 1072 } 1073 } 1074 1075 protected void loadProgrammerFile(RosterEntry r) { 1076 // Open and parse programmer file 1077 XmlFile pf = new XmlFile() { 1078 }; // XmlFile is abstract 1079 try { 1080 programmerRoot = pf.rootFromName(filename); 1081 1082 // get the showEmptyPanes attribute, if yes/no update our state 1083 if (programmerRoot.getChild("programmer").getAttribute("showEmptyPanes") != null) { 1084 programmerShowEmptyPanes = programmerRoot.getChild("programmer").getAttribute("showEmptyPanes").getValue(); 1085 log.debug("Found in programmer {}", programmerShowEmptyPanes); 1086 } else { 1087 programmerShowEmptyPanes = ""; 1088 } 1089 1090 // get extra any panes from the programmer file 1091 Attribute a; 1092 if ((a = programmerRoot.getChild("programmer").getAttribute("decoderFilePanes")) != null 1093 && a.getValue().equals("yes")) { 1094 if (decoderRoot != null) { 1095 decoderPaneList = decoderRoot.getChildren("pane"); 1096 } 1097 } 1098 1099 // load programmer config from programmer tree 1100 readConfig(programmerRoot, r); 1101 1102 } catch (org.jdom2.JDOMException e) { 1103 log.error("exception parsing programmer file: {}", filename, e); 1104 } catch (java.io.IOException e) { 1105 log.error("exception reading programmer file: {}", filename, e); 1106 } 1107 } 1108 1109 Element programmerRoot = null; 1110 1111 /** 1112 * @return true if decoder needs to be written 1113 */ 1114 protected boolean checkDirtyDecoder() { 1115 if (log.isDebugEnabled()) { 1116 log.debug("Checking decoder dirty status. CV: {} variables:{}", cvModel.decoderDirty(), variableModel.decoderDirty()); 1117 } 1118 return (getModePane() != null && (cvModel.decoderDirty() || variableModel.decoderDirty())); 1119 } 1120 1121 /** 1122 * @return true if file needs to be written 1123 */ 1124 protected boolean checkDirtyFile() { 1125 return (variableModel.fileDirty() || 1126 _rPane.guiChanged(_rosterEntry) || 1127 _flPane.guiChanged(_rosterEntry) || 1128 _rMPane.guiChanged(_rosterEntry) || 1129 (_physicsPane != null && _physicsPane.guiChanged(_rosterEntry) && _rosterEntry.isLocoDataEnabled()) || 1130 maxFnNumDirty); 1131 } 1132 1133 protected void handleDirtyFile() { 1134 } 1135 1136 /** 1137 * Close box has been clicked; handle check for dirty with respect to 1138 * decoder or file, then close. 1139 * 1140 * @param e Not used 1141 */ 1142 @Override 1143 public void windowClosing(java.awt.event.WindowEvent e) { 1144 1145 // Don't want to actually close if we return early 1146 setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); 1147 1148 // check for various types of dirty - first table data not written back 1149 if (log.isDebugEnabled()) { 1150 log.debug("Checking decoder dirty status. CV: {} variables:{}", cvModel.decoderDirty(), variableModel.decoderDirty()); 1151 } 1152 if (!noDecoder && checkDirtyDecoder()) { 1153 if (JmriJOptionPane.showConfirmDialog(this, 1154 Bundle.getMessage("PromptCloseWindowNotWrittenDecoder"), 1155 Bundle.getMessage("PromptChooseOne"), 1156 JmriJOptionPane.OK_CANCEL_OPTION) != JmriJOptionPane.OK_OPTION) { 1157 return; 1158 } 1159 } 1160 if (checkDirtyFile()) { 1161 int option = JmriJOptionPane.showOptionDialog(this, Bundle.getMessage("PromptCloseWindowNotWrittenConfig"), 1162 Bundle.getMessage("PromptChooseOne"), 1163 JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.WARNING_MESSAGE, null, 1164 new String[]{Bundle.getMessage("PromptSaveAndClose"), Bundle.getMessage("PromptClose"), Bundle.getMessage("ButtonCancel")}, 1165 Bundle.getMessage("PromptSaveAndClose")); 1166 if (option == 0) { // array position 0 PromptSaveAndClose 1167 // save requested 1168 if (!storeFile()) { 1169 return; // don't close if failed 1170 } 1171 } else if (option == 2 || option == JmriJOptionPane.CLOSED_OPTION ) { 1172 // cancel requested or Dialog closed 1173 return; // without doing anything 1174 } 1175 } 1176 if(maxFnNumDirty && !maxFnNumOld.equals("")){ 1177 _rosterEntry.setMaxFnNum(maxFnNumOld); 1178 } 1179 // Check for a "<new loco>" roster entry; if found, remove it 1180 List<RosterEntry> l = Roster.getDefault().matchingList(null, null, null, null, null, null, Bundle.getMessage("LabelNewDecoder")); 1181 if (l.size() > 0 && log.isDebugEnabled()) { 1182 log.debug("Removing {} <new loco> entries", l.size()); 1183 } 1184 int x = l.size() + 1; 1185 while (l.size() > 0) { 1186 Roster.getDefault().removeEntry(l.get(0)); 1187 l = Roster.getDefault().matchingList(null, null, null, null, null, null, Bundle.getMessage("LabelNewDecoder")); 1188 x--; 1189 if (x == 0) { 1190 log.error("We have tried to remove all the entries, however an error has occurred which has resulted in the entries not being deleted correctly"); 1191 l = new ArrayList<>(); 1192 } 1193 } 1194 1195 // OK, continue close 1196 setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); 1197 1198 // deregister shutdown hooks 1199 jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).deregister(decoderDirtyTask); 1200 decoderDirtyTask = null; 1201 jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).deregister(fileDirtyTask); 1202 fileDirtyTask = null; 1203 1204 // do the close itself 1205 super.windowClosing(e); 1206 } 1207 1208 void readConfig(Element root, RosterEntry r) { 1209 // check for "programmer" element at start 1210 Element base; 1211 if ((base = root.getChild("programmer")) == null) { 1212 log.error("xml file top element is not programmer"); 1213 return; 1214 } 1215 1216 // add the Info tab 1217 _rPane = new RosterEntryPane(r); 1218 _rPane.setMaximumSize(_rPane.getPreferredSize()); 1219 if (root.getChild("programmer").getAttribute("showRosterPane") != null) { 1220 if (root.getChild("programmer").getAttribute("showRosterPane").getValue().equals("no")) { 1221 makeInfoPane(r); 1222 } else { 1223 final int i = tabPane.getTabCount(); 1224 tabPane.addTab(Bundle.getMessage("ROSTER ENTRY"), makeStandinComponent()); 1225 threadCount.incrementAndGet(); 1226 new javax.swing.SwingWorker<JComponent, Object>(){ 1227 @Override 1228 public JComponent doInBackground() { 1229 return makeInfoPane(r); 1230 } 1231 @Override 1232 protected void done() { 1233 try { 1234 var result = get(); 1235 tabPane.setComponentAt(i, result); 1236 } catch (InterruptedException | java.util.concurrent.ExecutionException e) { 1237 log.error("Exception",e); 1238 } 1239 threadCount.decrementAndGet(); 1240 } 1241 }.execute(); 1242 } 1243 } else { 1244 final int i = tabPane.getTabCount(); 1245 tabPane.addTab(Bundle.getMessage("ROSTER ENTRY"), makeStandinComponent()); 1246 1247 threadCount.incrementAndGet(); 1248 new javax.swing.SwingWorker<JComponent, Object>(){ 1249 @Override 1250 public JComponent doInBackground() { 1251 return makeInfoPane(r); 1252 } 1253 @Override 1254 protected void done() { 1255 try { 1256 var result = get(); 1257 tabPane.setComponentAt(i, result); 1258 } catch (InterruptedException | java.util.concurrent.ExecutionException e) { 1259 log.error("Exception",e); 1260 } 1261 threadCount.decrementAndGet(); 1262 } 1263 }.execute(); 1264 } 1265 1266 // add the Physics tab (locomotive-level Physics parameters) 1267 // Follow the roster pane visibility setting 1268 if (_rosterEntry.isLocoDataEnabled()) { 1269 if (root.getChild("programmer").getAttribute("showRosterPane") != null && 1270 root.getChild("programmer").getAttribute("showRosterPane").getValue().equals("no")) { 1271 // create it, just don't make it visible 1272 _physicsPane = new RosterPhysicsPane(r); 1273 makePhysicsPane(r); 1274 } else { 1275 _physicsPane = new RosterPhysicsPane(r); 1276 final int iPhys = tabPane.getTabCount(); 1277 tabPane.addTab(RosterPhysicsPane.getTabTitle(), makeStandinComponent()); 1278 threadCount.incrementAndGet(); 1279 new javax.swing.SwingWorker<JComponent, Object>() { 1280 @Override 1281 public JComponent doInBackground() { 1282 return makePhysicsPane(r); 1283 } 1284 1285 @Override 1286 protected void done() { 1287 try { 1288 var result = get(); 1289 tabPane.setComponentAt(iPhys, result); 1290 } catch ( 1291 InterruptedException | 1292 java.util.concurrent.ExecutionException e) { 1293 log.error("Exception", e); 1294 } 1295 threadCount.decrementAndGet(); 1296 } 1297 }.execute(); 1298 } 1299 } 1300 1301 // add the Function Label tab 1302 if (root.getChild("programmer").getAttribute("showFnLanelPane").getValue().equals("yes") 1303 && !suppressFunctionLabels.equals("yes") 1304 ) { 1305 1306 final int i = tabPane.getTabCount(); 1307 tabPane.addTab(Bundle.getMessage("FUNCTION LABELS"), makeStandinComponent()); 1308 1309 threadCount.incrementAndGet(); 1310 new javax.swing.SwingWorker<JComponent, Object>(){ 1311 @Override 1312 public JComponent doInBackground() { 1313 return makeFunctionLabelPane(r); 1314 } 1315 @Override 1316 protected void done() { 1317 try { 1318 var result = get(); 1319 tabPane.setComponentAt(i, result); 1320 } catch (InterruptedException | java.util.concurrent.ExecutionException e) { 1321 log.error("Exception",e); 1322 } 1323 threadCount.decrementAndGet(); 1324 } 1325 }.execute(); 1326 1327 } else { 1328 // make it, just don't make it visible 1329 makeFunctionLabelPane(r); 1330 } 1331 1332 // add the Media tab 1333 if (root.getChild("programmer").getAttribute("showRosterMediaPane").getValue().equals("yes") 1334 && !suppressRosterMedia.equals("yes") 1335 ) { 1336 1337 final int i = tabPane.getTabCount(); 1338 tabPane.addTab(Bundle.getMessage("ROSTER MEDIA"), makeStandinComponent()); 1339 1340 threadCount.incrementAndGet(); 1341 new javax.swing.SwingWorker<JComponent, Object>(){ 1342 @Override 1343 public JComponent doInBackground() { 1344 return makeMediaPane(r); 1345 } 1346 @Override 1347 protected void done() { 1348 try { 1349 var result = get(); 1350 tabPane.setComponentAt(i, result); 1351 } catch (InterruptedException | java.util.concurrent.ExecutionException e) { 1352 log.error("Exception",e); 1353 } 1354 threadCount.decrementAndGet(); 1355 } 1356 }.execute(); 1357 1358 } else { 1359 // create it, just don't make it visible 1360 makeMediaPane(r); 1361 } 1362 1363 // add the comment tab 1364 JPanel commentTab = new JPanel(); 1365 var comment = new JTextArea(_rPane.getCommentDocument()); 1366 JScrollPane commentScroller = new JScrollPane(comment, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); 1367 commentTab.add(commentScroller); 1368 commentTab.setLayout(new BoxLayout(commentTab, BoxLayout.Y_AXIS)); 1369 tabPane.addTab(Bundle.getMessage("COMMENT PANE"), commentTab); 1370 1371 // for all "pane" elements in the programmer 1372 List<Element> progPaneList = base.getChildren("pane"); 1373 log.debug("will process {} pane definitions", progPaneList.size()); 1374 1375 for (Element temp : progPaneList) { 1376 // load each programmer pane 1377 List<Element> pnames = temp.getChildren("name"); 1378 boolean isProgPane = true; 1379 if ((pnames.size() > 0) && (decoderPaneList != null) && (decoderPaneList.size() > 0)) { 1380 String namePrimary = (pnames.get(0)).getValue(); // get non-localised name 1381 1382 // check if there is a same-name pane in decoder file 1383 // start at end to prevent concurrentmodification exception on remove 1384 for (int j = decoderPaneList.size() - 1; j >= 0; j--) { 1385 List<Element> dnames = decoderPaneList.get(j).getChildren("name"); 1386 if (dnames.size() > 0) { 1387 String namePrimaryDecoder = (dnames.get(0)).getValue(); // get non-localised name 1388 if (namePrimary.equals(namePrimaryDecoder)) { 1389 // replace programmer pane with same-name decoder pane 1390 temp = decoderPaneList.get(j); 1391 decoderPaneList.remove(j); // safe, not suspicious as we work end - front 1392 isProgPane = false; 1393 } 1394 } 1395 } 1396 } 1397 String name = jmri.util.jdom.LocaleSelector.getAttribute(temp, "name"); 1398 1399 // handle include/exclude 1400 if (isIncludedFE(temp, modelElem, _rosterEntry, "", "")) { 1401 newPane(name, temp, modelElem, false, isProgPane); // don't force showing if empty 1402 log.debug("readConfig - pane {} added", name); // these are also in RosterPrint 1403 } 1404 } 1405 log.trace("done processing panes"); 1406 } 1407 1408 /** 1409 * Make temporary contents for a pane while loading 1410 */ 1411 protected Component makeStandinComponent() { 1412 var retval = new JPanel(){ 1413 @Override 1414 public Dimension getPreferredSize() { 1415 // return a nominal size for the tabbed panes until manually resized 1416 return new java.awt.Dimension(900, 600); 1417 } 1418 }; 1419 retval.add(new JLabel(Bundle.getMessage("STANDIN MESSAGE"))); 1420 return retval; 1421 } 1422 1423 1424 /** 1425 * Reset all CV values to defaults stored earlier. 1426 * <p> 1427 * This will in turn update the variables. 1428 */ 1429 protected void resetToDefaults() { 1430 int n = defaultCvValues.length; 1431 for (int i = 0; i < n; i++) { 1432 CvValue cv = cvModel.getCvByNumber(defaultCvNumbers[i]); 1433 if (cv == null) { 1434 log.warn("Trying to set default in CV {} but didn't find the CV object", defaultCvNumbers[i]); 1435 } else { 1436 cv.setValue(defaultCvValues[i]); 1437 } 1438 } 1439 } 1440 1441 int[] defaultCvValues = null; 1442 String[] defaultCvNumbers = null; 1443 1444 /** 1445 * Save all CV values. 1446 * <p> 1447 * These stored values are used by {link #resetToDefaults()} 1448 */ 1449 protected void saveDefaults() { 1450 int n = cvModel.getRowCount(); 1451 defaultCvValues = new int[n]; 1452 defaultCvNumbers = new String[n]; 1453 1454 for (int i = 0; i < n; i++) { 1455 CvValue cv = cvModel.getCvByRow(i); 1456 defaultCvValues[i] = cv.getValue(); 1457 defaultCvNumbers[i] = cv.number(); 1458 } 1459 } 1460 1461 @InvokeOnAnyThread // transfers some operations to GUI thread 1462 protected JPanel makeInfoPane(RosterEntry r) { 1463 // create the identification pane (not configured by programmer file now; maybe later?) 1464 1465 JPanel outer = new JPanel(); 1466 ThreadingUtil.runOnGUI(()->{ 1467 outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS)); 1468 JPanel body = new JPanel(); 1469 body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS)); 1470 JScrollPane scrollPane = new JScrollPane(body); 1471 1472 // add roster info 1473 body.add(_rPane); 1474 1475 // add the store button 1476 JButton store = new JButton(Bundle.getMessage("ButtonSave")); 1477 store.setAlignmentX(JLabel.CENTER_ALIGNMENT); 1478 store.addActionListener(e -> storeFile()); 1479 1480 // add the reset button 1481 JButton reset = new JButton(Bundle.getMessage("ButtonResetDefaults")); 1482 reset.setAlignmentX(JLabel.CENTER_ALIGNMENT); 1483 if (decoderAllowResetDefaults.equals("no")) { 1484 reset.setEnabled(false); 1485 reset.setToolTipText(Bundle.getMessage("TipButtonResetDefaultsDisabled")); 1486 } else { 1487 reset.setToolTipText(Bundle.getMessage("TipButtonResetDefaults")); 1488 reset.addActionListener(e -> resetToDefaults()); 1489 } 1490 1491 int sizeX = Math.max(reset.getPreferredSize().width, store.getPreferredSize().width); 1492 int sizeY = Math.max(reset.getPreferredSize().height, store.getPreferredSize().height); 1493 store.setPreferredSize(new Dimension(sizeX, sizeY)); 1494 reset.setPreferredSize(new Dimension(sizeX, sizeY)); 1495 1496 store.setToolTipText(_rosterEntry.getFileName()); 1497 1498 JPanel buttons = new JPanel(); 1499 buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS)); 1500 1501 buttons.add(store); 1502 buttons.add(reset); 1503 1504 body.add(buttons); 1505 outer.add(scrollPane); 1506 1507 // arrange for the dcc address to be updated 1508 java.beans.PropertyChangeListener dccNews = e -> updateDccAddress(); 1509 primaryAddr = variableModel.findVar("Short Address"); 1510 if (primaryAddr == null) { 1511 log.debug("DCC Address monitor didn't find a Short Address variable"); 1512 } else { 1513 primaryAddr.addPropertyChangeListener(dccNews); 1514 } 1515 extendAddr = variableModel.findVar("Long Address"); 1516 if (extendAddr == null) { 1517 log.debug("DCC Address monitor didn't find an Long Address variable"); 1518 } else { 1519 extendAddr.addPropertyChangeListener(dccNews); 1520 } 1521 addMode = (EnumVariableValue) variableModel.findVar("Address Format"); 1522 if (addMode == null) { 1523 log.debug("DCC Address monitor didn't find an Address Format variable"); 1524 } else { 1525 addMode.addPropertyChangeListener(dccNews); 1526 } 1527 1528 // get right address to start 1529 updateDccAddress(); 1530 }); 1531 1532 return outer; 1533 } 1534 1535 1536 @InvokeOnAnyThread // transfers some operations to GUI thread 1537 protected JPanel makePhysicsPane(RosterEntry r) { 1538 // create the physics pane wrapper (not configured by programmer file) 1539 if (!r.isLocoDataEnabled()) { 1540 return null; 1541 } 1542 1543 JPanel outer = new JPanel(); 1544 ThreadingUtil.runOnGUI(() -> { 1545 outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS)); 1546 JPanel body = new JPanel(); 1547 body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS)); 1548 JScrollPane scrollPane = new JScrollPane(body); 1549 1550 // add physics info 1551 if (_physicsPane != null) { 1552 body.add(_physicsPane); 1553 } 1554 1555 // add the store button 1556 JButton store = new JButton(Bundle.getMessage("ButtonSave")); 1557 store.setAlignmentX(JLabel.CENTER_ALIGNMENT); 1558 store.addActionListener(e -> storeFile()); 1559 store.setToolTipText(_rosterEntry.getFileName()); 1560 JPanel buttons = new JPanel(); 1561 buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS)); 1562 buttons.add(store); 1563 body.add(buttons); 1564 1565 outer.add(scrollPane); 1566 }); 1567 return outer; 1568 } 1569 1570 @InvokeOnAnyThread // transfers some operations to GUI thread 1571 protected JPanel makeFunctionLabelPane(RosterEntry r) { 1572 // create the identification pane (not configured by programmer file now; maybe later?) 1573 1574 JPanel outer = new JPanel(); 1575 1576 ThreadingUtil.runOnGUI(()->{ 1577 outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS)); 1578 JPanel body = new JPanel(); 1579 body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS)); 1580 JScrollPane scrollPane = new JScrollPane(body); 1581 1582 // add tab description 1583 JLabel title = new JLabel(Bundle.getMessage("UseThisTabCustomize")); 1584 title.setAlignmentX(JLabel.CENTER_ALIGNMENT); 1585 body.add(title); 1586 body.add(new JLabel(" ")); // some padding 1587 1588 // add roster info 1589 _flPane = new FunctionLabelPane(r); 1590 //_flPane.setMaximumSize(_flPane.getPreferredSize()); 1591 body.add(_flPane); 1592 1593 // add the store button 1594 JButton store = new JButton(Bundle.getMessage("ButtonSave")); 1595 store.setAlignmentX(JLabel.CENTER_ALIGNMENT); 1596 store.addActionListener(e -> storeFile()); 1597 1598 store.setToolTipText(_rosterEntry.getFileName()); 1599 1600 JPanel buttons = new JPanel(); 1601 buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS)); 1602 1603 buttons.add(store); 1604 1605 body.add(buttons); 1606 outer.add(scrollPane); 1607 }); 1608 return outer; 1609 } 1610 1611 @InvokeOnAnyThread // transfers some operations to GUI thread 1612 protected JPanel makeMediaPane(RosterEntry r) { 1613 // create the identification pane (not configured by programmer file now; maybe later?) 1614 JPanel outer = new JPanel(); 1615 1616 ThreadingUtil.runOnGUI(()->{ 1617 outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS)); 1618 JPanel body = new JPanel(); 1619 body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS)); 1620 JScrollPane scrollPane = new JScrollPane(body); 1621 1622 // add tab description 1623 JLabel title = new JLabel(Bundle.getMessage("UseThisTabMedia")); 1624 title.setAlignmentX(JLabel.CENTER_ALIGNMENT); 1625 body.add(title); 1626 body.add(new JLabel(" ")); // some padding 1627 1628 // add roster info 1629 _rMPane = new RosterMediaPane(r); 1630 _rMPane.setMaximumSize(_rMPane.getPreferredSize()); 1631 body.add(_rMPane); 1632 1633 // add the store button 1634 JButton store = new JButton(Bundle.getMessage("ButtonSave")); 1635 store.setAlignmentX(JLabel.CENTER_ALIGNMENT); 1636 store.addActionListener(e -> storeFile()); 1637 1638 JPanel buttons = new JPanel(); 1639 buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS)); 1640 1641 buttons.add(store); 1642 1643 body.add(buttons); 1644 outer.add(scrollPane); 1645 }); 1646 return outer; 1647 } 1648 1649 // hold refs to variables to check dccAddress 1650 VariableValue primaryAddr = null; 1651 VariableValue extendAddr = null; 1652 EnumVariableValue addMode = null; 1653 1654 boolean longMode = false; 1655 String newAddr = null; 1656 1657 void updateDccAddress() { 1658 1659 if (log.isDebugEnabled()) { 1660 log.debug("updateDccAddress: short {} long {} mode {}", primaryAddr == null ? "<null>" : primaryAddr.getValueString(), extendAddr == null ? "<null>" : extendAddr.getValueString(), addMode == null ? "<null>" : addMode.getValueString()); 1661 } 1662 1663 new DccAddressVarHandler(primaryAddr, extendAddr, addMode) { 1664 @Override 1665 protected void doPrimary() { 1666 // short address mode 1667 longMode = false; 1668 if (primaryAddr != null && !primaryAddr.getValueString().equals("")) { 1669 newAddr = primaryAddr.getValueString(); 1670 } 1671 } 1672 1673 @Override 1674 protected void doExtended() { 1675 // long address 1676 if (extendAddr != null && !extendAddr.getValueString().equals("")) { 1677 longMode = true; 1678 newAddr = extendAddr.getValueString(); 1679 } 1680 } 1681 }; 1682 // update if needed 1683 if (newAddr != null) { 1684 // store DCC address, type 1685 _rPane.setDccAddress(newAddr); 1686 _rPane.setDccAddressLong(longMode); 1687 } 1688 } 1689 1690 public void newPane(String name, Element pane, Element modelElem, boolean enableEmpty, boolean programmerPane) { 1691 if (log.isDebugEnabled()) { 1692 log.debug("newPane with enableEmpty {} showEmptyPanes {}", enableEmpty, isShowingEmptyPanes()); 1693 } 1694 1695 // create place-keeper tab 1696 ThreadingUtil.runOnGUI(() -> { 1697 tabPane.addTab(name, makeStandinComponent()); 1698 }); 1699 1700 // create a panel to hold columns via separate thread 1701 final var parent = this; 1702 threadCount.incrementAndGet(); 1703 new javax.swing.SwingWorker<PaneProgPane, Object>(){ 1704 @Override 1705 public PaneProgPane doInBackground() { 1706 return new PaneProgPane(parent, name, pane, cvModel, variableModel, modelElem, _rosterEntry, programmerPane); 1707 } 1708 @Override 1709 protected void done() { 1710 try { 1711 var p = get(); 1712 p.setOpaque(true); 1713 if (noDecoder) { 1714 p.setNoDecoder(); 1715 cvModel.setNoDecoder(); 1716 } 1717 // how to handle the tab depends on whether it has contents and option setting 1718 int index; 1719 if (enableEmpty || !p.cvList.isEmpty() || !p.varList.isEmpty()) { 1720 // Was there a race condition here with qualified panes? 1721 // QualifiedVarTest attempts to invoke that, but haven't it with the following code 1722 index = tabPane.indexOfTab(name); 1723 tabPane.setComponentAt(tabPane.indexOfTab(name), p); // always add if not empty 1724 tabPane.setToolTipTextAt(tabPane.indexOfTab(name), p.getToolTipText()); 1725 } else if (isShowingEmptyPanes()) { 1726 // here empty, but showing anyway as disabled 1727 index = tabPane.indexOfTab(name); 1728 tabPane.setComponentAt(tabPane.indexOfTab(name), p); 1729 tabPane.setToolTipTextAt(tabPane.indexOfTab(name), 1730 Bundle.getMessage("TipTabEmptyNoCategory")); 1731 tabPane.setEnabledAt(tabPane.indexOfTab(name), true); // need to enable the pane so user can see message 1732 } else { 1733 // here not showing tab at all 1734 index = -1; 1735 log.trace("deleted {} tab here", name); 1736 tabPane.removeTabAt(tabPane.indexOfTab(name)); 1737 } 1738 1739 // remember it for programming 1740 paneList.add(p); 1741 1742 // if visible, set qualifications 1743 if (index >= 0) { 1744 processModifierElements(pane, p, variableModel, tabPane, name); 1745 } 1746 threadCount.decrementAndGet(); 1747 } catch (InterruptedException | java.util.concurrent.ExecutionException e) { 1748 log.error("Exception",e); 1749 } 1750 } 1751 }.execute(); 1752 1753 } 1754 1755 /** 1756 * If there are any modifier elements, process them. 1757 * 1758 * @param e Process the contents of this element 1759 * @param pane Destination of any visible items 1760 * @param model Used to locate any needed variables 1761 * @param tabPane For overall GUI navigation 1762 * @param name Which pane in the overall window 1763 */ 1764 protected void processModifierElements(Element e, final PaneProgPane pane, VariableTableModel model, final JTabbedPane tabPane, final String name) { 1765 QualifierAdder qa = new QualifierAdder() { 1766 @Override 1767 protected Qualifier createQualifier(VariableValue var, String relation, String value) { 1768 return new PaneQualifier(pane, var, Integer.parseInt(value), relation, tabPane, name); 1769 } 1770 1771 @Override 1772 protected void addListener(java.beans.PropertyChangeListener qc) { 1773 pane.addPropertyChangeListener(qc); 1774 } 1775 }; 1776 1777 qa.processModifierElements(e, model); 1778 } 1779 1780 @Override 1781 public BusyGlassPane getBusyGlassPane() { 1782 return glassPane; 1783 } 1784 1785 /** 1786 * Create a BusyGlassPane transparent layer over the panel blocking any 1787 * other interaction, excluding a supplied button. 1788 * 1789 * @param activeButton a button to put on top of the pane 1790 */ 1791 @Override 1792 public void prepGlassPane(AbstractButton activeButton) { 1793 List<Rectangle> rectangles = new ArrayList<>(); 1794 1795 if (glassPane != null) { 1796 glassPane.dispose(); 1797 } 1798 activeComponents.clear(); 1799 activeComponents.add(activeButton); 1800 if (activeButton == readChangesButton || activeButton == readAllButton 1801 || activeButton == writeChangesButton || activeButton == writeAllButton) { 1802 if (activeButton == readChangesButton) { 1803 for (JPanel jPanel : paneList) { 1804 assert jPanel instanceof PaneProgPane; 1805 activeComponents.add(((PaneProgPane) jPanel).readChangesButton); 1806 } 1807 } else if (activeButton == readAllButton) { 1808 for (JPanel jPanel : paneList) { 1809 assert jPanel instanceof PaneProgPane; 1810 activeComponents.add(((PaneProgPane) jPanel).readAllButton); 1811 } 1812 } else if (activeButton == writeChangesButton) { 1813 for (JPanel jPanel : paneList) { 1814 assert jPanel instanceof PaneProgPane; 1815 activeComponents.add(((PaneProgPane) jPanel).writeChangesButton); 1816 } 1817 } else { // (activeButton == writeAllButton) { 1818 for (JPanel jPanel : paneList) { 1819 assert jPanel instanceof PaneProgPane; 1820 activeComponents.add(((PaneProgPane) jPanel).writeAllButton); 1821 } 1822 } 1823 1824 for (int i = 0; i < tabPane.getTabCount(); i++) { 1825 rectangles.add(tabPane.getUI().getTabBounds(tabPane, i)); 1826 } 1827 } 1828 glassPane = new BusyGlassPane(activeComponents, rectangles, this.getContentPane(), this); 1829 this.setGlassPane(glassPane); 1830 } 1831 1832 @Override 1833 public void paneFinished() { 1834 log.debug("paneFinished with isBusy={}", isBusy()); 1835 if (!isBusy()) { 1836 if (glassPane != null) { 1837 glassPane.setVisible(false); 1838 glassPane.dispose(); 1839 glassPane = null; 1840 } 1841 setCursor(Cursor.getDefaultCursor()); 1842 enableButtons(true); 1843 } 1844 } 1845 1846 /** 1847 * Enable the read/write buttons. 1848 * <p> 1849 * In addition, if a programming mode pane is present, its "set" button is 1850 * enabled. 1851 * 1852 * @param stat Are reads possible? If false, so not enable the read buttons. 1853 */ 1854 @Override 1855 public void enableButtons(boolean stat) { 1856 log.debug("enableButtons({})", stat); 1857 if (noDecoder) { 1858 // If we don't have a decoder, no read or write is possible 1859 stat = false; 1860 } 1861 if (stat) { 1862 enableReadButtons(); 1863 } else { 1864 readChangesButton.setEnabled(false); 1865 readAllButton.setEnabled(false); 1866 } 1867 writeChangesButton.setEnabled(stat); 1868 writeAllButton.setEnabled(stat); 1869 1870 var tempModePane = getModePane(); 1871 if (tempModePane != null) { 1872 tempModePane.setEnabled(stat); 1873 } 1874 } 1875 1876 boolean justChanges; 1877 1878 @Override 1879 public boolean isBusy() { 1880 return _busy; 1881 } 1882 private boolean _busy = false; 1883 1884 private void setBusy(boolean stat) { 1885 log.debug("setBusy({})", stat); 1886 _busy = stat; 1887 1888 for (JPanel jPanel : paneList) { 1889 assert jPanel instanceof PaneProgPane; 1890 ((PaneProgPane) jPanel).enableButtons(!stat); 1891 } 1892 if (!stat) { 1893 paneFinished(); 1894 } 1895 } 1896 1897 /** 1898 * Invoked by "Read Changes" button, this sets in motion a continuing 1899 * sequence of "read changes" operations on the panes. 1900 * <p> 1901 * Each invocation of this method reads one pane; completion of that request 1902 * will cause it to happen again, reading the next pane, until there's 1903 * nothing left to read. 1904 * 1905 * @return true if a read has been started, false if the operation is 1906 * complete. 1907 */ 1908 public boolean readChanges() { 1909 log.debug("readChanges starts"); 1910 justChanges = true; 1911 for (JPanel jPanel : paneList) { 1912 assert jPanel instanceof PaneProgPane; 1913 ((PaneProgPane) jPanel).setToRead(justChanges, true); 1914 } 1915 setBusy(true); 1916 enableButtons(false); 1917 readChangesButton.setEnabled(true); 1918 glassPane.setVisible(true); 1919 paneListIndex = 0; 1920 // start operation 1921 return doRead(); 1922 } 1923 1924 /** 1925 * Invoked by the "Read All" button, this sets in motion a continuing 1926 * sequence of "read all" operations on the panes. 1927 * <p> 1928 * Each invocation of this method reads one pane; completion of that request 1929 * will cause it to happen again, reading the next pane, until there's 1930 * nothing left to read. 1931 * 1932 * @return true if a read has been started, false if the operation is 1933 * complete. 1934 */ 1935 public boolean readAll() { 1936 log.debug("readAll starts"); 1937 justChanges = false; 1938 for (JPanel jPanel : paneList) { 1939 assert jPanel instanceof PaneProgPane; 1940 ((PaneProgPane) jPanel).setToRead(justChanges, true); 1941 } 1942 setBusy(true); 1943 enableButtons(false); 1944 readAllButton.setEnabled(true); 1945 glassPane.setVisible(true); 1946 paneListIndex = 0; 1947 // start operation 1948 return doRead(); 1949 } 1950 1951 boolean doRead() { 1952 _read = true; 1953 while (paneListIndex < paneList.size()) { 1954 log.debug("doRead on {}", paneListIndex); 1955 _programmingPane = (PaneProgPane) paneList.get(paneListIndex); 1956 // some programming operations are instant, so need to have listener registered at readPaneAll 1957 _programmingPane.addPropertyChangeListener(this); 1958 boolean running; 1959 if (justChanges) { 1960 running = _programmingPane.readPaneChanges(); 1961 } else { 1962 running = _programmingPane.readPaneAll(); 1963 } 1964 1965 paneListIndex++; 1966 1967 if (running) { 1968 // operation in progress, stop loop until called back 1969 log.debug("doRead expecting callback from readPane {}", paneListIndex); 1970 return true; 1971 } else { 1972 _programmingPane.removePropertyChangeListener(this); 1973 } 1974 } 1975 // nothing to program, end politely 1976 _programmingPane = null; 1977 enableButtons(true); 1978 setBusy(false); 1979 readChangesButton.setSelected(false); 1980 readAllButton.setSelected(false); 1981 log.debug("doRead found nothing to do"); 1982 return false; 1983 } 1984 1985 /** 1986 * Invoked by "Write All" button, this sets in motion a continuing sequence 1987 * of "write all" operations on each pane. Each invocation of this method 1988 * writes one pane; completion of that request will cause it to happen 1989 * again, writing the next pane, until there's nothing left to write. 1990 * 1991 * @return true if a write has been started, false if the operation is 1992 * complete. 1993 */ 1994 public boolean writeAll() { 1995 log.debug("writeAll starts"); 1996 justChanges = false; 1997 for (JPanel jPanel : paneList) { 1998 assert jPanel instanceof PaneProgPane; 1999 ((PaneProgPane) jPanel).setToWrite(justChanges, true); 2000 } 2001 setBusy(true); 2002 enableButtons(false); 2003 writeAllButton.setEnabled(true); 2004 glassPane.setVisible(true); 2005 paneListIndex = 0; 2006 return doWrite(); 2007 } 2008 2009 /** 2010 * Invoked by "Write Changes" button, this sets in motion a continuing 2011 * sequence of "write changes" operations on each pane. 2012 * <p> 2013 * Each invocation of this method writes one pane; completion of that 2014 * request will cause it to happen again, writing the next pane, until 2015 * there's nothing left to write. 2016 * 2017 * @return true if a write has been started, false if the operation is 2018 * complete 2019 */ 2020 public boolean writeChanges() { 2021 log.debug("writeChanges starts"); 2022 justChanges = true; 2023 for (JPanel jPanel : paneList) { 2024 assert jPanel instanceof PaneProgPane; 2025 ((PaneProgPane) jPanel).setToWrite(justChanges, true); 2026 } 2027 setBusy(true); 2028 enableButtons(false); 2029 writeChangesButton.setEnabled(true); 2030 glassPane.setVisible(true); 2031 paneListIndex = 0; 2032 return doWrite(); 2033 } 2034 2035 boolean doWrite() { 2036 _read = false; 2037 while (paneListIndex < paneList.size()) { 2038 log.debug("doWrite starts on {}", paneListIndex); 2039 _programmingPane = (PaneProgPane) paneList.get(paneListIndex); 2040 // some programming operations are instant, so need to have listener registered at readPane 2041 _programmingPane.addPropertyChangeListener(this); 2042 boolean running; 2043 if (justChanges) { 2044 running = _programmingPane.writePaneChanges(); 2045 } else { 2046 running = _programmingPane.writePaneAll(); 2047 } 2048 2049 paneListIndex++; 2050 2051 if (running) { 2052 // operation in progress, stop loop until called back 2053 log.debug("doWrite expecting callback from writePane {}", paneListIndex); 2054 return true; 2055 } else { 2056 _programmingPane.removePropertyChangeListener(this); 2057 } 2058 } 2059 // nothing to program, end politely 2060 _programmingPane = null; 2061 enableButtons(true); 2062 setBusy(false); 2063 writeChangesButton.setSelected(false); 2064 writeAllButton.setSelected(false); 2065 log.debug("doWrite found nothing to do"); 2066 return false; 2067 } 2068 2069 /** 2070 * Prepare a roster entry to be printed, and display a selection list. 2071 * 2072 * @see jmri.jmrit.roster.PrintRosterEntry#doPrintPanes(boolean) 2073 * @param preview true if output should go to a Preview pane on screen, 2074 * false to output to a printer (dialog) 2075 */ 2076 public void printPanes(final boolean preview) { 2077 PrintRosterEntry pre = new PrintRosterEntry(_rosterEntry, paneList, _flPane, _rMPane, this); 2078 pre.printPanes(preview); 2079 } 2080 2081 boolean _read = true; 2082 PaneProgPane _programmingPane = null; 2083 2084 /** 2085 * Get notification of a variable property change in the pane, specifically 2086 * "busy" going to false at the end of a programming operation. 2087 * 2088 * @param e Event, used to find source 2089 */ 2090 @Override 2091 public void propertyChange(java.beans.PropertyChangeEvent e) { 2092 // check for the right event 2093 if (_programmingPane == null) { 2094 log.warn("unexpected propertyChange: {}", e); 2095 return; 2096 } else if (log.isDebugEnabled()) { 2097 log.debug("property changed: {} new value: {}", e.getPropertyName(), e.getNewValue()); 2098 } 2099 log.debug("check valid: {} {} {}", e.getSource() == _programmingPane, !e.getPropertyName().equals("Busy"), e.getNewValue().equals(Boolean.FALSE)); 2100 if (e.getSource() == _programmingPane 2101 && e.getPropertyName().equals("Busy") 2102 && e.getNewValue().equals(Boolean.FALSE)) { 2103 2104 log.debug("end of a programming pane operation, remove"); 2105 // remove existing listener 2106 _programmingPane.removePropertyChangeListener(this); 2107 _programmingPane = null; 2108 // restart the operation 2109 if (_read && readChangesButton.isSelected()) { 2110 log.debug("restart readChanges"); 2111 doRead(); 2112 } else if (_read && readAllButton.isSelected()) { 2113 log.debug("restart readAll"); 2114 doRead(); 2115 } else if (writeChangesButton.isSelected()) { 2116 log.debug("restart writeChanges"); 2117 doWrite(); 2118 } else if (writeAllButton.isSelected()) { 2119 log.debug("restart writeAll"); 2120 doWrite(); 2121 } else { 2122 log.debug("read/write end because button is lifted"); 2123 setBusy(false); 2124 } 2125 } 2126 } 2127 2128 /** 2129 * Store the locomotives information in the roster (and a RosterEntry file). 2130 * 2131 * @return false if store failed 2132 */ 2133 @InvokeOnGuiThread 2134 public boolean storeFile() { 2135 log.debug("storeFile starts"); 2136 2137 if (_rPane.checkDuplicate()) { 2138 JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorDuplicateID")); 2139 return false; 2140 } 2141 2142 // reload the RosterEntry 2143 updateDccAddress(); 2144 _rPane.update(_rosterEntry); 2145 _flPane.update(_rosterEntry); 2146 _rMPane.update(_rosterEntry); 2147 if (_physicsPane != null && _rosterEntry.isLocoDataEnabled()) { 2148 _physicsPane.update(_rosterEntry); 2149 } 2150 2151 // id has to be set! 2152 if (_rosterEntry.getId().equals("") || _rosterEntry.getId().equals(Bundle.getMessage("LabelNewDecoder"))) { 2153 log.debug("storeFile without a filename; issued dialog"); 2154 JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("PromptFillInID")); 2155 return false; 2156 } 2157 2158 // if there isn't a filename, store using the id 2159 _rosterEntry.ensureFilenameExists(); 2160 String filename = _rosterEntry.getFileName(); 2161 2162 // do actual file writes in a separate thread, wait for success 2163 threadCount.incrementAndGet(); 2164 new javax.swing.SwingWorker<Object, Object>(){ 2165 @Override 2166 public Object doInBackground() { 2167 actualFileWrites(); 2168 return null; 2169 } 2170 @Override 2171 protected void done() { 2172 // show OK status 2173 progStatus.setText(java.text.MessageFormat.format( 2174 Bundle.getMessage("StateSaveOK"), filename)); 2175 threadCount.decrementAndGet(); 2176 } 2177 }.execute(); 2178 2179 // mark this as a success 2180 variableModel.setFileDirty(false); 2181 maxFnNumDirty = false; 2182 2183 // save date changed, update 2184 _rPane.updateGUI(_rosterEntry); 2185 if (_physicsPane != null && _rosterEntry.isLocoDataEnabled()) { 2186 _physicsPane.updateGUI(_rosterEntry); 2187 } 2188 2189 return true; 2190 } 2191 2192 @InvokeOnAnyThread 2193 private void actualFileWrites() { 2194 // create the RosterEntry to its file 2195 _rosterEntry.writeFile(cvModel, variableModel); 2196 2197 // and store an updated roster file 2198 FileUtil.createDirectory(FileUtil.getUserFilesPath()); 2199 Roster.getDefault().writeRoster(); 2200 } 2201 2202 /** 2203 * Local dispose, which also invokes parent. Note that we remove the 2204 * components (removeAll) before taking those apart. 2205 */ 2206 @OverridingMethodsMustInvokeSuper 2207 @Override 2208 public void dispose() { 2209 log.debug("dispose local"); 2210 2211 // remove listeners (not much of a point, though) 2212 readChangesButton.removeItemListener(l1); 2213 writeChangesButton.removeItemListener(l2); 2214 readAllButton.removeItemListener(l3); 2215 writeAllButton.removeItemListener(l4); 2216 if (_programmingPane != null) { 2217 _programmingPane.removePropertyChangeListener(this); 2218 } 2219 2220 // dispose the list of panes 2221 //noinspection ForLoopReplaceableByForEach 2222 for (int i = 0; i < paneList.size(); i++) { 2223 PaneProgPane p = (PaneProgPane) paneList.get(i); 2224 tabPane.remove(p); 2225 p.dispose(); 2226 } 2227 paneList.clear(); 2228 2229 // dispose of things we owned, in order of dependence 2230 // null checks are needed for (partial) testing 2231 if (_rPane != null) _rPane.dispose(); 2232 if (_flPane != null) _flPane.dispose(); 2233 if (_rMPane != null) _rMPane.dispose(); 2234 if (variableModel != null) variableModel.dispose(); 2235 if (cvModel != null) cvModel.dispose(); 2236 if (_rosterEntry != null) { 2237 _rosterEntry.setOpen(false); 2238 } 2239 2240 // remove references to everything we remember 2241 progStatus = null; 2242 cvModel = null; 2243 variableModel = null; 2244 _rosterEntry = null; 2245 _rPane = null; 2246 _flPane = null; 2247 _rMPane = null; 2248 2249 paneList.clear(); 2250 paneList = null; 2251 _programmingPane = null; 2252 2253 tabPane = null; 2254 readChangesButton = null; 2255 writeChangesButton = null; 2256 readAllButton = null; 2257 writeAllButton = null; 2258 2259 log.debug("dispose superclass"); 2260 removeAll(); 2261 super.dispose(); 2262 } 2263 2264 /** 2265 * Set value of Preference option to show empty panes. 2266 * 2267 * @param yes true if empty panes should be shown 2268 */ 2269 public static void setShowEmptyPanes(boolean yes) { 2270 if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) { 2271 InstanceManager.getDefault(ProgrammerConfigManager.class).setShowEmptyPanes(yes); 2272 } 2273 } 2274 2275 /** 2276 * Get value of Preference option to show empty panes. 2277 * 2278 * @return value from programmer config. manager, else true. 2279 */ 2280 public static boolean getShowEmptyPanes() { 2281 return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null || 2282 InstanceManager.getDefault(ProgrammerConfigManager.class).isShowEmptyPanes(); 2283 } 2284 2285 public static boolean getDontDetachPanes() { 2286 return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null || 2287 InstanceManager.getDefault(ProgrammerConfigManager.class).isDontDetachPanes(); 2288 } 2289 public static void setDontDetachPanes(boolean yes) { 2290 if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) { 2291 InstanceManager.getDefault(ProgrammerConfigManager.class).setDontDetachPanes(yes); 2292 } 2293 } 2294 // This method is here to allow override in testing 2295 protected boolean checkDontDetachPanes() { return getDontDetachPanes(); } 2296 2297 2298 /** 2299 * Get value of whether current item should show empty panes. 2300 */ 2301 private boolean isShowingEmptyPanes() { 2302 boolean temp = getShowEmptyPanes(); 2303 if (programmerShowEmptyPanes.equals("yes")) { 2304 temp = true; 2305 } else if (programmerShowEmptyPanes.equals("no")) { 2306 temp = false; 2307 } 2308 if (decoderShowEmptyPanes.equals("yes")) { 2309 temp = true; 2310 } else if (decoderShowEmptyPanes.equals("no")) { 2311 temp = false; 2312 } 2313 return temp; 2314 } 2315 2316 /** 2317 * Option to control appearance of CV numbers in tool tips. 2318 * 2319 * @param yes true is CV numbers should be shown 2320 */ 2321 public static void setShowCvNumbers(boolean yes) { 2322 if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) { 2323 InstanceManager.getDefault(ProgrammerConfigManager.class).setShowCvNumbers(yes); 2324 } 2325 } 2326 2327 public static boolean getShowCvNumbers() { 2328 return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null || 2329 InstanceManager.getDefault(ProgrammerConfigManager.class).isShowCvNumbers(); 2330 } 2331 2332 public static void setCanCacheDefault(boolean yes) { 2333 if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) { 2334 InstanceManager.getDefault(ProgrammerConfigManager.class).setCanCacheDefault(yes); 2335 } 2336 } 2337 2338 public static boolean getCanCacheDefault() { 2339 return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null || 2340 InstanceManager.getDefault(ProgrammerConfigManager.class).isCanCacheDefault(); 2341 } 2342 2343 public static void setDoConfirmRead(boolean yes) { 2344 if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) { 2345 InstanceManager.getDefault(ProgrammerConfigManager.class).setDoConfirmRead(yes); 2346 } 2347 } 2348 2349 public static boolean getDoConfirmRead() { 2350 return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null || 2351 InstanceManager.getDefault(ProgrammerConfigManager.class).isDoConfirmRead(); 2352 } 2353 2354 public static void setDisableProgrammingTrack(boolean yes) { 2355 if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) { 2356 InstanceManager.getDefault(ProgrammerConfigManager.class).setDisableProgrammingTrack(yes); 2357 } 2358 } 2359 2360 public static boolean getDisableProgrammingTrack() { 2361 return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null || 2362 InstanceManager.getDefault(ProgrammerConfigManager.class).isDisableProgrammingTrack(); 2363 } 2364 2365 public static void setDisableProgrammingOnMain(boolean yes) { 2366 if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) { 2367 InstanceManager.getDefault(ProgrammerConfigManager.class).setDisableProgrammingOnMain(yes); 2368 } 2369 } 2370 2371 public static boolean getDisableProgrammingOnMain() { 2372 return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null || 2373 InstanceManager.getDefault(ProgrammerConfigManager.class).isDisableProgrammingOnMain(); 2374 } 2375 2376 public RosterEntry getRosterEntry() { 2377 return _rosterEntry; 2378 } 2379 2380 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(PaneProgFrame.class); 2381 2382}