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