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