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}