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) && _rosterEntry.isLocoDataEnabled()) || 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 (_rosterEntry.isLocoDataEnabled()) { 1261 if (root.getChild("programmer").getAttribute("showRosterPane") != null && 1262 root.getChild("programmer").getAttribute("showRosterPane").getValue().equals("no")) { 1263 // create it, just don't make it visible 1264 _physicsPane = new RosterPhysicsPane(r); 1265 makePhysicsPane(r); 1266 } else { 1267 _physicsPane = new RosterPhysicsPane(r); 1268 final int iPhys = tabPane.getTabCount(); 1269 tabPane.addTab(RosterPhysicsPane.getTabTitle(), makeStandinComponent()); 1270 threadCount.incrementAndGet(); 1271 new javax.swing.SwingWorker<JComponent, Object>() { 1272 @Override 1273 public JComponent doInBackground() { 1274 return makePhysicsPane(r); 1275 } 1276 1277 @Override 1278 protected void done() { 1279 try { 1280 var result = get(); 1281 tabPane.setComponentAt(iPhys, result); 1282 } catch ( 1283 InterruptedException | 1284 java.util.concurrent.ExecutionException e) { 1285 log.error("Exception", e); 1286 } 1287 threadCount.decrementAndGet(); 1288 } 1289 }.execute(); 1290 } 1291 } 1292 1293 // add the Function Label tab 1294 if (root.getChild("programmer").getAttribute("showFnLanelPane").getValue().equals("yes") 1295 && !suppressFunctionLabels.equals("yes") 1296 ) { 1297 1298 final int i = tabPane.getTabCount(); 1299 tabPane.addTab(Bundle.getMessage("FUNCTION LABELS"), makeStandinComponent()); 1300 1301 threadCount.incrementAndGet(); 1302 new javax.swing.SwingWorker<JComponent, Object>(){ 1303 @Override 1304 public JComponent doInBackground() { 1305 return makeFunctionLabelPane(r); 1306 } 1307 @Override 1308 protected void done() { 1309 try { 1310 var result = get(); 1311 tabPane.setComponentAt(i, result); 1312 } catch (InterruptedException | java.util.concurrent.ExecutionException e) { 1313 log.error("Exception",e); 1314 } 1315 threadCount.decrementAndGet(); 1316 } 1317 }.execute(); 1318 1319 } else { 1320 // make it, just don't make it visible 1321 makeFunctionLabelPane(r); 1322 } 1323 1324 // add the Media tab 1325 if (root.getChild("programmer").getAttribute("showRosterMediaPane").getValue().equals("yes") 1326 && !suppressRosterMedia.equals("yes") 1327 ) { 1328 1329 final int i = tabPane.getTabCount(); 1330 tabPane.addTab(Bundle.getMessage("ROSTER MEDIA"), makeStandinComponent()); 1331 1332 threadCount.incrementAndGet(); 1333 new javax.swing.SwingWorker<JComponent, Object>(){ 1334 @Override 1335 public JComponent doInBackground() { 1336 return makeMediaPane(r); 1337 } 1338 @Override 1339 protected void done() { 1340 try { 1341 var result = get(); 1342 tabPane.setComponentAt(i, result); 1343 } catch (InterruptedException | java.util.concurrent.ExecutionException e) { 1344 log.error("Exception",e); 1345 } 1346 threadCount.decrementAndGet(); 1347 } 1348 }.execute(); 1349 1350 } else { 1351 // create it, just don't make it visible 1352 makeMediaPane(r); 1353 } 1354 1355 // add the comment tab 1356 JPanel commentTab = new JPanel(); 1357 var comment = new JTextArea(_rPane.getCommentDocument()); 1358 JScrollPane commentScroller = new JScrollPane(comment, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); 1359 commentTab.add(commentScroller); 1360 commentTab.setLayout(new BoxLayout(commentTab, BoxLayout.Y_AXIS)); 1361 tabPane.addTab(Bundle.getMessage("COMMENT PANE"), commentTab); 1362 1363 // for all "pane" elements in the programmer 1364 List<Element> progPaneList = base.getChildren("pane"); 1365 log.debug("will process {} pane definitions", progPaneList.size()); 1366 1367 for (Element temp : progPaneList) { 1368 // load each programmer pane 1369 List<Element> pnames = temp.getChildren("name"); 1370 boolean isProgPane = true; 1371 if ((pnames.size() > 0) && (decoderPaneList != null) && (decoderPaneList.size() > 0)) { 1372 String namePrimary = (pnames.get(0)).getValue(); // get non-localised name 1373 1374 // check if there is a same-name pane in decoder file 1375 // start at end to prevent concurrentmodification exception on remove 1376 for (int j = decoderPaneList.size() - 1; j >= 0; j--) { 1377 List<Element> dnames = decoderPaneList.get(j).getChildren("name"); 1378 if (dnames.size() > 0) { 1379 String namePrimaryDecoder = (dnames.get(0)).getValue(); // get non-localised name 1380 if (namePrimary.equals(namePrimaryDecoder)) { 1381 // replace programmer pane with same-name decoder pane 1382 temp = decoderPaneList.get(j); 1383 decoderPaneList.remove(j); // safe, not suspicious as we work end - front 1384 isProgPane = false; 1385 } 1386 } 1387 } 1388 } 1389 String name = jmri.util.jdom.LocaleSelector.getAttribute(temp, "name"); 1390 1391 // handle include/exclude 1392 if (isIncludedFE(temp, modelElem, _rosterEntry, "", "")) { 1393 newPane(name, temp, modelElem, false, isProgPane); // don't force showing if empty 1394 log.debug("readConfig - pane {} added", name); // these are also in RosterPrint 1395 } 1396 } 1397 log.trace("done processing panes"); 1398 } 1399 1400 /** 1401 * Make temporary contents for a pane while loading 1402 */ 1403 protected Component makeStandinComponent() { 1404 var retval = new JPanel(){ 1405 @Override 1406 public Dimension getPreferredSize() { 1407 // return a nominal size for the tabbed panes until manually resized 1408 return new java.awt.Dimension(900, 600); 1409 } 1410 }; 1411 retval.add(new JLabel(Bundle.getMessage("STANDIN MESSAGE"))); 1412 return retval; 1413 } 1414 1415 1416 /** 1417 * Reset all CV values to defaults stored earlier. 1418 * <p> 1419 * This will in turn update the variables. 1420 */ 1421 protected void resetToDefaults() { 1422 int n = defaultCvValues.length; 1423 for (int i = 0; i < n; i++) { 1424 CvValue cv = cvModel.getCvByNumber(defaultCvNumbers[i]); 1425 if (cv == null) { 1426 log.warn("Trying to set default in CV {} but didn't find the CV object", defaultCvNumbers[i]); 1427 } else { 1428 cv.setValue(defaultCvValues[i]); 1429 } 1430 } 1431 } 1432 1433 int[] defaultCvValues = null; 1434 String[] defaultCvNumbers = null; 1435 1436 /** 1437 * Save all CV values. 1438 * <p> 1439 * These stored values are used by {link #resetToDefaults()} 1440 */ 1441 protected void saveDefaults() { 1442 int n = cvModel.getRowCount(); 1443 defaultCvValues = new int[n]; 1444 defaultCvNumbers = new String[n]; 1445 1446 for (int i = 0; i < n; i++) { 1447 CvValue cv = cvModel.getCvByRow(i); 1448 defaultCvValues[i] = cv.getValue(); 1449 defaultCvNumbers[i] = cv.number(); 1450 } 1451 } 1452 1453 @InvokeOnAnyThread // transfers some operations to GUI thread 1454 protected JPanel makeInfoPane(RosterEntry r) { 1455 // create the identification pane (not configured by programmer file now; maybe later?) 1456 1457 JPanel outer = new JPanel(); 1458 ThreadingUtil.runOnGUI(()->{ 1459 outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS)); 1460 JPanel body = new JPanel(); 1461 body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS)); 1462 JScrollPane scrollPane = new JScrollPane(body); 1463 1464 // add roster info 1465 body.add(_rPane); 1466 1467 // add the store button 1468 JButton store = new JButton(Bundle.getMessage("ButtonSave")); 1469 store.setAlignmentX(JLabel.CENTER_ALIGNMENT); 1470 store.addActionListener(e -> storeFile()); 1471 1472 // add the reset button 1473 JButton reset = new JButton(Bundle.getMessage("ButtonResetDefaults")); 1474 reset.setAlignmentX(JLabel.CENTER_ALIGNMENT); 1475 if (decoderAllowResetDefaults.equals("no")) { 1476 reset.setEnabled(false); 1477 reset.setToolTipText(Bundle.getMessage("TipButtonResetDefaultsDisabled")); 1478 } else { 1479 reset.setToolTipText(Bundle.getMessage("TipButtonResetDefaults")); 1480 reset.addActionListener(e -> resetToDefaults()); 1481 } 1482 1483 int sizeX = Math.max(reset.getPreferredSize().width, store.getPreferredSize().width); 1484 int sizeY = Math.max(reset.getPreferredSize().height, store.getPreferredSize().height); 1485 store.setPreferredSize(new Dimension(sizeX, sizeY)); 1486 reset.setPreferredSize(new Dimension(sizeX, sizeY)); 1487 1488 store.setToolTipText(_rosterEntry.getFileName()); 1489 1490 JPanel buttons = new JPanel(); 1491 buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS)); 1492 1493 buttons.add(store); 1494 buttons.add(reset); 1495 1496 body.add(buttons); 1497 outer.add(scrollPane); 1498 1499 // arrange for the dcc address to be updated 1500 java.beans.PropertyChangeListener dccNews = e -> updateDccAddress(); 1501 primaryAddr = variableModel.findVar("Short Address"); 1502 if (primaryAddr == null) { 1503 log.debug("DCC Address monitor didn't find a Short Address variable"); 1504 } else { 1505 primaryAddr.addPropertyChangeListener(dccNews); 1506 } 1507 extendAddr = variableModel.findVar("Long Address"); 1508 if (extendAddr == null) { 1509 log.debug("DCC Address monitor didn't find an Long Address variable"); 1510 } else { 1511 extendAddr.addPropertyChangeListener(dccNews); 1512 } 1513 addMode = (EnumVariableValue) variableModel.findVar("Address Format"); 1514 if (addMode == null) { 1515 log.debug("DCC Address monitor didn't find an Address Format variable"); 1516 } else { 1517 addMode.addPropertyChangeListener(dccNews); 1518 } 1519 1520 // get right address to start 1521 updateDccAddress(); 1522 }); 1523 1524 return outer; 1525 } 1526 1527 1528 @InvokeOnAnyThread // transfers some operations to GUI thread 1529 protected JPanel makePhysicsPane(RosterEntry r) { 1530 // create the physics pane wrapper (not configured by programmer file) 1531 if (!r.isLocoDataEnabled()) { 1532 return null; 1533 } 1534 1535 JPanel outer = new JPanel(); 1536 ThreadingUtil.runOnGUI(() -> { 1537 outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS)); 1538 JPanel body = new JPanel(); 1539 body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS)); 1540 JScrollPane scrollPane = new JScrollPane(body); 1541 1542 // add physics info 1543 if (_physicsPane != null) { 1544 body.add(_physicsPane); 1545 } 1546 1547 // add the store button 1548 JButton store = new JButton(Bundle.getMessage("ButtonSave")); 1549 store.setAlignmentX(JLabel.CENTER_ALIGNMENT); 1550 store.addActionListener(e -> storeFile()); 1551 store.setToolTipText(_rosterEntry.getFileName()); 1552 JPanel buttons = new JPanel(); 1553 buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS)); 1554 buttons.add(store); 1555 body.add(buttons); 1556 1557 outer.add(scrollPane); 1558 }); 1559 return outer; 1560 } 1561 1562 @InvokeOnAnyThread // transfers some operations to GUI thread 1563 protected JPanel makeFunctionLabelPane(RosterEntry r) { 1564 // create the identification pane (not configured by programmer file now; maybe later?) 1565 1566 JPanel outer = new JPanel(); 1567 1568 ThreadingUtil.runOnGUI(()->{ 1569 outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS)); 1570 JPanel body = new JPanel(); 1571 body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS)); 1572 JScrollPane scrollPane = new JScrollPane(body); 1573 1574 // add tab description 1575 JLabel title = new JLabel(Bundle.getMessage("UseThisTabCustomize")); 1576 title.setAlignmentX(JLabel.CENTER_ALIGNMENT); 1577 body.add(title); 1578 body.add(new JLabel(" ")); // some padding 1579 1580 // add roster info 1581 _flPane = new FunctionLabelPane(r); 1582 //_flPane.setMaximumSize(_flPane.getPreferredSize()); 1583 body.add(_flPane); 1584 1585 // add the store button 1586 JButton store = new JButton(Bundle.getMessage("ButtonSave")); 1587 store.setAlignmentX(JLabel.CENTER_ALIGNMENT); 1588 store.addActionListener(e -> storeFile()); 1589 1590 store.setToolTipText(_rosterEntry.getFileName()); 1591 1592 JPanel buttons = new JPanel(); 1593 buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS)); 1594 1595 buttons.add(store); 1596 1597 body.add(buttons); 1598 outer.add(scrollPane); 1599 }); 1600 return outer; 1601 } 1602 1603 @InvokeOnAnyThread // transfers some operations to GUI thread 1604 protected JPanel makeMediaPane(RosterEntry r) { 1605 // create the identification pane (not configured by programmer file now; maybe later?) 1606 JPanel outer = new JPanel(); 1607 1608 ThreadingUtil.runOnGUI(()->{ 1609 outer.setLayout(new BoxLayout(outer, BoxLayout.Y_AXIS)); 1610 JPanel body = new JPanel(); 1611 body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS)); 1612 JScrollPane scrollPane = new JScrollPane(body); 1613 1614 // add tab description 1615 JLabel title = new JLabel(Bundle.getMessage("UseThisTabMedia")); 1616 title.setAlignmentX(JLabel.CENTER_ALIGNMENT); 1617 body.add(title); 1618 body.add(new JLabel(" ")); // some padding 1619 1620 // add roster info 1621 _rMPane = new RosterMediaPane(r); 1622 _rMPane.setMaximumSize(_rMPane.getPreferredSize()); 1623 body.add(_rMPane); 1624 1625 // add the store button 1626 JButton store = new JButton(Bundle.getMessage("ButtonSave")); 1627 store.setAlignmentX(JLabel.CENTER_ALIGNMENT); 1628 store.addActionListener(e -> storeFile()); 1629 1630 JPanel buttons = new JPanel(); 1631 buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS)); 1632 1633 buttons.add(store); 1634 1635 body.add(buttons); 1636 outer.add(scrollPane); 1637 }); 1638 return outer; 1639 } 1640 1641 // hold refs to variables to check dccAddress 1642 VariableValue primaryAddr = null; 1643 VariableValue extendAddr = null; 1644 EnumVariableValue addMode = null; 1645 1646 boolean longMode = false; 1647 String newAddr = null; 1648 1649 void updateDccAddress() { 1650 1651 if (log.isDebugEnabled()) { 1652 log.debug("updateDccAddress: short {} long {} mode {}", primaryAddr == null ? "<null>" : primaryAddr.getValueString(), extendAddr == null ? "<null>" : extendAddr.getValueString(), addMode == null ? "<null>" : addMode.getValueString()); 1653 } 1654 1655 new DccAddressVarHandler(primaryAddr, extendAddr, addMode) { 1656 @Override 1657 protected void doPrimary() { 1658 // short address mode 1659 longMode = false; 1660 if (primaryAddr != null && !primaryAddr.getValueString().equals("")) { 1661 newAddr = primaryAddr.getValueString(); 1662 } 1663 } 1664 1665 @Override 1666 protected void doExtended() { 1667 // long address 1668 if (extendAddr != null && !extendAddr.getValueString().equals("")) { 1669 longMode = true; 1670 newAddr = extendAddr.getValueString(); 1671 } 1672 } 1673 }; 1674 // update if needed 1675 if (newAddr != null) { 1676 // store DCC address, type 1677 _rPane.setDccAddress(newAddr); 1678 _rPane.setDccAddressLong(longMode); 1679 } 1680 } 1681 1682 public void newPane(String name, Element pane, Element modelElem, boolean enableEmpty, boolean programmerPane) { 1683 if (log.isDebugEnabled()) { 1684 log.debug("newPane with enableEmpty {} showEmptyPanes {}", enableEmpty, isShowingEmptyPanes()); 1685 } 1686 1687 // create place-keeper tab 1688 ThreadingUtil.runOnGUI(() -> { 1689 tabPane.addTab(name, makeStandinComponent()); 1690 }); 1691 1692 // create a panel to hold columns via separate thread 1693 final var parent = this; 1694 threadCount.incrementAndGet(); 1695 new javax.swing.SwingWorker<PaneProgPane, Object>(){ 1696 @Override 1697 public PaneProgPane doInBackground() { 1698 return new PaneProgPane(parent, name, pane, cvModel, variableModel, modelElem, _rosterEntry, programmerPane); 1699 } 1700 @Override 1701 protected void done() { 1702 try { 1703 var p = get(); 1704 p.setOpaque(true); 1705 if (noDecoder) { 1706 p.setNoDecoder(); 1707 cvModel.setNoDecoder(); 1708 } 1709 // how to handle the tab depends on whether it has contents and option setting 1710 int index; 1711 if (enableEmpty || !p.cvList.isEmpty() || !p.varList.isEmpty()) { 1712 // Was there a race condition here with qualified panes? 1713 // QualifiedVarTest attempts to invoke that, but haven't it with the following code 1714 index = tabPane.indexOfTab(name); 1715 tabPane.setComponentAt(tabPane.indexOfTab(name), p); // always add if not empty 1716 tabPane.setToolTipTextAt(tabPane.indexOfTab(name), p.getToolTipText()); 1717 } else if (isShowingEmptyPanes()) { 1718 // here empty, but showing anyway as disabled 1719 index = tabPane.indexOfTab(name); 1720 tabPane.setComponentAt(tabPane.indexOfTab(name), p); 1721 tabPane.setToolTipTextAt(tabPane.indexOfTab(name), 1722 Bundle.getMessage("TipTabEmptyNoCategory")); 1723 tabPane.setEnabledAt(tabPane.indexOfTab(name), true); // need to enable the pane so user can see message 1724 } else { 1725 // here not showing tab at all 1726 index = -1; 1727 log.trace("deleted {} tab here", name); 1728 tabPane.removeTabAt(tabPane.indexOfTab(name)); 1729 } 1730 1731 // remember it for programming 1732 paneList.add(p); 1733 1734 // if visible, set qualifications 1735 if (index >= 0) { 1736 processModifierElements(pane, p, variableModel, tabPane, name); 1737 } 1738 threadCount.decrementAndGet(); 1739 } catch (InterruptedException | java.util.concurrent.ExecutionException e) { 1740 log.error("Exception",e); 1741 } 1742 } 1743 }.execute(); 1744 1745 } 1746 1747 /** 1748 * If there are any modifier elements, process them. 1749 * 1750 * @param e Process the contents of this element 1751 * @param pane Destination of any visible items 1752 * @param model Used to locate any needed variables 1753 * @param tabPane For overall GUI navigation 1754 * @param name Which pane in the overall window 1755 */ 1756 protected void processModifierElements(Element e, final PaneProgPane pane, VariableTableModel model, final JTabbedPane tabPane, final String name) { 1757 QualifierAdder qa = new QualifierAdder() { 1758 @Override 1759 protected Qualifier createQualifier(VariableValue var, String relation, String value) { 1760 return new PaneQualifier(pane, var, Integer.parseInt(value), relation, tabPane, name); 1761 } 1762 1763 @Override 1764 protected void addListener(java.beans.PropertyChangeListener qc) { 1765 pane.addPropertyChangeListener(qc); 1766 } 1767 }; 1768 1769 qa.processModifierElements(e, model); 1770 } 1771 1772 @Override 1773 public BusyGlassPane getBusyGlassPane() { 1774 return glassPane; 1775 } 1776 1777 /** 1778 * Create a BusyGlassPane transparent layer over the panel blocking any 1779 * other interaction, excluding a supplied button. 1780 * 1781 * @param activeButton a button to put on top of the pane 1782 */ 1783 @Override 1784 public void prepGlassPane(AbstractButton activeButton) { 1785 List<Rectangle> rectangles = new ArrayList<>(); 1786 1787 if (glassPane != null) { 1788 glassPane.dispose(); 1789 } 1790 activeComponents.clear(); 1791 activeComponents.add(activeButton); 1792 if (activeButton == readChangesButton || activeButton == readAllButton 1793 || activeButton == writeChangesButton || activeButton == writeAllButton) { 1794 if (activeButton == readChangesButton) { 1795 for (JPanel jPanel : paneList) { 1796 assert jPanel instanceof PaneProgPane; 1797 activeComponents.add(((PaneProgPane) jPanel).readChangesButton); 1798 } 1799 } else if (activeButton == readAllButton) { 1800 for (JPanel jPanel : paneList) { 1801 assert jPanel instanceof PaneProgPane; 1802 activeComponents.add(((PaneProgPane) jPanel).readAllButton); 1803 } 1804 } else if (activeButton == writeChangesButton) { 1805 for (JPanel jPanel : paneList) { 1806 assert jPanel instanceof PaneProgPane; 1807 activeComponents.add(((PaneProgPane) jPanel).writeChangesButton); 1808 } 1809 } else { // (activeButton == writeAllButton) { 1810 for (JPanel jPanel : paneList) { 1811 assert jPanel instanceof PaneProgPane; 1812 activeComponents.add(((PaneProgPane) jPanel).writeAllButton); 1813 } 1814 } 1815 1816 for (int i = 0; i < tabPane.getTabCount(); i++) { 1817 rectangles.add(tabPane.getUI().getTabBounds(tabPane, i)); 1818 } 1819 } 1820 glassPane = new BusyGlassPane(activeComponents, rectangles, this.getContentPane(), this); 1821 this.setGlassPane(glassPane); 1822 } 1823 1824 @Override 1825 public void paneFinished() { 1826 log.debug("paneFinished with isBusy={}", isBusy()); 1827 if (!isBusy()) { 1828 if (glassPane != null) { 1829 glassPane.setVisible(false); 1830 glassPane.dispose(); 1831 glassPane = null; 1832 } 1833 setCursor(Cursor.getDefaultCursor()); 1834 enableButtons(true); 1835 } 1836 } 1837 1838 /** 1839 * Enable the read/write buttons. 1840 * <p> 1841 * In addition, if a programming mode pane is present, its "set" button is 1842 * enabled. 1843 * 1844 * @param stat Are reads possible? If false, so not enable the read buttons. 1845 */ 1846 @Override 1847 public void enableButtons(boolean stat) { 1848 log.debug("enableButtons({})", stat); 1849 if (noDecoder) { 1850 // If we don't have a decoder, no read or write is possible 1851 stat = false; 1852 } 1853 if (stat) { 1854 enableReadButtons(); 1855 } else { 1856 readChangesButton.setEnabled(false); 1857 readAllButton.setEnabled(false); 1858 } 1859 writeChangesButton.setEnabled(stat); 1860 writeAllButton.setEnabled(stat); 1861 1862 var tempModePane = getModePane(); 1863 if (tempModePane != null) { 1864 tempModePane.setEnabled(stat); 1865 } 1866 } 1867 1868 boolean justChanges; 1869 1870 @Override 1871 public boolean isBusy() { 1872 return _busy; 1873 } 1874 private boolean _busy = false; 1875 1876 private void setBusy(boolean stat) { 1877 log.debug("setBusy({})", stat); 1878 _busy = stat; 1879 1880 for (JPanel jPanel : paneList) { 1881 assert jPanel instanceof PaneProgPane; 1882 ((PaneProgPane) jPanel).enableButtons(!stat); 1883 } 1884 if (!stat) { 1885 paneFinished(); 1886 } 1887 } 1888 1889 /** 1890 * Invoked by "Read Changes" button, this sets in motion a continuing 1891 * sequence of "read changes" operations on the panes. 1892 * <p> 1893 * Each invocation of this method reads one pane; completion of that request 1894 * will cause it to happen again, reading the next pane, until there's 1895 * nothing left to read. 1896 * 1897 * @return true if a read has been started, false if the operation is 1898 * complete. 1899 */ 1900 public boolean readChanges() { 1901 log.debug("readChanges starts"); 1902 justChanges = true; 1903 for (JPanel jPanel : paneList) { 1904 assert jPanel instanceof PaneProgPane; 1905 ((PaneProgPane) jPanel).setToRead(justChanges, true); 1906 } 1907 setBusy(true); 1908 enableButtons(false); 1909 readChangesButton.setEnabled(true); 1910 glassPane.setVisible(true); 1911 paneListIndex = 0; 1912 // start operation 1913 return doRead(); 1914 } 1915 1916 /** 1917 * Invoked by the "Read All" button, this sets in motion a continuing 1918 * sequence of "read all" operations on the panes. 1919 * <p> 1920 * Each invocation of this method reads one pane; completion of that request 1921 * will cause it to happen again, reading the next pane, until there's 1922 * nothing left to read. 1923 * 1924 * @return true if a read has been started, false if the operation is 1925 * complete. 1926 */ 1927 public boolean readAll() { 1928 log.debug("readAll starts"); 1929 justChanges = false; 1930 for (JPanel jPanel : paneList) { 1931 assert jPanel instanceof PaneProgPane; 1932 ((PaneProgPane) jPanel).setToRead(justChanges, true); 1933 } 1934 setBusy(true); 1935 enableButtons(false); 1936 readAllButton.setEnabled(true); 1937 glassPane.setVisible(true); 1938 paneListIndex = 0; 1939 // start operation 1940 return doRead(); 1941 } 1942 1943 boolean doRead() { 1944 _read = true; 1945 while (paneListIndex < paneList.size()) { 1946 log.debug("doRead on {}", paneListIndex); 1947 _programmingPane = (PaneProgPane) paneList.get(paneListIndex); 1948 // some programming operations are instant, so need to have listener registered at readPaneAll 1949 _programmingPane.addPropertyChangeListener(this); 1950 boolean running; 1951 if (justChanges) { 1952 running = _programmingPane.readPaneChanges(); 1953 } else { 1954 running = _programmingPane.readPaneAll(); 1955 } 1956 1957 paneListIndex++; 1958 1959 if (running) { 1960 // operation in progress, stop loop until called back 1961 log.debug("doRead expecting callback from readPane {}", paneListIndex); 1962 return true; 1963 } else { 1964 _programmingPane.removePropertyChangeListener(this); 1965 } 1966 } 1967 // nothing to program, end politely 1968 _programmingPane = null; 1969 enableButtons(true); 1970 setBusy(false); 1971 readChangesButton.setSelected(false); 1972 readAllButton.setSelected(false); 1973 log.debug("doRead found nothing to do"); 1974 return false; 1975 } 1976 1977 /** 1978 * Invoked by "Write All" button, this sets in motion a continuing sequence 1979 * of "write all" operations on each pane. Each invocation of this method 1980 * writes one pane; completion of that request will cause it to happen 1981 * again, writing the next pane, until there's nothing left to write. 1982 * 1983 * @return true if a write has been started, false if the operation is 1984 * complete. 1985 */ 1986 public boolean writeAll() { 1987 log.debug("writeAll starts"); 1988 justChanges = false; 1989 for (JPanel jPanel : paneList) { 1990 assert jPanel instanceof PaneProgPane; 1991 ((PaneProgPane) jPanel).setToWrite(justChanges, true); 1992 } 1993 setBusy(true); 1994 enableButtons(false); 1995 writeAllButton.setEnabled(true); 1996 glassPane.setVisible(true); 1997 paneListIndex = 0; 1998 return doWrite(); 1999 } 2000 2001 /** 2002 * Invoked by "Write Changes" button, this sets in motion a continuing 2003 * sequence of "write changes" operations on each pane. 2004 * <p> 2005 * Each invocation of this method writes one pane; completion of that 2006 * request will cause it to happen again, writing the next pane, until 2007 * there's nothing left to write. 2008 * 2009 * @return true if a write has been started, false if the operation is 2010 * complete 2011 */ 2012 public boolean writeChanges() { 2013 log.debug("writeChanges starts"); 2014 justChanges = true; 2015 for (JPanel jPanel : paneList) { 2016 assert jPanel instanceof PaneProgPane; 2017 ((PaneProgPane) jPanel).setToWrite(justChanges, true); 2018 } 2019 setBusy(true); 2020 enableButtons(false); 2021 writeChangesButton.setEnabled(true); 2022 glassPane.setVisible(true); 2023 paneListIndex = 0; 2024 return doWrite(); 2025 } 2026 2027 boolean doWrite() { 2028 _read = false; 2029 while (paneListIndex < paneList.size()) { 2030 log.debug("doWrite starts on {}", paneListIndex); 2031 _programmingPane = (PaneProgPane) paneList.get(paneListIndex); 2032 // some programming operations are instant, so need to have listener registered at readPane 2033 _programmingPane.addPropertyChangeListener(this); 2034 boolean running; 2035 if (justChanges) { 2036 running = _programmingPane.writePaneChanges(); 2037 } else { 2038 running = _programmingPane.writePaneAll(); 2039 } 2040 2041 paneListIndex++; 2042 2043 if (running) { 2044 // operation in progress, stop loop until called back 2045 log.debug("doWrite expecting callback from writePane {}", paneListIndex); 2046 return true; 2047 } else { 2048 _programmingPane.removePropertyChangeListener(this); 2049 } 2050 } 2051 // nothing to program, end politely 2052 _programmingPane = null; 2053 enableButtons(true); 2054 setBusy(false); 2055 writeChangesButton.setSelected(false); 2056 writeAllButton.setSelected(false); 2057 log.debug("doWrite found nothing to do"); 2058 return false; 2059 } 2060 2061 /** 2062 * Prepare a roster entry to be printed, and display a selection list. 2063 * 2064 * @see jmri.jmrit.roster.PrintRosterEntry#doPrintPanes(boolean) 2065 * @param preview true if output should go to a Preview pane on screen, 2066 * false to output to a printer (dialog) 2067 */ 2068 public void printPanes(final boolean preview) { 2069 PrintRosterEntry pre = new PrintRosterEntry(_rosterEntry, paneList, _flPane, _rMPane, this); 2070 pre.printPanes(preview); 2071 } 2072 2073 boolean _read = true; 2074 PaneProgPane _programmingPane = null; 2075 2076 /** 2077 * Get notification of a variable property change in the pane, specifically 2078 * "busy" going to false at the end of a programming operation. 2079 * 2080 * @param e Event, used to find source 2081 */ 2082 @Override 2083 public void propertyChange(java.beans.PropertyChangeEvent e) { 2084 // check for the right event 2085 if (_programmingPane == null) { 2086 log.warn("unexpected propertyChange: {}", e); 2087 return; 2088 } else if (log.isDebugEnabled()) { 2089 log.debug("property changed: {} new value: {}", e.getPropertyName(), e.getNewValue()); 2090 } 2091 log.debug("check valid: {} {} {}", e.getSource() == _programmingPane, !e.getPropertyName().equals("Busy"), e.getNewValue().equals(Boolean.FALSE)); 2092 if (e.getSource() == _programmingPane 2093 && e.getPropertyName().equals("Busy") 2094 && e.getNewValue().equals(Boolean.FALSE)) { 2095 2096 log.debug("end of a programming pane operation, remove"); 2097 // remove existing listener 2098 _programmingPane.removePropertyChangeListener(this); 2099 _programmingPane = null; 2100 // restart the operation 2101 if (_read && readChangesButton.isSelected()) { 2102 log.debug("restart readChanges"); 2103 doRead(); 2104 } else if (_read && readAllButton.isSelected()) { 2105 log.debug("restart readAll"); 2106 doRead(); 2107 } else if (writeChangesButton.isSelected()) { 2108 log.debug("restart writeChanges"); 2109 doWrite(); 2110 } else if (writeAllButton.isSelected()) { 2111 log.debug("restart writeAll"); 2112 doWrite(); 2113 } else { 2114 log.debug("read/write end because button is lifted"); 2115 setBusy(false); 2116 } 2117 } 2118 } 2119 2120 /** 2121 * Store the locomotives information in the roster (and a RosterEntry file). 2122 * 2123 * @return false if store failed 2124 */ 2125 @InvokeOnGuiThread 2126 public boolean storeFile() { 2127 log.debug("storeFile starts"); 2128 2129 if (_rPane.checkDuplicate()) { 2130 JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorDuplicateID")); 2131 return false; 2132 } 2133 2134 // reload the RosterEntry 2135 updateDccAddress(); 2136 _rPane.update(_rosterEntry); 2137 _flPane.update(_rosterEntry); 2138 _rMPane.update(_rosterEntry); 2139 if (_physicsPane != null && _rosterEntry.isLocoDataEnabled()) { 2140 _physicsPane.update(_rosterEntry); 2141 } 2142 2143 // id has to be set! 2144 if (_rosterEntry.getId().equals("") || _rosterEntry.getId().equals(Bundle.getMessage("LabelNewDecoder"))) { 2145 log.debug("storeFile without a filename; issued dialog"); 2146 JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("PromptFillInID")); 2147 return false; 2148 } 2149 2150 // if there isn't a filename, store using the id 2151 _rosterEntry.ensureFilenameExists(); 2152 String filename = _rosterEntry.getFileName(); 2153 2154 // do actual file writes in a separate thread, wait for success 2155 threadCount.incrementAndGet(); 2156 new javax.swing.SwingWorker<Object, Object>(){ 2157 @Override 2158 public Object doInBackground() { 2159 actualFileWrites(); 2160 return null; 2161 } 2162 @Override 2163 protected void done() { 2164 // show OK status 2165 progStatus.setText(java.text.MessageFormat.format( 2166 Bundle.getMessage("StateSaveOK"), filename)); 2167 threadCount.decrementAndGet(); 2168 } 2169 }.execute(); 2170 2171 // mark this as a success 2172 variableModel.setFileDirty(false); 2173 maxFnNumDirty = false; 2174 2175 // save date changed, update 2176 _rPane.updateGUI(_rosterEntry); 2177 if (_physicsPane != null && _rosterEntry.isLocoDataEnabled()) { 2178 _physicsPane.updateGUI(_rosterEntry); 2179 } 2180 2181 return true; 2182 } 2183 2184 @InvokeOnAnyThread 2185 private void actualFileWrites() { 2186 // create the RosterEntry to its file 2187 _rosterEntry.writeFile(cvModel, variableModel); 2188 2189 // and store an updated roster file 2190 FileUtil.createDirectory(FileUtil.getUserFilesPath()); 2191 Roster.getDefault().writeRoster(); 2192 } 2193 2194 /** 2195 * Local dispose, which also invokes parent. Note that we remove the 2196 * components (removeAll) before taking those apart. 2197 */ 2198 @OverridingMethodsMustInvokeSuper 2199 @Override 2200 public void dispose() { 2201 log.debug("dispose local"); 2202 2203 // remove listeners (not much of a point, though) 2204 readChangesButton.removeItemListener(l1); 2205 writeChangesButton.removeItemListener(l2); 2206 readAllButton.removeItemListener(l3); 2207 writeAllButton.removeItemListener(l4); 2208 if (_programmingPane != null) { 2209 _programmingPane.removePropertyChangeListener(this); 2210 } 2211 2212 // dispose the list of panes 2213 //noinspection ForLoopReplaceableByForEach 2214 for (int i = 0; i < paneList.size(); i++) { 2215 PaneProgPane p = (PaneProgPane) paneList.get(i); 2216 tabPane.remove(p); 2217 p.dispose(); 2218 } 2219 paneList.clear(); 2220 2221 // dispose of things we owned, in order of dependence 2222 // null checks are needed for (partial) testing 2223 if (_rPane != null) _rPane.dispose(); 2224 if (_flPane != null) _flPane.dispose(); 2225 if (_rMPane != null) _rMPane.dispose(); 2226 if (variableModel != null) variableModel.dispose(); 2227 if (cvModel != null) cvModel.dispose(); 2228 if (_rosterEntry != null) { 2229 _rosterEntry.setOpen(false); 2230 } 2231 2232 // remove references to everything we remember 2233 progStatus = null; 2234 cvModel = null; 2235 variableModel = null; 2236 _rosterEntry = null; 2237 _rPane = null; 2238 _flPane = null; 2239 _rMPane = null; 2240 2241 paneList.clear(); 2242 paneList = null; 2243 _programmingPane = null; 2244 2245 tabPane = null; 2246 readChangesButton = null; 2247 writeChangesButton = null; 2248 readAllButton = null; 2249 writeAllButton = null; 2250 2251 log.debug("dispose superclass"); 2252 removeAll(); 2253 super.dispose(); 2254 } 2255 2256 /** 2257 * Set value of Preference option to show empty panes. 2258 * 2259 * @param yes true if empty panes should be shown 2260 */ 2261 public static void setShowEmptyPanes(boolean yes) { 2262 if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) { 2263 InstanceManager.getDefault(ProgrammerConfigManager.class).setShowEmptyPanes(yes); 2264 } 2265 } 2266 2267 /** 2268 * Get value of Preference option to show empty panes. 2269 * 2270 * @return value from programmer config. manager, else true. 2271 */ 2272 public static boolean getShowEmptyPanes() { 2273 return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null || 2274 InstanceManager.getDefault(ProgrammerConfigManager.class).isShowEmptyPanes(); 2275 } 2276 2277 /** 2278 * Get value of whether current item should show empty panes. 2279 */ 2280 private boolean isShowingEmptyPanes() { 2281 boolean temp = getShowEmptyPanes(); 2282 if (programmerShowEmptyPanes.equals("yes")) { 2283 temp = true; 2284 } else if (programmerShowEmptyPanes.equals("no")) { 2285 temp = false; 2286 } 2287 if (decoderShowEmptyPanes.equals("yes")) { 2288 temp = true; 2289 } else if (decoderShowEmptyPanes.equals("no")) { 2290 temp = false; 2291 } 2292 return temp; 2293 } 2294 2295 /** 2296 * Option to control appearance of CV numbers in tool tips. 2297 * 2298 * @param yes true is CV numbers should be shown 2299 */ 2300 public static void setShowCvNumbers(boolean yes) { 2301 if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) { 2302 InstanceManager.getDefault(ProgrammerConfigManager.class).setShowCvNumbers(yes); 2303 } 2304 } 2305 2306 public static boolean getShowCvNumbers() { 2307 return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null || 2308 InstanceManager.getDefault(ProgrammerConfigManager.class).isShowCvNumbers(); 2309 } 2310 2311 public static void setCanCacheDefault(boolean yes) { 2312 if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) { 2313 InstanceManager.getDefault(ProgrammerConfigManager.class).setCanCacheDefault(yes); 2314 } 2315 } 2316 2317 public static boolean getCanCacheDefault() { 2318 return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null || 2319 InstanceManager.getDefault(ProgrammerConfigManager.class).isCanCacheDefault(); 2320 } 2321 2322 public static void setDoConfirmRead(boolean yes) { 2323 if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) { 2324 InstanceManager.getDefault(ProgrammerConfigManager.class).setDoConfirmRead(yes); 2325 } 2326 } 2327 2328 public static boolean getDoConfirmRead() { 2329 return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null || 2330 InstanceManager.getDefault(ProgrammerConfigManager.class).isDoConfirmRead(); 2331 } 2332 2333 public static void setDisableProgrammingTrack(boolean yes) { 2334 if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) { 2335 InstanceManager.getDefault(ProgrammerConfigManager.class).setDisableProgrammingTrack(yes); 2336 } 2337 } 2338 2339 public static boolean getDisableProgrammingTrack() { 2340 return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null || 2341 InstanceManager.getDefault(ProgrammerConfigManager.class).isDisableProgrammingTrack(); 2342 } 2343 2344 public static void setDisableProgrammingOnMain(boolean yes) { 2345 if (InstanceManager.getNullableDefault(ProgrammerConfigManager.class) != null) { 2346 InstanceManager.getDefault(ProgrammerConfigManager.class).setDisableProgrammingOnMain(yes); 2347 } 2348 } 2349 2350 public static boolean getDisableProgrammingOnMain() { 2351 return InstanceManager.getNullableDefault(ProgrammerConfigManager.class) == null || 2352 InstanceManager.getDefault(ProgrammerConfigManager.class).isDisableProgrammingOnMain(); 2353 } 2354 2355 public RosterEntry getRosterEntry() { 2356 return _rosterEntry; 2357 } 2358 2359 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(PaneProgFrame.class); 2360 2361}