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.List;
011import javax.swing.BorderFactory;
012import javax.swing.BoxLayout;
013import javax.swing.ImageIcon;
014import javax.swing.JButton;
015import javax.swing.JComponent;
016import javax.swing.JLabel;
017import javax.swing.JList;
018import javax.swing.JOptionPane;
019import javax.swing.JPanel;
020import javax.swing.JScrollPane;
021import javax.swing.JSeparator;
022import javax.swing.JTabbedPane;
023import javax.swing.ListSelectionModel;
024import javax.swing.event.ListSelectionEvent;
025import jmri.InstanceManager;
026import jmri.ShutDownManager;
027import jmri.swing.PreferencesPanel;
028import jmri.util.FileUtil;
029import jmri.util.ThreadingUtil;
030import org.jdom2.Element;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034/**
035 * Provide access to the connection preferences via a tabbed pane.
036 *
037 * @author Bob Jacobsen Copyright 2010, 2019
038 * @author Randall Wood 2012, 2016
039 */
040public class EditConnectionPreferences extends AppConfigBase {
041
042    @Override
043    public String getHelpTarget() {
044        return "package.apps.TabbedPreferences";
045    }
046
047    @Override
048    public String getTitle() {
049        return Bundle.getMessage("TitlePreferences");
050    }
051    // Preferences Window Title
052
053    @Override
054    public boolean isMultipleInstances() {
055        return false;
056    } // only one of these!
057
058    ArrayList<Element> preferencesElements = new ArrayList<>();
059
060    JPanel detailpanel = new JPanel();
061    { 
062        // The default panel needs to have a CardLayout
063        detailpanel.setLayout(new CardLayout());
064    }
065
066    /**
067     * The dialog that displays the preferences.
068     * Used by the quit button to dispose the dialog.
069     */
070    final EditConnectionPreferencesDialog dialog;
071    
072    ArrayList<PreferencesCatItems> preferencesArray = new ArrayList<>();
073    JPanel buttonpanel;
074    JList<String> list;
075    JButton save;
076    JButton quit = null;
077    JScrollPane listScroller;
078
079    public EditConnectionPreferences(EditConnectionPreferencesDialog dialog) {
080
081        this.dialog = dialog;
082        
083        /*
084         * Adds the place holders for the menu managedPreferences so that any managedPreferences added by
085         * third party code is added to the end
086         */
087        preferencesArray.add(new PreferencesCatItems("CONNECTIONS", rb
088                .getString("MenuConnections"), 100));
089    }
090
091    public void init() {
092        list = new JList<>();
093        listScroller = new JScrollPane(list);
094        listScroller.setPreferredSize(new Dimension(100, 100));
095
096        buttonpanel = new JPanel();
097        buttonpanel.setLayout(new BoxLayout(buttonpanel, BoxLayout.Y_AXIS));
098        buttonpanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 3));
099
100        detailpanel = new JPanel();
101        detailpanel.setLayout(new CardLayout());
102        detailpanel.setBorder(BorderFactory.createEmptyBorder(6, 3, 6, 6));
103
104        save = new JButton(
105                ConfigBundle.getMessage("ButtonSave"),
106                new ImageIcon(FileUtil.findURL("program:resources/icons/misc/gui3/SaveIcon.png", FileUtil.Location.INSTALLED)));
107        save.addActionListener((ActionEvent e) -> {
108            dialog.restartProgram = true;
109            savePressed(invokeSaveOptions());
110        });
111
112        quit = new JButton(
113                ConfigBundle.getMessage("ButtonQuit"));
114//                new ImageIcon(FileUtil.findURL("program:resources/icons/misc/gui3/SaveIcon.png", FileUtil.Location.INSTALLED)));
115        quit.addActionListener((ActionEvent e) -> {
116            if (dialog != null) {
117                dialog.restartProgram = false;
118                dialog.dispose();
119            }
120        });
121
122        setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
123        getTabbedPreferences().preferencesArray.stream().forEach((preferences) -> {
124            detailpanel.add(preferences.getPanel(), preferences.getPrefItem());
125        });
126
127        updateJList();
128        add(buttonpanel);
129        add(new JSeparator(JSeparator.VERTICAL));
130        add(detailpanel);
131
132        list.setSelectedIndex(0);
133        selection(preferencesArray.get(0).getPrefItem());
134    }
135
136    // package only - for EditConnectionPreferencesDialog
137    boolean isDirty() {
138        // if not for the debug statements, this method could be the one line:
139        // return this.getPreferencesPanels().values.stream().anyMatch((panel) -> (panel.isDirty()));
140        return this.getPreferencesPanels().values().stream().map((panel) -> {
141            // wrapped in isDebugEnabled test to prevent overhead of assembling message
142            if (log.isDebugEnabled()) {
143                log.debug("PreferencesPanel {} ({}) is {}.",
144                        panel.getClass().getName(),
145                        (panel.getTabbedPreferencesTitle() != null) ? panel.getTabbedPreferencesTitle() : panel.getPreferencesItemText(),
146                        (panel.isDirty()) ? "dirty" : "clean");
147            }
148            return panel;
149        }).anyMatch((panel) -> (panel.isDirty()));
150    }
151
152    // package only - for EditConnectionPreferencesDialog
153    boolean invokeSaveOptions() {
154        boolean restartRequired = false;
155        for (PreferencesPanel panel : this.getPreferencesPanels().values()) {
156            // wrapped in isDebugEnabled test to prevent overhead of assembling message
157            if (log.isDebugEnabled()) {
158                log.debug("PreferencesPanel {} ({}) is {}.",
159                        panel.getClass().getName(),
160                        (panel.getTabbedPreferencesTitle() != null) ? panel.getTabbedPreferencesTitle() : panel.getPreferencesItemText(),
161                        (panel.isDirty()) ? "dirty" : "clean");
162            }
163            panel.savePreferences();
164            // wrapped in isDebugEnabled test to prevent overhead of assembling message
165            if (log.isDebugEnabled()) {
166                log.debug("PreferencesPanel {} ({}) restart is {}required.",
167                        panel.getClass().getName(),
168                        (panel.getTabbedPreferencesTitle() != null) ? panel.getTabbedPreferencesTitle() : panel.getPreferencesItemText(),
169                        (panel.isRestartRequired()) ? "" : "not ");
170            }
171            if (!restartRequired) {
172                restartRequired = panel.isRestartRequired();
173            }
174        }
175        return restartRequired;
176    }
177
178    void selection(String view) {
179        CardLayout cl = (CardLayout) (detailpanel.getLayout());
180        cl.show(detailpanel, view);
181    }
182
183    public void addPreferencesPanel(PreferencesPanel panel) {
184        this.getPreferencesPanels().put(panel.getClass().getName(), panel);
185        addItem(panel.getPreferencesItem(),
186                panel.getPreferencesItemText(),
187                panel.getTabbedPreferencesTitle(),
188                panel.getLabelKey(),
189                panel,
190                panel.getPreferencesTooltip(),
191                panel.getSortOrder()
192        );
193    }
194
195    private void addItem(String prefItem, String itemText, String tabTitle,
196            String labelKey, PreferencesPanel item, String tooltip, int sortOrder) {
197        PreferencesCatItems itemBeingAdded = null;
198        for (PreferencesCatItems preferences : preferencesArray) {
199            if (preferences.getPrefItem().equals(prefItem)) {
200                itemBeingAdded = preferences;
201                // the lowest sort order of any panel sets the sort order for
202                // the preferences category
203                if (sortOrder < preferences.sortOrder) {
204                    preferences.sortOrder = sortOrder;
205                }
206                break;
207            }
208        }
209        if (itemBeingAdded == null) {
210            itemBeingAdded = new PreferencesCatItems(prefItem, itemText, sortOrder);
211            preferencesArray.add(itemBeingAdded);
212            // As this is a new item in the selection list, we need to update
213            // the JList.
214            updateJList();
215        }
216        if (tabTitle == null) {
217            tabTitle = itemText;
218        }
219        itemBeingAdded.addPreferenceItem(tabTitle, labelKey, item.getPreferencesComponent(), tooltip, sortOrder);
220    }
221
222    /* Method allows for the preference to goto a specific list item */
223    public void gotoPreferenceItem(String selection, String subCategory) {
224
225        selection(selection);
226        list.setSelectedIndex(getCategoryIndexFromString(selection));
227        if (subCategory == null || subCategory.isEmpty()) {
228            return;
229        }
230        preferencesArray.get(getCategoryIndexFromString(selection))
231                .gotoSubCategory(subCategory);
232    }
233
234    /*
235     * Returns a List of existing Preference Categories.
236     */
237    public List<String> getPreferenceMenuList() {
238        ArrayList<String> choices = new ArrayList<>();
239        for (PreferencesCatItems preferences : preferencesArray) {
240            choices.add(preferences.getPrefItem());
241        }
242        return choices;
243    }
244
245    int getCategoryIndexFromString(String category) {
246        for (int x = 0; x < preferencesArray.size(); x++) {
247            if (preferencesArray.get(x).getPrefItem().equals(category)) {
248                return (x);
249            }
250        }
251        return -1;
252    }
253
254    protected ArrayList<String> getChoices() {
255        ArrayList<String> choices = new ArrayList<>();
256        for (PreferencesCatItems preferences : preferencesArray) {
257            choices.add(preferences.getItemString());
258        }
259        return choices;
260    }
261
262    void updateJList() {
263        buttonpanel.removeAll();
264        if (list.getListSelectionListeners().length > 0) {
265            list.removeListSelectionListener(list.getListSelectionListeners()[0]);
266        }
267        List<String> choices = this.getChoices();
268        list = new JList<>(choices.toArray(new String[choices.size()]));
269        listScroller = new JScrollPane(list);
270        listScroller.setPreferredSize(new Dimension(100, 100));
271
272        list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
273        list.setLayoutOrientation(JList.VERTICAL);
274        list.addListSelectionListener((ListSelectionEvent e) -> {
275            PreferencesCatItems item = preferencesArray.get(list.getSelectedIndex());
276            selection(item.getPrefItem());
277        });
278        buttonpanel.add(listScroller);
279        buttonpanel.add(save);
280        
281        if (quit != null) {
282            buttonpanel.add(quit);
283        }
284    }
285
286    public boolean isPreferencesValid() {
287        return this.getPreferencesPanels().values().stream().allMatch((panel) -> (panel.isPreferencesValid()));
288    }
289
290    @Override
291    public void savePressed(boolean restartRequired) {
292        ShutDownManager sdm = InstanceManager.getDefault(ShutDownManager.class);
293        if (!this.isPreferencesValid() && !sdm.isShuttingDown()) {
294            for (PreferencesPanel panel : this.getPreferencesPanels().values()) {
295                if (!panel.isPreferencesValid()) {
296                    switch (JOptionPane.showConfirmDialog(this,
297                            Bundle.getMessage("InvalidPreferencesMessage", panel.getTabbedPreferencesTitle()),
298                            Bundle.getMessage("InvalidPreferencesTitle"),
299                            JOptionPane.YES_NO_OPTION,
300                            JOptionPane.ERROR_MESSAGE)) {
301                        case JOptionPane.YES_OPTION:
302                            // abort save and return to broken preferences
303                            this.gotoPreferenceItem(panel.getPreferencesItem(), panel.getTabbedPreferencesTitle());
304                            return;
305                        default:
306                            // do nothing
307                            break;
308                    }
309                }
310            }
311        }
312        super.savePressed(restartRequired);
313    }
314
315    static class PreferencesCatItems implements java.io.Serializable {
316
317        /*
318         * This contains details of all list managedPreferences to be displayed in the
319         * preferences
320         */
321        String itemText;
322        String prefItem;
323        int sortOrder = Integer.MAX_VALUE;
324        JTabbedPane tabbedPane = new JTabbedPane();
325        ArrayList<String> disableItemsList = new ArrayList<>();
326
327        private final ArrayList<TabDetails> tabDetailsArray = new ArrayList<>();
328
329        PreferencesCatItems(String pref, String title, int sortOrder) {
330            prefItem = pref;
331            itemText = title;
332            this.sortOrder = sortOrder;
333        }
334
335        void addPreferenceItem(String title, String labelkey, JComponent item,
336                String tooltip, int sortOrder) {
337            for (TabDetails tabDetails : tabDetailsArray) {
338                if (tabDetails.getTitle().equals(title)) {
339                    // If we have a match then we do not need to add it back in.
340                    return;
341                }
342            }
343            TabDetails tab = new TabDetails(labelkey, title, item, tooltip, sortOrder);
344            tabDetailsArray.add(tab);
345            tabDetailsArray.sort((TabDetails o1, TabDetails o2) -> {
346                int comparison = Integer.compare(o1.sortOrder, o2.sortOrder);
347                return (comparison != 0) ? comparison : o1.tabTitle.compareTo(o2.tabTitle);
348            });
349            JScrollPane scroller = new JScrollPane(tab.getPanel());
350            scroller.setBorder(BorderFactory.createEmptyBorder());
351            ThreadingUtil.runOnGUI(() -> {
352
353                tabbedPane.addTab(tab.getTitle(), null, scroller, tab.getToolTip());
354
355                for (String disableItem : disableItemsList) {
356                    if (item.getClass().getName().equals(disableItem)) {
357                        tabbedPane.setEnabledAt(tabbedPane.indexOfTab(tab.getTitle()), false);
358                        return;
359                    }
360                }
361            });
362        }
363
364        String getPrefItem() {
365            return prefItem;
366        }
367
368        String getItemString() {
369            return itemText;
370        }
371
372        /*
373         * This returns a JPanel if only one item is configured for a menu item
374         * or it returns a JTabbedFrame if there are multiple managedPreferences for the menu
375         */
376        JComponent getPanel() {
377            if (tabDetailsArray.size() == 1) {
378                return tabDetailsArray.get(0).getPanel();
379            } else {
380                if (tabbedPane.getTabCount() == 0) {
381                    for (TabDetails tab : tabDetailsArray) {
382                        ThreadingUtil.runOnGUI(() -> {
383                            JScrollPane scroller = new JScrollPane(tab.getPanel());
384                            scroller.setBorder(BorderFactory.createEmptyBorder());
385
386                            tabbedPane.addTab(tab.getTitle(), null, scroller, tab.getToolTip());
387
388                            for (String disableItem : disableItemsList) {
389                                if (tab.getItem().getClass().getName().equals(disableItem)) {
390                                    tabbedPane.setEnabledAt(tabbedPane.indexOfTab(tab.getTitle()), false);
391                                    return;
392                                }
393                            }
394                        });
395                    }
396                }
397                return tabbedPane;
398            }
399        }
400
401        void gotoSubCategory(String sub) {
402            if (tabDetailsArray.size() == 1) {
403                return;
404            }
405            for (int i = 0; i < tabDetailsArray.size(); i++) {
406                if (tabDetailsArray.get(i).getTitle().equals(sub)) {
407                    tabbedPane.setSelectedIndex(i);
408                    return;
409                }
410            }
411        }
412
413        static class TabDetails implements java.io.Serializable {
414
415            /* This contains all the JPanels that make up a preferences menus */
416            JComponent tabItem;
417            String tabTooltip;
418            String tabTitle;
419            JPanel tabPanel = new JPanel();
420            private final int sortOrder;
421
422            TabDetails(String labelkey, String tabTit, JComponent item,
423                    String tooltip, int sortOrder) {
424                tabItem = item;
425                tabTitle = tabTit;
426                tabTooltip = tooltip;
427                this.sortOrder = sortOrder;
428
429                JComponent p = new JPanel();
430                p.setLayout(new BorderLayout());
431                if (labelkey != null) {
432                    // insert label at top
433                    // As this can be multi-line, embed the text within <html>
434                    // tags and replace newlines with <br> tag
435                    JLabel t = new JLabel("<html>"
436                            + labelkey.replace(String.valueOf('\n'), "<br>")
437                            + "</html>");
438                    t.setHorizontalAlignment(JLabel.CENTER);
439                    t.setAlignmentX(0.5f);
440                    t.setPreferredSize(t.getMinimumSize());
441                    t.setMaximumSize(t.getMinimumSize());
442                    t.setOpaque(false);
443                    p.add(t, BorderLayout.NORTH);
444                }
445                p.add(item, BorderLayout.CENTER);
446                ThreadingUtil.runOnGUI(() -> {
447                    tabPanel.setLayout(new BorderLayout());
448                    tabPanel.add(p, BorderLayout.CENTER);
449                });
450            }
451
452            String getToolTip() {
453                return tabTooltip;
454            }
455
456            String getTitle() {
457                return tabTitle;
458            }
459
460            JPanel getPanel() {
461                return tabPanel;
462            }
463
464            JComponent getItem() {
465                return tabItem;
466            }
467
468            int getSortOrder() {
469                return sortOrder;
470            }
471        }
472    }
473
474    /**
475     * Ensure a TabbedPreferences instance is always available.
476     *
477     * @return the default TabbedPreferences instance, creating it if needed
478     */
479    private TabbedPreferences getTabbedPreferences() {
480        return InstanceManager.getOptionalDefault(TabbedPreferences.class).orElseGet(() -> {
481            return InstanceManager.setDefault(TabbedPreferences.class, new TabbedPreferences());
482        });
483    }
484    
485    private final static Logger log = LoggerFactory.getLogger(EditConnectionPreferences.class);
486
487}