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}