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