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