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