001package apps.gui3.tabbedpreferences; 002 003import apps.AppConfigBase; 004import apps.ConfigBundle; 005 006import java.awt.BorderLayout; 007import java.awt.CardLayout; 008import java.awt.Dimension; 009import java.awt.event.ActionEvent; 010import java.util.ArrayList; 011import java.util.HashSet; 012import java.util.List; 013import java.util.ServiceLoader; 014import java.util.Set; 015import java.util.function.BooleanSupplier; 016 017import javax.swing.BorderFactory; 018import javax.swing.BoxLayout; 019import javax.swing.ImageIcon; 020import javax.swing.JButton; 021import javax.swing.JComponent; 022import javax.swing.JLabel; 023import javax.swing.JList; 024import javax.swing.JPanel; 025import javax.swing.JScrollPane; 026import javax.swing.JSeparator; 027import javax.swing.JTabbedPane; 028import javax.swing.ListSelectionModel; 029import javax.swing.event.ListSelectionEvent; 030 031import jmri.*; 032import jmri.swing.PreferencesPanel; 033import jmri.swing.PreferencesSubPanel; 034import jmri.util.FileUtil; 035import jmri.util.ThreadingUtil; 036import jmri.util.swing.JmriJOptionPane; 037 038import org.jdom2.Element; 039 040/** 041 * Provide access to preferences via a tabbed pane. 042 * 043 * Preferences panels provided by a {@link java.util.ServiceLoader} will be 044 * automatically loaded if they implement the 045 * {@link jmri.swing.PreferencesPanel} interface. 046 * <p> 047 * JMRI apps (generally) create one object of this type on the main thread as 048 * part of initialization, which is then made available via the 049 * {@link InstanceManager}. 050 * 051 * @author Bob Jacobsen Copyright 2010, 2019 052 * @author Randall Wood 2012, 2016 053 */ 054public class TabbedPreferences extends AppConfigBase { 055 056 @Override 057 public String getHelpTarget() { 058 return "package.apps.TabbedPreferences"; 059 } 060 061 @Override 062 public String getTitle() { 063 return Bundle.getMessage("TitlePreferences"); 064 } 065 // Preferences Window Title 066 067 @Override 068 public boolean isMultipleInstances() { 069 return false; 070 } // only one of these! 071 072 ArrayList<Element> preferencesElements = new ArrayList<>(); 073 074 JPanel detailpanel = new JPanel(); 075 { 076 // The default panel needs to have a CardLayout 077 detailpanel.setLayout(new CardLayout()); 078 } 079 080 ArrayList<PreferencesCatItems> preferencesArray = new ArrayList<>(); 081 JPanel buttonpanel; 082 JList<String> list; 083 JButton save; 084 JScrollPane listScroller; 085 086 public TabbedPreferences() { 087 088 /* 089 * Adds the place holders for the menu managedPreferences so that any managedPreferences add by 090 * third party code is added to the end 091 */ 092 preferencesArray.add(new PreferencesCatItems("CONNECTIONS", rb 093 .getString("MenuConnections"), 100)); 094 095 preferencesArray.add(new PreferencesCatItems("DEFAULTS", rb 096 .getString("MenuDefaults"), 200)); 097 098 preferencesArray.add(new PreferencesCatItems("FILELOCATIONS", rb 099 .getString("MenuFileLocation"), 300)); 100 101 preferencesArray.add(new PreferencesCatItems("STARTUP", rb 102 .getString("MenuStartUp"), 400)); 103 104 preferencesArray.add(new PreferencesCatItems("DISPLAY", rb 105 .getString("MenuDisplay"), 500)); 106 107 preferencesArray.add(new PreferencesCatItems("MESSAGES", rb 108 .getString("MenuMessages"), 600)); 109 110 preferencesArray.add(new PreferencesCatItems("ROSTER", rb 111 .getString("MenuRoster"), 700)); 112 113 preferencesArray.add(new PreferencesCatItems("THROTTLE", rb 114 .getString("MenuThrottle"), 800)); 115 116 preferencesArray.add(new PreferencesCatItems("WITHROTTLE", rb 117 .getString("MenuWiThrottle"), 900)); 118 119 // initialization process via init 120 init(); 121 } 122 123 /** 124 * Initialize, including loading classes provided by a 125 * {@link java.util.ServiceLoader}. 126 * <p> 127 * This creates a thread which creates items, then 128 * invokes the GUI thread to add them in. 129 */ 130 private void init() { 131 list = new JList<>(); 132 listScroller = new JScrollPane(list); 133 listScroller.setPreferredSize(new Dimension(100, 100)); 134 135 buttonpanel = new JPanel(); 136 buttonpanel.setLayout(new BoxLayout(buttonpanel, BoxLayout.Y_AXIS)); 137 buttonpanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 3)); 138 139 detailpanel = new JPanel(); 140 detailpanel.setLayout(new CardLayout()); 141 detailpanel.setBorder(BorderFactory.createEmptyBorder(6, 3, 6, 6)); 142 143 save = new JButton( 144 ConfigBundle.getMessage("ButtonSave"), 145 new ImageIcon(FileUtil.findURL("program:resources/icons/misc/gui3/SaveIcon.png", FileUtil.Location.INSTALLED))); 146 save.addActionListener((ActionEvent e) -> { 147 savePressed(invokeSaveOptions()); 148 }); 149 150 setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); 151 // panels that are dependent upon another panel being added first 152 Set<PreferencesPanel> delayed = new HashSet<>(); 153 154 // add preference panels registered with the Instance Manager 155 for (PreferencesPanel panel : InstanceManager.getList(jmri.swing.PreferencesPanel.class)) { 156 if (panel instanceof PreferencesSubPanel) { 157 String parent = ((PreferencesSubPanel) panel).getParentClassName(); 158 if (!this.getPreferencesPanels().containsKey(parent)) { 159 delayed.add(panel); 160 } else { 161 ((PreferencesSubPanel) panel).setParent(this.getPreferencesPanels().get(parent)); 162 } 163 } 164 if (!delayed.contains(panel)) { 165 this.addPreferencesPanel(panel); 166 } 167 } 168 169 for (PreferencesPanel panel : ServiceLoader.load(PreferencesPanel.class)) { 170 if (panel instanceof PreferencesSubPanel) { 171 String parent = ((PreferencesSubPanel) panel).getParentClassName(); 172 if (!this.getPreferencesPanels().containsKey(parent)) { 173 delayed.add(panel); 174 } else { 175 ((PreferencesSubPanel) panel).setParent(this.getPreferencesPanels().get(parent)); 176 } 177 } 178 if (!delayed.contains(panel)) { 179 this.addPreferencesPanel(panel); 180 } 181 } 182 while (!delayed.isEmpty()) { 183 Set<PreferencesPanel> iterated = new HashSet<>(delayed); 184 iterated.stream().filter((panel) -> (panel instanceof PreferencesSubPanel)).forEach((panel) -> { 185 String parent = ((PreferencesSubPanel) panel).getParentClassName(); 186 if (this.getPreferencesPanels().containsKey(parent)) { 187 ((PreferencesSubPanel) panel).setParent(this.getPreferencesPanels().get(parent)); 188 delayed.remove(panel); 189 this.addPreferencesPanel(panel); 190 } 191 }); 192 } 193 preferencesArray.stream().forEach((preferences) -> { 194 detailpanel.add(preferences.getPanel(), preferences.getPrefItem()); 195 }); 196 preferencesArray.sort((PreferencesCatItems o1, PreferencesCatItems o2) -> { 197 int comparison = Integer.compare(o1.sortOrder, o2.sortOrder); 198 return (comparison != 0) ? comparison : o1.getPrefItem().compareTo(o2.getPrefItem()); 199 }); 200 201 updateJList(); 202 add(buttonpanel); 203 add(new JSeparator(JSeparator.VERTICAL)); 204 add(detailpanel); 205 206 list.setSelectedIndex(0); 207 selection(preferencesArray.get(0).getPrefItem()); 208 } 209 210 // package only - for TabbedPreferencesFrame 211 boolean isDirty() { 212 // if not for the debug statements, this method could be the one line: 213 // return this.getPreferencesPanels().values.stream().anyMatch((panel) -> (panel.isDirty())); 214 return this.getPreferencesPanels().values().stream().map((panel) -> { 215 // wrapped in isDebugEnabled test to prevent overhead of assembling message 216 if (log.isDebugEnabled()) { 217 log.debug("PreferencesPanel {} ({}) is {}.", 218 panel.getClass().getName(), 219 (panel.getTabbedPreferencesTitle() != null) ? panel.getTabbedPreferencesTitle() : panel.getPreferencesItemText(), 220 (panel.isDirty()) ? "dirty" : "clean"); 221 } 222 return panel; 223 }).anyMatch((panel) -> (panel.isDirty())); 224 } 225 226 // package only - for TabbedPreferencesFrame 227 boolean invokeSaveOptions() { 228 boolean restartRequired = false; 229 for (PreferencesPanel panel : this.getPreferencesPanels().values()) { 230 // wrapped in isDebugEnabled test to prevent overhead of assembling message 231 if (log.isDebugEnabled()) { 232 log.debug("PreferencesPanel {} ({}) is {}.", 233 panel.getClass().getName(), 234 (panel.getTabbedPreferencesTitle() != null) ? panel.getTabbedPreferencesTitle() : panel.getPreferencesItemText(), 235 (panel.isDirty()) ? "dirty" : "clean"); 236 } 237 panel.savePreferences(); 238 // wrapped in isDebugEnabled test to prevent overhead of assembling message 239 if (log.isDebugEnabled()) { 240 log.debug("PreferencesPanel {} ({}) restart is {}required.", 241 panel.getClass().getName(), 242 (panel.getTabbedPreferencesTitle() != null) ? panel.getTabbedPreferencesTitle() : panel.getPreferencesItemText(), 243 (panel.isRestartRequired()) ? "" : "not "); 244 } 245 if (!restartRequired) { 246 restartRequired = panel.isRestartRequired(); 247 } 248 } 249 return restartRequired; 250 } 251 252 void selection(String view) { 253 CardLayout cl = (CardLayout) (detailpanel.getLayout()); 254 cl.show(detailpanel, view); 255 } 256 257 public void addPreferencesPanel(PreferencesPanel panel) { 258 this.getPreferencesPanels().put(panel.getClass().getName(), panel); 259 addItem(panel.getPreferencesItem(), 260 panel.getPreferencesItemText(), 261 panel.getTabbedPreferencesTitle(), 262 panel.getLabelKey(), 263 panel, 264 panel.getPreferencesTooltip(), 265 panel.getSortOrder() 266 ); 267 } 268 269 private void addItem(String prefItem, String itemText, String tabTitle, 270 String labelKey, PreferencesPanel item, String tooltip, int sortOrder) { 271 PreferencesCatItems itemBeingAdded = null; 272 for (PreferencesCatItems preferences : preferencesArray) { 273 if (preferences.getPrefItem().equals(prefItem)) { 274 itemBeingAdded = preferences; 275 // the lowest sort order of any panel sets the sort order for 276 // the preferences category 277 if (sortOrder < preferences.sortOrder) { 278 preferences.sortOrder = sortOrder; 279 } 280 break; 281 } 282 } 283 if (itemBeingAdded == null) { 284 itemBeingAdded = new PreferencesCatItems( 285 prefItem, itemText, sortOrder, item.getIsEnabled()); 286 preferencesArray.add(itemBeingAdded); 287 // As this is a new item in the selection list, we need to update 288 // the JList. 289 updateJList(); 290 } 291 if (tabTitle == null) { 292 tabTitle = itemText; 293 } 294 itemBeingAdded.addPreferenceItem(tabTitle, labelKey, item.getPreferencesComponent(), tooltip, sortOrder); 295 } 296 297 /* Method allows for the preference to goto a specific list item */ 298 public void gotoPreferenceItem(String selection, String subCategory) { 299 300 selection(selection); 301 list.setSelectedIndex(getCategoryIndexFromString(selection)); 302 if (subCategory == null || subCategory.isEmpty()) { 303 return; 304 } 305 preferencesArray.get(getCategoryIndexFromString(selection)) 306 .gotoSubCategory(subCategory); 307 } 308 309 /* 310 * Returns a List of existing Preference Categories. 311 */ 312 public List<String> getPreferenceMenuList() { 313 ArrayList<String> choices = new ArrayList<>(); 314 for (PreferencesCatItems preferences : preferencesArray) { 315 choices.add(preferences.getPrefItem()); 316 } 317 return choices; 318 } 319 320 /* 321 * Returns a list of Sub Category Items for a give category 322 */ 323 public List<String> getPreferenceSubCategory(String category) { 324 int index = getCategoryIndexFromString(category); 325 return preferencesArray.get(index).getSubCategoriesList(); 326 } 327 328 int getCategoryIndexFromString(String category) { 329 for (int x = 0; x < preferencesArray.size(); x++) { 330 if (preferencesArray.get(x).getPrefItem().equals(category)) { 331 return (x); 332 } 333 } 334 return -1; 335 } 336 337 public void disablePreferenceItem(String selection, String subCategory) { 338 if (subCategory == null || subCategory.isEmpty()) { 339 // need to do something here like just disable the item 340 341 } else { 342 preferencesArray.get(getCategoryIndexFromString(selection)) 343 .disableSubCategory(subCategory); 344 } 345 } 346 347 protected ArrayList<String> getChoices() { 348 ArrayList<String> choices = new ArrayList<>(); 349 for (PreferencesCatItems preferences : preferencesArray) { 350 choices.add(preferences.getItemString()); 351 } 352 return choices; 353 } 354 355 void updateJList() { 356 buttonpanel.removeAll(); 357 if (list.getListSelectionListeners().length > 0) { 358 list.removeListSelectionListener(list.getListSelectionListeners()[0]); 359 } 360 List<String> choices = this.getChoices(); 361 list = new JList<>(choices.toArray(new String[choices.size()])); 362 listScroller = new JScrollPane(list); 363 listScroller.setPreferredSize(new Dimension(100, 100)); 364 365 list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); 366 list.setLayoutOrientation(JList.VERTICAL); 367 ReferenceNotNull<Integer> lastSelection = new ReferenceNotNull<>(-1); 368 list.addListSelectionListener((ListSelectionEvent e) -> { 369 PreferencesCatItems item = preferencesArray.get(list.getSelectedIndex()); 370 String newSelection = item.getPrefItem(); 371 372 BooleanSupplier getIsEnabled = item.getIsEnabled; 373 if (list.getSelectedIndex() != lastSelection.get() 374 && getIsEnabled != null 375 && !getIsEnabled.getAsBoolean()) { 376 // The new selection is currently disabled 377 // so return to previous selection 378 list.setSelectedIndex(lastSelection.get()); 379 } else { 380 lastSelection.set(list.getSelectedIndex()); 381 selection(newSelection); 382 } 383 }); 384 buttonpanel.add(listScroller); 385 buttonpanel.add(save); 386 } 387 388 public boolean isPreferencesValid() { 389 return this.getPreferencesPanels().values().stream().allMatch((panel) -> (panel.isPreferencesValid())); 390 } 391 392 @Override 393 public void savePressed(boolean restartRequired) { 394 ShutDownManager sdm = InstanceManager.getDefault(ShutDownManager.class); 395 if (!this.isPreferencesValid() && !sdm.isShuttingDown()) { 396 for (PreferencesPanel panel : this.getPreferencesPanels().values()) { 397 if (!panel.isPreferencesValid()) { 398 switch (JmriJOptionPane.showConfirmDialog(this, 399 Bundle.getMessage("InvalidPreferencesMessage", panel.getTabbedPreferencesTitle()), 400 Bundle.getMessage("InvalidPreferencesTitle"), 401 JmriJOptionPane.YES_NO_OPTION, 402 JmriJOptionPane.ERROR_MESSAGE)) { 403 case JmriJOptionPane.YES_OPTION: 404 // abort save and return to broken preferences 405 this.gotoPreferenceItem(panel.getPreferencesItem(), panel.getTabbedPreferencesTitle()); 406 return; 407 default: 408 // do nothing 409 break; 410 } 411 } 412 } 413 } 414 super.savePressed(restartRequired); 415 } 416 417 static class PreferencesCatItems implements java.io.Serializable { 418 419 /* 420 * This contains details of all list managedPreferences to be displayed in the 421 * preferences 422 */ 423 String itemText; 424 String prefItem; 425 int sortOrder = Integer.MAX_VALUE; 426 BooleanSupplier getIsEnabled; 427 JTabbedPane tabbedPane = new JTabbedPane(); 428 ArrayList<String> disableItemsList = new ArrayList<>(); 429 430 private final ArrayList<TabDetails> tabDetailsArray = new ArrayList<>(); 431 432 PreferencesCatItems(String pref, String title, int sortOrder) { 433 prefItem = pref; 434 itemText = title; 435 this.sortOrder = sortOrder; 436 } 437 438 PreferencesCatItems(String pref, String title, int sortOrder, BooleanSupplier getIsEnabled) { 439 prefItem = pref; 440 itemText = title; 441 this.sortOrder = sortOrder; 442 this.getIsEnabled = getIsEnabled; 443 } 444 445 void addPreferenceItem(String title, String labelkey, JComponent item, 446 String tooltip, int sortOrder) { 447 for (TabDetails tabDetails : tabDetailsArray) { 448 if (tabDetails.getTitle().equals(title)) { 449 // If we have a match then we do not need to add it back in. 450 return; 451 } 452 } 453 TabDetails tab = new TabDetails(labelkey, title, item, tooltip, sortOrder); 454 tabDetailsArray.add(tab); 455 tabDetailsArray.sort((TabDetails o1, TabDetails o2) -> { 456 int comparison = Integer.compare(o1.sortOrder, o2.sortOrder); 457 return (comparison != 0) ? comparison : o1.tabTitle.compareTo(o2.tabTitle); 458 }); 459 JScrollPane scroller = new JScrollPane(tab.getPanel()); 460 scroller.setBorder(BorderFactory.createEmptyBorder()); 461 ThreadingUtil.runOnGUI(() -> { 462 463 tabbedPane.addTab(tab.getTitle(), null, scroller, tab.getToolTip()); 464 465 for (String disableItem : disableItemsList) { 466 if (item.getClass().getName().equals(disableItem)) { 467 tabbedPane.setEnabledAt(tabbedPane.indexOfTab(tab.getTitle()), false); 468 return; 469 } 470 } 471 }); 472 } 473 474 String getPrefItem() { 475 return prefItem; 476 } 477 478 String getItemString() { 479 return itemText; 480 } 481 482 ArrayList<String> getSubCategoriesList() { 483 ArrayList<String> choices = new ArrayList<>(); 484 for (TabDetails tabDetails : tabDetailsArray) { 485 choices.add(tabDetails.getTitle()); 486 } 487 return choices; 488 } 489 490 /* 491 * This returns a JPanel if only one item is configured for a menu item 492 * or it returns a JTabbedFrame if there are multiple managedPreferences for the menu 493 */ 494 JComponent getPanel() { 495 if (tabDetailsArray.size() == 1) { 496 return tabDetailsArray.get(0).getPanel(); 497 } else { 498 if (tabbedPane.getTabCount() == 0) { 499 for (TabDetails tab : tabDetailsArray) { 500 ThreadingUtil.runOnGUI(() -> { 501 JScrollPane scroller = new JScrollPane(tab.getPanel()); 502 scroller.setBorder(BorderFactory.createEmptyBorder()); 503 504 tabbedPane.addTab(tab.getTitle(), null, scroller, tab.getToolTip()); 505 506 for (String disableItem : disableItemsList) { 507 if (tab.getItem().getClass().getName().equals(disableItem)) { 508 tabbedPane.setEnabledAt(tabbedPane.indexOfTab(tab.getTitle()), false); 509 return; 510 } 511 } 512 }); 513 } 514 } 515 return tabbedPane; 516 } 517 } 518 519 void gotoSubCategory(String sub) { 520 if (tabDetailsArray.size() == 1) { 521 return; 522 } 523 for (int i = 0; i < tabDetailsArray.size(); i++) { 524 if (tabDetailsArray.get(i).getTitle().equals(sub)) { 525 tabbedPane.setSelectedIndex(i); 526 return; 527 } 528 } 529 } 530 531 void disableSubCategory(String sub) { 532 if (tabDetailsArray.isEmpty()) { 533 // So the tab preferences might not have been initialised when 534 // the call to disable an item is called therefore store it for 535 // later on 536 disableItemsList.add(sub); 537 return; 538 } 539 for (int i = 0; i < tabDetailsArray.size(); i++) { 540 if ((tabDetailsArray.get(i).getItem()).getClass().getName() 541 .equals(sub)) { 542 tabbedPane.setEnabledAt(i, false); 543 return; 544 } 545 } 546 } 547 548 static class TabDetails implements java.io.Serializable { 549 550 /* This contains all the JPanels that make up a preferences menus */ 551 JComponent tabItem; 552 String tabTooltip; 553 String tabTitle; 554 JPanel tabPanel = new JPanel(); 555 private final int sortOrder; 556 557 TabDetails(String labelkey, String tabTit, JComponent item, 558 String tooltip, int sortOrder) { 559 tabItem = item; 560 tabTitle = tabTit; 561 tabTooltip = tooltip; 562 this.sortOrder = sortOrder; 563 564 JComponent p = new JPanel(); 565 p.setLayout(new BorderLayout()); 566 if (labelkey != null) { 567 // insert label at top 568 // As this can be multi-line, embed the text within <html> 569 // tags and replace newlines with <br> tag 570 JLabel t = new JLabel("<html>" 571 + labelkey.replace(String.valueOf('\n'), "<br>") 572 + "</html>"); 573 t.setHorizontalAlignment(JLabel.CENTER); 574 t.setAlignmentX(0.5f); 575 t.setPreferredSize(t.getMinimumSize()); 576 t.setMaximumSize(t.getMinimumSize()); 577 t.setOpaque(false); 578 p.add(t, BorderLayout.NORTH); 579 } 580 p.add(item, BorderLayout.CENTER); 581 ThreadingUtil.runOnGUI(() -> { 582 tabPanel.setLayout(new BorderLayout()); 583 tabPanel.add(p, BorderLayout.CENTER); 584 }); 585 } 586 587 String getToolTip() { 588 return tabTooltip; 589 } 590 591 String getTitle() { 592 return tabTitle; 593 } 594 595 JPanel getPanel() { 596 return tabPanel; 597 } 598 599 JComponent getItem() { 600 return tabItem; 601 } 602 603 int getSortOrder() { 604 return sortOrder; 605 } 606 } 607 } 608 609 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TabbedPreferences.class); 610 611}