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