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}