001package jmri.managers;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.Component;
006import java.awt.Dimension;
007import java.awt.Point;
008import java.awt.Toolkit;
009import java.io.File;
010import java.io.FileNotFoundException;
011import java.lang.reflect.Constructor;
012import java.lang.reflect.InvocationTargetException;
013import java.lang.reflect.Method;
014import java.util.ArrayList;
015import java.util.HashMap;
016import java.util.HashSet;
017import java.util.Map.Entry;
018import java.util.Set;
019import java.util.concurrent.ConcurrentHashMap;
020import javax.annotation.Nonnull;
021import javax.annotation.CheckForNull;
022import javax.swing.BoxLayout;
023import javax.swing.JCheckBox;
024import javax.swing.JLabel;
025import javax.swing.JPanel;
026import jmri.ConfigureManager;
027import jmri.InstanceInitializer;
028import jmri.InstanceManager;
029import jmri.InstanceManagerAutoInitialize;
030import jmri.JmriException;
031import jmri.UserPreferencesManager;
032import jmri.beans.Bean;
033import jmri.implementation.AbstractInstanceInitializer;
034import jmri.profile.Profile;
035import jmri.profile.ProfileManager;
036import jmri.profile.ProfileUtils;
037import jmri.swing.JmriJTablePersistenceManager;
038import jmri.util.FileUtil;
039import jmri.util.JmriJFrame;
040import jmri.util.jdom.JDOMUtil;
041import jmri.util.node.NodeIdentity;
042import jmri.util.swing.JmriJOptionPane;
043import org.jdom2.DataConversionException;
044import org.jdom2.Element;
045import org.jdom2.JDOMException;
046import org.openide.util.lookup.ServiceProvider;
047
048/**
049 * Implementation of {@link UserPreferencesManager} that saves user interface
050 * preferences that should be automatically remembered as they are set.
051 * <p>This class is intended to be a transitional class from a single user
052 * interface preferences manager to multiple, domain-specific (windows, tables,
053 * dialogs, etc) user interface preferences managers. Domain-specific managers
054 * can more efficiently, both in the API and at runtime, handle each user
055 * interface preference need than a single monolithic manager.</p>
056 *
057 * <p>The following items are available.  Each item has its own section in the
058 * <b>user-interface.xml</b> file.</p>
059 *
060 * <dl>
061 *   <dt><b>Class Preferences</b></dt>
062 *   <dd>This contains reminders and selections from dialogs displayed to users.  These are normally
063 *      related to the JMRI NamedBeans represented by the various PanelPro tables. The
064 *      responses are shown in <b>Preferences -&gt; Messages</b>.  This provides the ability to
065 *      revert previous choices.  See {@link jmri.jmrit.beantable.usermessagepreferences.UserMessagePreferencesPane}
066 *
067 *      <p>The dialogs are invoked by the various <b>show&lt;Info|Warning|Error&gt;Message</b> dialogs.
068 *
069 *      There are two types of messages created by the dialogs.</p>
070 *      <dl>
071 *        <dt><b>multipleChoice</b></dt>
072 *        <dd>The multiple choice message has a keyword and the selected option. It only exists when the
073 *          selected option index is greater than zero.</dd>
074 *
075 *        <dt><b>reminderPrompts</b></dt>
076 *        <dd>The reminder prompt message has a keyword, such as <i>remindSaveRoute</i>.  It only exists when
077 *          the reminder is active.</dd>
078 *      </dl>
079 *
080 *      <p>When the <i>Skip message in future?</i> or <i>Remember this setting for next time?</i> is selected,
081 *      an entry will be added.  The {@link #setClassDescription(String)} method will use Java reflection
082 *      to request additional information from the class that was used to the show dialog.  This requires some
083 *      specific changes to the originating class.</p>
084 *
085 *      <dl>
086 *        <dt><b>Class Constructor</b></dt>
087 *        <dd>A constructor without parameters is required.  This is used to get the class so that
088 *        the following public methods can be invoked.</dd>
089 *
090 *        <dt><b>getClassDescription()</b></dt>
091 *        <dd>This returns a string that will be used by <b>Preferences -&gt; Messages</b>.</dd>
092 *
093 *        <dt><b>setMessagePreferenceDetails()</b></dt>
094 *        <dd>This does not return anything directly.  It makes call backs using two methods.
095 *          <dl>
096 *            <dt>{@link #setMessageItemDetails(String, String, String, HashMap, int)}</dt>
097 *            <dd>Descriptive information, the items for a combo box and the current selection are sent.
098 *            This information is used to create the <b>multipleChoice</b> item.</dd>
099 *
100 *            <dt>{@link #setPreferenceItemDetails(String, String, String)}</dt>
101 *            <dd>Descriptive information is sent to create the <b>reminderPrompt</b> item.</dd>
102 *          </dl>
103 *        </dd>
104 *      </dl>
105 *      <p>The messages are normally created by the various NamedBean classes.  LogixNG uses a
106 *      separate class instead of changing each affected class.  This provides a concise example
107 *      of the required changes at
108 * <a href="https://github.com/JMRI/JMRI/blob/master/java/src/jmri/jmrit/logixng/LogixNG_UserPreferences.java">LogixNG_UserPreferences</a></p>
109 *   </dd>
110 *
111 *   <dt><b>Checkbox State</b></dt>
112 *   <dd>Contains the last checkbox state.<br>Methods:
113 *     <ul>
114 *       <li>{@link #getCheckboxPreferenceState(String, boolean)}</li>
115 *       <li>{@link #setCheckboxPreferenceState(String, boolean)}</li>
116 *     </ul>
117 *   </dd>
118 *
119 *   <dt><b>Combobox Selection</b></dt>
120 *   <dd>Contains the last combo box selection.<br>Methods:
121 *     <ul>
122 *       <li>{@link #getComboBoxLastSelection(String)}</li>
123 *       <li>{@link #setComboBoxLastSelection(String, String)}</li>
124 *     </ul>
125 *   </dd>
126 *
127 *   <dt><b>Settings</b></dt>
128 *   <dd>The existence of an entry indicates a true state.<br>Methods:
129 *     <ul>
130 *       <li>{@link #getSimplePreferenceState(String)}</li>
131 *       <li>{@link #setSimplePreferenceState(String, boolean)}</li>
132 *     </ul>
133 *   </dd>
134 *
135 *   <dt><b>Window Details</b></dt>
136 *   <dd>The main data is the window location and size.  This is handled by
137 *     {@link jmri.util.JmriJFrame}.  The window details can also include
138 *     window specific properties.<br>Methods:
139 *     <ul>
140 *       <li>{@link #getProperty(String, String)}</li>
141 *       <li>{@link #setProperty(String, String, Object)}</li>
142 *     </ul>
143 *   </dd>
144 * </dl>
145 *
146 *
147 *
148 * @author Randall Wood (C) 2016
149 */
150public class JmriUserPreferencesManager extends Bean implements UserPreferencesManager, InstanceManagerAutoInitialize {
151
152    public static final String SAVE_ALLOWED = "saveAllowed";
153
154    private static final String CLASSPREFS_NAMESPACE = "http://jmri.org/xml/schema/auxiliary-configuration/class-preferences-4-3-5.xsd"; // NOI18N
155    private static final String CLASSPREFS_ELEMENT = "classPreferences"; // NOI18N
156    private static final String COMBOBOX_NAMESPACE = "http://jmri.org/xml/schema/auxiliary-configuration/combobox-4-3-5.xsd"; // NOI18N
157    private static final String COMBOBOX_ELEMENT = "comboBoxLastValue"; // NOI18N
158    private static final String CHECKBOX_NAMESPACE = "http://jmri.org/xml/schema/auxiliary-configuration/checkbox-4-21-3.xsd"; // NOI18N
159    private static final String CHECKBOX_ELEMENT = "checkBoxLastValue"; // NOI18N
160    private static final String SETTINGS_NAMESPACE = "http://jmri.org/xml/schema/auxiliary-configuration/settings-4-3-5.xsd"; // NOI18N
161    private static final String SETTINGS_ELEMENT = "settings"; // NOI18N
162    private static final String WINDOWS_NAMESPACE = "http://jmri.org/xml/schema/auxiliary-configuration/window-details-4-3-5.xsd"; // NOI18N
163    private static final String WINDOWS_ELEMENT = "windowDetails"; // NOI18N
164
165    private static final String REMINDER = "reminder";
166    private static final String JMRI_UTIL_JMRI_JFRAME = "jmri.util.JmriJFrame";
167    private static final String CLASS = "class";
168    private static final String VALUE = "value";
169    private static final String WIDTH = "width";
170    private static final String HEIGHT = "height";
171    private static final String PROPERTIES = "properties";
172
173    private boolean dirty = false;
174    private boolean loading = false;
175    private boolean allowSave;
176    private final ArrayList<String> simplePreferenceList = new ArrayList<>();
177    //sessionList is used for messages to be suppressed for the current JMRI session only
178    private final ArrayList<String> sessionPreferenceList = new ArrayList<>();
179    protected final HashMap<String, String> comboBoxLastSelection = new HashMap<>();
180    protected final HashMap<String, Boolean> checkBoxLastSelection = new HashMap<>();
181    private final HashMap<String, WindowLocations> windowDetails = new HashMap<>();
182    private final HashMap<String, ClassPreferences> classPreferenceList = new HashMap<>();
183    private File file;
184
185    public JmriUserPreferencesManager() {
186        // prevent attempts to write during construction
187        this.allowSave = false;
188
189        //I18N in ManagersBundle.properties (this is a checkbox on prefs tab Messages|Misc items)
190        this.setPreferenceItemDetails(getClassName(), REMINDER, Bundle.getMessage("HideReminderLocationMessage")); // NOI18N
191        //I18N in ManagersBundle.properties (this is the title of prefs tab Messages|Misc items)
192        this.classPreferenceList.get(getClassName()).setDescription(Bundle.getMessage("UserPreferences")); // NOI18N
193
194        // allow attempts to write
195        this.allowSave = true;
196        this.dirty = false;
197    }
198
199    @Override
200    public synchronized void setSaveAllowed(boolean saveAllowed) {
201        boolean old = this.allowSave;
202        this.allowSave = saveAllowed;
203        if (saveAllowed && this.dirty) {
204            this.savePreferences();
205        }
206        this.firePropertyChange(SAVE_ALLOWED, old, this.allowSave);
207    }
208
209    @Override
210    public synchronized boolean isSaveAllowed() {
211        return this.allowSave;
212    }
213
214    @Override
215    public Dimension getScreen() {
216        return Toolkit.getDefaultToolkit().getScreenSize();
217    }
218
219    /**
220     * This is used to remember the last selected state of a checkBox and thus
221     * allow that checkBox to be set to a true state when it is next
222     * initialized. This can also be used anywhere else that a simple yes/no,
223     * true/false type preference needs to be stored.
224     * <p>
225     * It should not be used for remembering if a user wants to suppress a
226     * message as there is no means in the GUI for the user to reset the flag.
227     * setPreferenceState() should be used in this instance The name is
228     * free-form, but to avoid ambiguity it should start with the package name
229     * (package.Class) for the primary using class.
230     *
231     * @param name  A unique name to identify the state being stored
232     * @param state simple boolean.
233     */
234    @Override
235    public void setSimplePreferenceState(String name, boolean state) {
236        if (state) {
237            if (!simplePreferenceList.contains(name)) {
238                simplePreferenceList.add(name);
239            }
240        } else {
241            simplePreferenceList.remove(name);
242        }
243        this.saveSimplePreferenceState();
244    }
245
246    @Override
247    public boolean getSimplePreferenceState(String name) {
248        return simplePreferenceList.contains(name);
249    }
250
251    @Nonnull
252    @Override
253    public ArrayList<String> getSimplePreferenceStateList() {
254        return new ArrayList<>(simplePreferenceList);
255    }
256
257    /**
258     * Displays remember dialogue on save.
259     * {@inheritDoc}
260     */
261    @Override
262    public void setPreferenceState(String strClass, String item, boolean state) {
263        // convert old manager preferences to new manager preferences
264        if (strClass.equals("jmri.managers.DefaultUserMessagePreferences")) {
265            this.setPreferenceState("jmri.managers.JmriUserPreferencesManager", item, state);
266            return;
267        }
268        if (!classPreferenceList.containsKey(strClass)) {
269            classPreferenceList.put(strClass, new ClassPreferences());
270            setClassDescription(strClass);
271        }
272        ArrayList<PreferenceList> a = classPreferenceList.get(strClass).getPreferenceList();
273        boolean found = false;
274        for (int i = 0; i < a.size(); i++) {
275            if (a.get(i).getItem().equals(item)) {
276                a.get(i).setState(state);
277                found = true;
278            }
279        }
280        if (!found) {
281            a.add(new PreferenceList(item, state));
282        }
283        displayRememberMsg();
284        this.savePreferencesState();
285    }
286
287    @Override
288    public boolean getPreferenceState(String strClass, String item) {
289        if (classPreferenceList.containsKey(strClass)) {
290            ArrayList<PreferenceList> a = classPreferenceList.get(strClass).getPreferenceList();
291            for (int i = 0; i < a.size(); i++) {
292                if (a.get(i).getItem().equals(item)) {
293                    return a.get(i).getState();
294                }
295            }
296        }
297        return false;
298    }
299
300    @Override
301    public final void setPreferenceItemDetails(String strClass, String item, String description) {
302        if (!classPreferenceList.containsKey(strClass)) {
303            classPreferenceList.put(strClass, new ClassPreferences());
304        }
305        ArrayList<PreferenceList> a = classPreferenceList.get(strClass).getPreferenceList();
306        for (int i = 0; i < a.size(); i++) {
307            if (a.get(i).getItem().equals(item)) {
308                a.get(i).setDescription(description);
309                return;
310            }
311        }
312        a.add(new PreferenceList(item, description));
313    }
314
315    @Nonnull
316    @Override
317    public ArrayList<String> getPreferenceList(String strClass) {
318        if (classPreferenceList.containsKey(strClass)) {
319            ArrayList<PreferenceList> a = classPreferenceList.get(strClass).getPreferenceList();
320            ArrayList<String> list = new ArrayList<>();
321            for (int i = 0; i < a.size(); i++) {
322                list.add(a.get(i).getItem());
323            }
324            return list;
325        }
326        //Just return a blank array list will save call code checking for null
327        return new ArrayList<>();
328    }
329
330    @Override
331    @CheckForNull
332    public String getPreferenceItemName(String strClass, int n) {
333        if (classPreferenceList.containsKey(strClass)) {
334            return classPreferenceList.get(strClass).getPreferenceName(n);
335        }
336        return null;
337    }
338
339    @Override
340    @CheckForNull
341    public String getPreferenceItemDescription(String strClass, String item) {
342        if (classPreferenceList.containsKey(strClass)) {
343            ArrayList<PreferenceList> a = classPreferenceList.get(strClass).getPreferenceList();
344            for (int i = 0; i < a.size(); i++) {
345                if (a.get(i).getItem().equals(item)) {
346                    return a.get(i).getDescription();
347                }
348            }
349        }
350        return null;
351
352    }
353
354    /**
355     * Used to surpress messages for a particular session, the information is
356     * not stored, can not be changed via the GUI.
357     * <p>
358     * This can be used to help prevent over loading the user with repetitive
359     * error messages such as turnout not found while loading a panel file due
360     * to a connection failing. The name is free-form, but to avoid ambiguity it
361     * should start with the package name (package.Class) for the primary using
362     * class.
363     *
364     * @param name A unique identifier for preference.
365     */
366    @Override
367    public void setSessionPreferenceState(String name, boolean state) {
368        if (state) {
369            if (!sessionPreferenceList.contains(name)) {
370                sessionPreferenceList.add(name);
371            }
372        } else {
373            sessionPreferenceList.remove(name);
374        }
375    }
376
377    /**
378     * {@inheritDoc}
379     */
380    @Override
381    public boolean getSessionPreferenceState(String name) {
382        return sessionPreferenceList.contains(name);
383    }
384
385    /**
386     * {@inheritDoc}
387     */
388    @Override
389    public void showInfoMessage(String title, String message, String strClass, String item) {
390        showInfoMessage(title, message, strClass, item, false, true);
391    }
392
393    /**
394     * {@inheritDoc}
395     */
396    @Override
397    public void showInfoMessage(@CheckForNull Component parentComponent, String title, String message, String strClass, String item) {
398        showInfoMessage(parentComponent, title, message, strClass, item, false, true);
399    }
400
401    /**
402     * {@inheritDoc}
403     */
404    @Override
405    public void showErrorMessage(String title, String message, final String strClass, final String item, final boolean sessionOnly, final boolean alwaysRemember) {
406        this.showMessage(null, title, message, strClass, item, sessionOnly, alwaysRemember, JmriJOptionPane.ERROR_MESSAGE);
407    }
408
409    /**
410     * {@inheritDoc}
411     */
412    @Override
413    public void showErrorMessage(@CheckForNull Component parentComponent, String title, String message, final String strClass, final String item, final boolean sessionOnly, final boolean alwaysRemember) {
414        this.showMessage(parentComponent, title, message, strClass, item, sessionOnly, alwaysRemember, JmriJOptionPane.ERROR_MESSAGE);
415    }
416
417    /**
418     * {@inheritDoc}
419     */
420    @Override
421    public void showInfoMessage(String title, String message, final String strClass, final String item, final boolean sessionOnly, final boolean alwaysRemember) {
422        this.showMessage(null, title, message, strClass, item, sessionOnly, alwaysRemember, JmriJOptionPane.INFORMATION_MESSAGE);
423    }
424
425    /**
426     * {@inheritDoc}
427     */
428    @Override
429    public void showInfoMessage(@CheckForNull Component parentComponent, String title, String message, final String strClass, final String item, final boolean sessionOnly, final boolean alwaysRemember) {
430        this.showMessage(parentComponent, title, message, strClass, item, sessionOnly, alwaysRemember, JmriJOptionPane.INFORMATION_MESSAGE);
431    }
432
433    /**
434     * {@inheritDoc}
435     */
436    @Override
437    public void showWarningMessage(String title, String message, final String strClass, final String item, final boolean sessionOnly, final boolean alwaysRemember) {
438        this.showMessage(null, title, message, strClass, item, sessionOnly, alwaysRemember, JmriJOptionPane.WARNING_MESSAGE);
439    }
440
441    /**
442     * {@inheritDoc}
443     */
444    @Override
445    public void showWarningMessage(@CheckForNull Component parentComponent, String title, String message, final String strClass, final String item, final boolean sessionOnly, final boolean alwaysRemember) {
446        this.showMessage(parentComponent, title, message, strClass, item, sessionOnly, alwaysRemember, JmriJOptionPane.WARNING_MESSAGE);
447    }
448
449    protected void showMessage(@CheckForNull Component parentComponent, String title, String message, final String strClass,
450        final String item, final boolean sessionOnly, final boolean alwaysRemember, int type) {
451        final String preference = strClass + "." + item;
452
453        if (this.getSessionPreferenceState(preference)) {
454            return;
455        }
456        if (!this.getPreferenceState(strClass, item)) {
457            JPanel container = new JPanel();
458            container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS));
459            container.add(new JLabel(message));
460            //I18N in ManagersBundle.properties
461            final JCheckBox rememberSession = new JCheckBox(Bundle.getMessage("SkipMessageSession")); // NOI18N
462            if (sessionOnly) {
463                rememberSession.setFont(rememberSession.getFont().deriveFont(10f));
464                container.add(rememberSession);
465            }
466            //I18N in ManagersBundle.properties
467            final JCheckBox remember = new JCheckBox(Bundle.getMessage("SkipMessageFuture")); // NOI18N
468            if (alwaysRemember) {
469                remember.setFont(remember.getFont().deriveFont(10f));
470                container.add(remember);
471            }
472            JmriJOptionPane.showMessageDialog(parentComponent, // center over parent component if present
473                    container,
474                    title,
475                    type);
476            if (remember.isSelected()) {
477                this.setPreferenceState(strClass, item, true);
478            }
479            if (rememberSession.isSelected()) {
480                this.setSessionPreferenceState(preference, true);
481            }
482
483        }
484    }
485
486    @Override
487    @CheckForNull
488    public String getComboBoxLastSelection(String comboBoxName) {
489        return this.comboBoxLastSelection.get(comboBoxName);
490    }
491
492    @Override
493    public void setComboBoxLastSelection(String comboBoxName, String lastValue) {
494        comboBoxLastSelection.put(comboBoxName, lastValue);
495        setChangeMade(false);
496        this.saveComboBoxLastSelections();
497    }
498
499    @Override
500    public boolean getCheckboxPreferenceState(String name, boolean defaultState) {
501        return this.checkBoxLastSelection.getOrDefault(name, defaultState);
502    }
503
504    @Override
505    public void setCheckboxPreferenceState(String name, boolean state) {
506        checkBoxLastSelection.put(name, state);
507        setChangeMade(false);
508        this.saveCheckBoxLastSelections();
509    }
510
511    public synchronized boolean getChangeMade() {
512        return dirty;
513    }
514
515    public synchronized void setChangeMade(boolean fireUpdate) {
516        dirty = true;
517        if (fireUpdate) {
518            this.firePropertyChange(UserPreferencesManager.PREFERENCES_UPDATED, null, null);
519        }
520    }
521
522    //The reset is used after the preferences have been loaded for the first time
523    @Override
524    public synchronized void resetChangeMade() {
525        dirty = false;
526    }
527
528    /**
529     * Check if this object is loading preferences from storage.
530     *
531     * @return true if loading preferences; false otherwise
532     */
533    protected boolean isLoading() {
534        return loading;
535    }
536
537    @Override
538    public void setLoading() {
539        loading = true;
540    }
541
542    @Override
543    public void finishLoading() {
544        loading = false;
545        resetChangeMade();
546    }
547
548    public void displayRememberMsg() {
549        if (loading) {
550            return;
551        }
552        showInfoMessage(Bundle.getMessage("Reminder"), Bundle.getMessage("ReminderLine"), getClassName(), REMINDER); // NOI18N
553    }
554
555    @Override
556    public Point getWindowLocation(String strClass) {
557        if (windowDetails.containsKey(strClass)) {
558            return windowDetails.get(strClass).getLocation();
559        }
560        return null;
561    }
562
563    @Override
564    public Dimension getWindowSize(String strClass) {
565        if (windowDetails.containsKey(strClass)) {
566            return windowDetails.get(strClass).getSize();
567        }
568        return null;
569    }
570
571    @Override
572    public boolean getSaveWindowSize(String strClass) {
573        if (windowDetails.containsKey(strClass)) {
574            return windowDetails.get(strClass).getSaveSize();
575        }
576        return false;
577    }
578
579    @Override
580    public boolean getSaveWindowLocation(String strClass) {
581        if (windowDetails.containsKey(strClass)) {
582            return windowDetails.get(strClass).getSaveLocation();
583        }
584        return false;
585    }
586
587    @Override
588    public void setSaveWindowSize(String strClass, boolean b) {
589        if ((strClass == null) || (strClass.equals(JMRI_UTIL_JMRI_JFRAME))) {
590            return;
591        }
592        if (!windowDetails.containsKey(strClass)) {
593            windowDetails.put(strClass, new WindowLocations());
594        }
595        windowDetails.get(strClass).setSaveSize(b);
596        this.saveWindowDetails();
597    }
598
599    @Override
600    public void setSaveWindowLocation(String strClass, boolean b) {
601        if ((strClass == null) || (strClass.equals(JMRI_UTIL_JMRI_JFRAME))) {
602            return;
603        }
604        if (!windowDetails.containsKey(strClass)) {
605            windowDetails.put(strClass, new WindowLocations());
606        }
607        windowDetails.get(strClass).setSaveLocation(b);
608        this.saveWindowDetails();
609    }
610
611    @Override
612    public void setWindowLocation(String strClass, Point location) {
613        if ((strClass == null) || (strClass.equals(JMRI_UTIL_JMRI_JFRAME))) {
614            return;
615        }
616        if (!windowDetails.containsKey(strClass)) {
617            windowDetails.put(strClass, new WindowLocations());
618        }
619        windowDetails.get(strClass).setLocation(location);
620        this.saveWindowDetails();
621    }
622
623    @Override
624    public void setWindowSize(String strClass, Dimension dim) {
625        if ((strClass == null) || (strClass.equals(JMRI_UTIL_JMRI_JFRAME))) {
626            return;
627        }
628        if (!windowDetails.containsKey(strClass)) {
629            windowDetails.put(strClass, new WindowLocations());
630        }
631        windowDetails.get(strClass).setSize(dim);
632        this.saveWindowDetails();
633    }
634
635    @Override
636    public ArrayList<String> getWindowList() {
637        return new ArrayList<>(windowDetails.keySet());
638    }
639
640    @Override
641    public void setProperty(String strClass, String key, Object value) {
642        if (strClass.equals(JmriJFrame.class.getName())) {
643            return;
644        }
645        if (!windowDetails.containsKey(strClass)) {
646            windowDetails.put(strClass, new WindowLocations());
647        }
648        windowDetails.get(strClass).setProperty(key, value);
649        this.saveWindowDetails();
650    }
651
652    @Override
653    public Object getProperty(String strClass, String key) {
654        if (windowDetails.containsKey(strClass)) {
655            return windowDetails.get(strClass).getProperty(key);
656        }
657        return null;
658    }
659
660    @Override
661    public Set<String> getPropertyKeys(String strClass) {
662        if (windowDetails.containsKey(strClass)) {
663            return windowDetails.get(strClass).getPropertyKeys();
664        }
665        return null;
666    }
667
668    @Override
669    public boolean hasProperties(String strClass) {
670        return windowDetails.containsKey(strClass);
671    }
672
673    @Nonnull
674    @Override
675    public String getClassDescription(String strClass) {
676        if (classPreferenceList.containsKey(strClass)) {
677            return classPreferenceList.get(strClass).getDescription();
678        }
679        return "";
680    }
681
682    @Nonnull
683    @Override
684    public ArrayList<String> getPreferencesClasses() {
685        return new ArrayList<>(this.classPreferenceList.keySet());
686    }
687
688    /**
689     * Given that we know the class as a string, we will try and attempt to
690     * gather details about the preferences that has been added, so that we can
691     * make better sense of the details in the preferences window.
692     * <p>
693     * This looks for specific methods within the class called
694     * "getClassDescription" and "setMessagePreferencesDetails". If found it
695     * will invoke the methods, this will then trigger the class to send details
696     * about its preferences back to this code.
697     */
698    @Override
699    public void setClassDescription(String strClass) {
700        try {
701            Class<?> cl = Class.forName(strClass);
702            Object t;
703            try {
704                t = cl.getDeclaredConstructor().newInstance();
705            } catch (IllegalArgumentException | NullPointerException | ExceptionInInitializerError | NoSuchMethodException | java.lang.reflect.InvocationTargetException ex) {
706                log.error("setClassDescription({}) failed in newInstance", strClass, ex);
707                return;
708            }
709            boolean classDesFound;
710            boolean classSetFound;
711            String desc = null;
712            Method method;
713            //look through declared methods first, then all methods
714            try {
715                method = cl.getDeclaredMethod("getClassDescription");
716                desc = (String) method.invoke(t);
717                classDesFound = true;
718            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException | ExceptionInInitializerError | NoSuchMethodException ex) {
719                log.debug("Unable to call declared method \"getClassDescription\" with exception", ex);
720                classDesFound = false;
721            }
722            if (!classDesFound) {
723                try {
724                    method = cl.getMethod("getClassDescription");
725                    desc = (String) method.invoke(t);
726                } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException | ExceptionInInitializerError | NoSuchMethodException ex) {
727                    log.debug("Unable to call undeclared method \"getClassDescription\" with exception", ex);
728                    classDesFound = false;
729                }
730            }
731            if (classDesFound) {
732                if (!classPreferenceList.containsKey(strClass)) {
733                    classPreferenceList.put(strClass, new ClassPreferences(desc));
734                } else {
735                    classPreferenceList.get(strClass).setDescription(desc);
736                }
737                this.savePreferencesState();
738            }
739
740            try {
741                method = cl.getDeclaredMethod("setMessagePreferencesDetails");
742                method.invoke(t);
743                classSetFound = true;
744            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException | ExceptionInInitializerError | NoSuchMethodException ex) {
745                // TableAction.setMessagePreferencesDetails() method is routinely not present in multiple classes
746                log.debug("Unable to call declared method \"setMessagePreferencesDetails\" with exception", ex);
747                classSetFound = false;
748            }
749            if (!classSetFound) {
750                try {
751                    method = cl.getMethod("setMessagePreferencesDetails");
752                    method.invoke(t);
753                } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException | ExceptionInInitializerError | NoSuchMethodException ex) {
754                    log.debug("Unable to call undeclared method \"setMessagePreferencesDetails\" with exception", ex);
755                }
756            }
757
758        } catch (ClassNotFoundException ex) {
759            log.warn("class name \"{}\" cannot be found, perhaps an expected plugin is missing?", strClass);
760        } catch (IllegalAccessException ex) {
761            log.error("unable to access class \"{}\"", strClass, ex);
762        } catch (InstantiationException ex) {
763            log.error("unable to get a class name \"{}\"", strClass, ex);
764        }
765    }
766
767    /**
768     * Add descriptive details about a specific message box, so that if it needs
769     * to be reset in the preferences, then it is easily identifiable. displayed
770     * to the user in the preferences GUI.
771     *
772     * @param strClass      String value of the calling class/group
773     * @param item          String value of the specific item this is used for.
774     * @param description   A meaningful description that can be used in a label
775     *                      to describe the item
776     * @param options       A map of the integer value of the option against a
777     *                      meaningful description.
778     * @param defaultOption The default option for the given item.
779     */
780    @Override
781    public void setMessageItemDetails(String strClass, String item, String description, HashMap<Integer, String> options, int defaultOption) {
782        if (!classPreferenceList.containsKey(strClass)) {
783            classPreferenceList.put(strClass, new ClassPreferences());
784        }
785        ArrayList<MultipleChoice> a = classPreferenceList.get(strClass).getMultipleChoiceList();
786        for (int i = 0; i < a.size(); i++) {
787            if (a.get(i).getItem().equals(item)) {
788                a.get(i).setMessageItems(description, options, defaultOption);
789                return;
790            }
791        }
792        a.add(new MultipleChoice(description, item, options, defaultOption));
793    }
794
795    @Override
796    public HashMap<Integer, String> getChoiceOptions(String strClass, String item) {
797        if (classPreferenceList.containsKey(strClass)) {
798            ArrayList<MultipleChoice> a = classPreferenceList.get(strClass).getMultipleChoiceList();
799            for (int i = 0; i < a.size(); i++) {
800                if (a.get(i).getItem().equals(item)) {
801                    return a.get(i).getOptions();
802                }
803            }
804        }
805        return new HashMap<>();
806    }
807
808    @Override
809    public int getMultipleChoiceSize(String strClass) {
810        if (classPreferenceList.containsKey(strClass)) {
811            return classPreferenceList.get(strClass).getMultipleChoiceListSize();
812        }
813        return 0;
814    }
815
816    @Override
817    public ArrayList<String> getMultipleChoiceList(String strClass) {
818        if (classPreferenceList.containsKey(strClass)) {
819            ArrayList<MultipleChoice> a = classPreferenceList.get(strClass).getMultipleChoiceList();
820            ArrayList<String> list = new ArrayList<>();
821            for (int i = 0; i < a.size(); i++) {
822                list.add(a.get(i).getItem());
823            }
824            return list;
825        }
826        return new ArrayList<>();
827    }
828
829    @Override
830    public String getChoiceName(String strClass, int n) {
831        if (classPreferenceList.containsKey(strClass)) {
832            return classPreferenceList.get(strClass).getChoiceName(n);
833        }
834        return null;
835    }
836
837    @Override
838    public String getChoiceDescription(String strClass, String item) {
839        if (classPreferenceList.containsKey(strClass)) {
840            ArrayList<MultipleChoice> a = classPreferenceList.get(strClass).getMultipleChoiceList();
841            for (int i = 0; i < a.size(); i++) {
842                if (a.get(i).getItem().equals(item)) {
843                    return a.get(i).getOptionDescription();
844                }
845            }
846        }
847        return null;
848    }
849
850    @Override
851    public int getMultipleChoiceOption(String strClass, String item) {
852        if (classPreferenceList.containsKey(strClass)) {
853            ArrayList<MultipleChoice> a = classPreferenceList.get(strClass).getMultipleChoiceList();
854            for (int i = 0; i < a.size(); i++) {
855                if (a.get(i).getItem().equals(item)) {
856                    return a.get(i).getValue();
857                }
858            }
859        }
860        return 0;
861    }
862
863    @Override
864    public int getMultipleChoiceDefaultOption(String strClass, String choice) {
865        if (classPreferenceList.containsKey(strClass)) {
866            ArrayList<MultipleChoice> a = classPreferenceList.get(strClass).getMultipleChoiceList();
867            for (int i = 0; i < a.size(); i++) {
868                if (a.get(i).getItem().equals(choice)) {
869                    return a.get(i).getDefaultValue();
870                }
871            }
872        }
873        return 0;
874    }
875
876    @Override
877    public void setMultipleChoiceOption(String strClass, String choice, String value) {
878        if (!classPreferenceList.containsKey(strClass)) {
879            classPreferenceList.put(strClass, new ClassPreferences());
880        }
881        classPreferenceList.get(strClass).getMultipleChoiceList().stream()
882                .filter(mc -> (mc.getItem().equals(choice))).forEachOrdered(mc -> mc.setValue(value));
883        this.savePreferencesState();
884    }
885
886    @Override
887    public void setMultipleChoiceOption(String strClass, String choice, int value) {
888
889        // LogixNG bug fix:
890        // The class 'strClass' must have a default constructor. Otherwise,
891        // an error is logged to the log. Early versions of LogixNG used
892        // AbstractLogixNGTableAction and ??? as strClass, which didn't work.
893        // Now, LogixNG uses the class jmri.jmrit.logixng.LogixNG_UserPreferences
894        // for this purpose.
895        if ("jmri.jmrit.beantable.AbstractLogixNGTableAction".equals(strClass)) return;
896        if ("jmri.jmrit.logixng.tools.swing.TreeEditor".equals(strClass)) return;
897
898        if (!classPreferenceList.containsKey(strClass)) {
899            classPreferenceList.put(strClass, new ClassPreferences());
900        }
901        boolean set = false;
902        for (MultipleChoice mc : classPreferenceList.get(strClass).getMultipleChoiceList()) {
903            if (mc.getItem().equals(choice)) {
904                mc.setValue(value);
905                set = true;
906            }
907        }
908        if (!set) {
909            classPreferenceList.get(strClass).getMultipleChoiceList().add(new MultipleChoice(choice, value));
910            setClassDescription(strClass);
911        }
912        displayRememberMsg();
913        this.savePreferencesState();
914    }
915
916    public String getClassDescription() {
917        return "Preference Manager";
918    }
919
920    protected final String getClassName() {
921        return this.getClass().getName();
922    }
923
924    protected final ClassPreferences getClassPreferences(String strClass) {
925        return this.classPreferenceList.get(strClass);
926    }
927
928    @Override
929    public int getPreferencesSize(String strClass) {
930        if (classPreferenceList.containsKey(strClass)) {
931            return classPreferenceList.get(strClass).getPreferencesSize();
932        }
933        return 0;
934    }
935
936    public final void readUserPreferences() {
937        log.trace("starting readUserPreferences");
938        this.allowSave = false;
939        this.loading = true;
940        File perNodeConfig = null;
941        try {
942            perNodeConfig = FileUtil.getFile(FileUtil.PROFILE + Profile.PROFILE + "/" + NodeIdentity.storageIdentity() + "/" + Profile.UI_CONFIG); // NOI18N
943            if (!perNodeConfig.canRead()) {
944                perNodeConfig = null;
945                log.trace("    sharedConfig can't be read");
946            }
947        } catch (FileNotFoundException ex) {
948            // ignore - this only means that sharedConfig does not exist.
949            log.trace("    FileNotFoundException: sharedConfig does not exist");
950        }
951        if (perNodeConfig != null) {
952            file = perNodeConfig;
953            log.debug("  start perNodeConfig file: {}", file.getPath());
954            this.readComboBoxLastSelections();
955            this.readCheckBoxLastSelections();
956            this.readPreferencesState();
957            this.readSimplePreferenceState();
958            this.readWindowDetails();
959        } else {
960            try {
961                file = FileUtil.getFile(FileUtil.PROFILE + Profile.UI_CONFIG_FILENAME);
962                if (file.exists()) {
963                    log.debug("start load user pref file: {}", file.getPath());
964                    try {
965                        boolean result = InstanceManager.getDefault(ConfigureManager.class).load(file, true);
966                        if (!result) {
967                            log.error("Failed to load file:{}", file);
968                        }
969                        this.allowSave = true;
970                        this.savePreferences(); // write new preferences format immediately
971                    } catch (JmriException e) {
972                        log.error("Unhandled problem loading configuration: {}", e.getMessage());
973                    } catch (NullPointerException e) {
974                        log.error("NPE when trying to load user pref {}", file);
975                    }
976                } else {
977                    // if we got here, there is no saved user preferences
978                    log.info("No saved user preferences file");
979                }
980            } catch (FileNotFoundException ex) {
981                // ignore - this only means that UserPrefsProfileConfig.xml does not exist.
982                log.debug("UserPrefsProfileConfig.xml does not exist");
983            }
984        }
985        this.loading = false;
986        this.allowSave = true;
987        log.trace("  ending readUserPreferences");
988    }
989
990    private void readComboBoxLastSelections() {
991        Element element = this.readElement(COMBOBOX_ELEMENT, COMBOBOX_NAMESPACE);
992        if (element != null) {
993            element.getChildren("comboBox").stream().forEach(combo ->
994                comboBoxLastSelection.put(combo.getAttributeValue("name"), combo.getAttributeValue("lastSelected")));
995        }
996    }
997
998    private void saveComboBoxLastSelections() {
999        this.setChangeMade(false);
1000        if (this.allowSave && !comboBoxLastSelection.isEmpty()) {
1001            Element element = new Element(COMBOBOX_ELEMENT, COMBOBOX_NAMESPACE);
1002            // Do not store blank last entered/selected values
1003            comboBoxLastSelection.entrySet().stream().
1004                    filter(cbls -> (cbls.getValue() != null && !cbls.getValue().isEmpty())).map(cbls -> {
1005                Element combo = new Element("comboBox");
1006                combo.setAttribute("name", cbls.getKey());
1007                combo.setAttribute("lastSelected", cbls.getValue());
1008                return combo;
1009            }).forEach(element::addContent);
1010            this.saveElement(element);
1011            this.resetChangeMade();
1012        }
1013    }
1014
1015    private void readCheckBoxLastSelections() {
1016        Element element = this.readElement(CHECKBOX_ELEMENT, CHECKBOX_NAMESPACE);
1017        if (element != null) {
1018            element.getChildren("checkBox").stream().forEach(checkbox ->
1019                checkBoxLastSelection.put(checkbox.getAttributeValue("name"), "yes".equals(checkbox.getAttributeValue("lastChecked"))));
1020        }
1021    }
1022
1023    private void saveCheckBoxLastSelections() {
1024        this.setChangeMade(false);
1025        if (this.allowSave && !checkBoxLastSelection.isEmpty()) {
1026            Element element = new Element(CHECKBOX_ELEMENT, CHECKBOX_NAMESPACE);
1027            // Do not store blank last entered/selected values
1028            checkBoxLastSelection.entrySet().stream().
1029                    filter(cbls -> (cbls.getValue() != null)).map(cbls -> {
1030                Element checkbox = new Element("checkBox");
1031                checkbox.setAttribute("name", cbls.getKey());
1032                checkbox.setAttribute("lastChecked", cbls.getValue() ? "yes" : "no");
1033                return checkbox;
1034            }).forEach(element::addContent);
1035            this.saveElement(element);
1036            this.resetChangeMade();
1037        }
1038    }
1039
1040    private void readPreferencesState() {
1041        Element element = this.readElement(CLASSPREFS_ELEMENT, CLASSPREFS_NAMESPACE);
1042        if (element != null) {
1043            element.getChildren("preferences").stream().forEach(preferences -> {
1044                String clazz = preferences.getAttributeValue(CLASS);
1045                log.debug("Reading class preferences for \"{}\"", clazz);
1046                preferences.getChildren("multipleChoice").stream().forEach(mc ->
1047                    mc.getChildren("option").stream().forEach(option -> {
1048                        int value = 0;
1049                        try {
1050                            value = option.getAttribute(VALUE).getIntValue();
1051                        } catch (DataConversionException ex) {
1052                            log.error("failed to convert positional attribute");
1053                        }
1054                        this.setMultipleChoiceOption(clazz, option.getAttributeValue("item"), value);
1055                    }));
1056                preferences.getChildren("reminderPrompts").stream().forEach(rp ->
1057                    rp.getChildren(REMINDER).stream().forEach(reminder -> {
1058                        log.debug("Setting preferences state \"true\" for \"{}\", \"{}\"", clazz, reminder.getText());
1059                        this.setPreferenceState(clazz, reminder.getText(), true);
1060                    }));
1061            });
1062        }
1063    }
1064
1065    private void savePreferencesState() {
1066        this.setChangeMade(true);
1067        if (this.allowSave) {
1068            Element element = new Element(CLASSPREFS_ELEMENT, CLASSPREFS_NAMESPACE);
1069            this.classPreferenceList.keySet().stream().forEach(name -> {
1070                ClassPreferences cp = this.classPreferenceList.get(name);
1071                if (!cp.multipleChoiceList.isEmpty() || !cp.preferenceList.isEmpty()) {
1072                    Element clazz = new Element("preferences");
1073                    clazz.setAttribute(CLASS, name);
1074                    if (!cp.multipleChoiceList.isEmpty()) {
1075                        Element choices = new Element("multipleChoice");
1076                        // only save non-default values
1077                        cp.multipleChoiceList.stream().filter(mc -> (mc.getDefaultValue() != mc.getValue())).forEach(mc ->
1078                            choices.addContent(new Element("option")
1079                                    .setAttribute("item", mc.getItem())
1080                                    .setAttribute(VALUE, Integer.toString(mc.getValue()))));
1081                        if (!choices.getChildren().isEmpty()) {
1082                            clazz.addContent(choices);
1083                        }
1084                    }
1085                    if (!cp.preferenceList.isEmpty()) {
1086                        Element reminders = new Element("reminderPrompts");
1087                        cp.preferenceList.stream().filter(pl -> (pl.getState())).forEach(pl ->
1088                            reminders.addContent(new Element(REMINDER).addContent(pl.getItem())));
1089                        if (!reminders.getChildren().isEmpty()) {
1090                            clazz.addContent(reminders);
1091                        }
1092                    }
1093                    element.addContent(clazz);
1094                }
1095            });
1096            if (!element.getChildren().isEmpty()) {
1097                this.saveElement(element);
1098            }
1099        }
1100    }
1101
1102    private void readSimplePreferenceState() {
1103        Element element = this.readElement(SETTINGS_ELEMENT, SETTINGS_NAMESPACE);
1104        if (element != null) {
1105            element.getChildren("setting").stream().forEach(setting ->
1106                this.simplePreferenceList.add(setting.getText()));
1107        }
1108    }
1109
1110    private void saveSimplePreferenceState() {
1111        this.setChangeMade(false);
1112        if (this.allowSave) {
1113            Element element = new Element(SETTINGS_ELEMENT, SETTINGS_NAMESPACE);
1114            getSimplePreferenceStateList().stream().forEach(setting ->
1115                element.addContent(new Element("setting").addContent(setting)));
1116            this.saveElement(element);
1117            this.resetChangeMade();
1118        }
1119    }
1120
1121    private void readWindowDetails() {
1122        // TODO: COMPLETE!
1123        Element element = this.readElement(WINDOWS_ELEMENT, WINDOWS_NAMESPACE);
1124        if (element != null) {
1125            element.getChildren("window").stream().forEach(window -> {
1126                String reference = window.getAttributeValue(CLASS);
1127                log.debug("Reading window details for {}", reference);
1128                try {
1129                    if (window.getAttribute("locX") != null && window.getAttribute("locY") != null) {
1130                        double x = window.getAttribute("locX").getDoubleValue();
1131                        double y = window.getAttribute("locY").getDoubleValue();
1132                        this.setWindowLocation(reference, new java.awt.Point((int) x, (int) y));
1133                    }
1134                    if (window.getAttribute(WIDTH) != null && window.getAttribute(HEIGHT) != null) {
1135                        double width = window.getAttribute(WIDTH).getDoubleValue();
1136                        double height = window.getAttribute(HEIGHT).getDoubleValue();
1137                        this.setWindowSize(reference, new java.awt.Dimension((int) width, (int) height));
1138                    }
1139                } catch (DataConversionException ex) {
1140                    log.error("Unable to read dimensions of window \"{}\"", reference);
1141                }
1142                if (window.getChild(PROPERTIES) != null) {
1143                    window.getChild(PROPERTIES).getChildren().stream().forEach(property -> {
1144                        String key = property.getChild("key").getText();
1145                        try {
1146                            Class<?> cl = Class.forName(property.getChild(VALUE).getAttributeValue(CLASS));
1147                            Constructor<?> ctor = cl.getConstructor(new Class<?>[]{String.class});
1148                            Object value = ctor.newInstance(new Object[]{property.getChild(VALUE).getText()});
1149                            log.debug("Setting property {} for {} to {}", key, reference, value);
1150                            this.setProperty(reference, key, value);
1151                        } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
1152                            log.error("Unable to retrieve property \"{}\" for window \"{}\"", key, reference);
1153                        } catch (NullPointerException ex) {
1154                            // null properties do not get set
1155                            log.debug("Property \"{}\" for window \"{}\" is null", key, reference);
1156                        }
1157                    });
1158                }
1159            });
1160        }
1161    }
1162
1163    @SuppressFBWarnings(value = "DMI_ENTRY_SETS_MAY_REUSE_ENTRY_OBJECTS",
1164            justification = "needs to copy the items of the hashmap windowDetails")
1165    private void saveWindowDetails() {
1166        this.setChangeMade(false);
1167        if (this.allowSave) {
1168            if (!windowDetails.isEmpty()) {
1169                Element element = new Element(WINDOWS_ELEMENT, WINDOWS_NAMESPACE);
1170                // Copy the entries before iterate over them since
1171                // ConcurrentModificationException may happen otherwise
1172                Set<Entry<String, WindowLocations>> entries = new HashSet<>(windowDetails.entrySet());
1173                for (Entry<String, WindowLocations> entry : entries) {
1174                    Element window = new Element("window");
1175                    window.setAttribute(CLASS, entry.getKey());
1176                    if (entry.getValue().getSaveLocation()) {
1177                        try {
1178                            window.setAttribute("locX", Double.toString(entry.getValue().getLocation().getX()));
1179                            window.setAttribute("locY", Double.toString(entry.getValue().getLocation().getY()));
1180                        } catch (NullPointerException ex) {
1181                            // Expected if the location has not been set or the window is open
1182                        }
1183                    }
1184                    if (entry.getValue().getSaveSize()) {
1185                        try {
1186                            double height = entry.getValue().getSize().getHeight();
1187                            double width = entry.getValue().getSize().getWidth();
1188                            // Do not save the width or height if set to zero
1189                            if (!(height == 0.0 && width == 0.0)) {
1190                                window.setAttribute(WIDTH, Double.toString(width));
1191                                window.setAttribute(HEIGHT, Double.toString(height));
1192                            }
1193                        } catch (NullPointerException ex) {
1194                            // Expected if the size has not been set or the window is open
1195                        }
1196                    }
1197                    if (!entry.getValue().parameters.isEmpty()) {
1198                        Element properties = new Element(PROPERTIES);
1199                        entry.getValue().parameters.entrySet().stream().map(property -> {
1200                            Element propertyElement = new Element("property");
1201                            propertyElement.addContent(new Element("key").setText(property.getKey()));
1202                            Object value = property.getValue();
1203                            if (value != null) {
1204                                propertyElement.addContent(new Element(VALUE)
1205                                        .setAttribute(CLASS, value.getClass().getName())
1206                                        .setText(value.toString()));
1207                            }
1208                            return propertyElement;
1209                        }).forEach(properties::addContent);
1210                        window.addContent(properties);
1211                    }
1212                    element.addContent(window);
1213                }
1214                this.saveElement(element);
1215                this.resetChangeMade();
1216            }
1217        }
1218    }
1219
1220    /**
1221     *
1222     * @return an Element or null if the requested element does not exist
1223     */
1224    @CheckForNull
1225    private Element readElement(@Nonnull String elementName, @Nonnull String namespace) {
1226        org.w3c.dom.Element element = ProfileUtils.getUserInterfaceConfiguration(ProfileManager.getDefault().getActiveProfile()).getConfigurationFragment(elementName, namespace, false);
1227        if (element != null) {
1228            return JDOMUtil.toJDOMElement(element);
1229        }
1230        return null;
1231    }
1232
1233    protected void saveElement(@Nonnull Element element) {
1234        log.trace("Saving {} element.", element.getName());
1235        try {
1236            ProfileUtils.getUserInterfaceConfiguration(ProfileManager.getDefault().getActiveProfile()).putConfigurationFragment(JDOMUtil.toW3CElement(element), false);
1237        } catch (JDOMException ex) {
1238            log.error("Unable to save user preferences", ex);
1239        }
1240    }
1241
1242    private void savePreferences() {
1243        this.saveComboBoxLastSelections();
1244        this.saveCheckBoxLastSelections();
1245        this.savePreferencesState();
1246        this.saveSimplePreferenceState();
1247        this.saveWindowDetails();
1248        this.resetChangeMade();
1249        InstanceManager.getOptionalDefault(JmriJTablePersistenceManager.class).ifPresent(manager ->
1250            manager.savePreferences(ProfileManager.getDefault().getActiveProfile()));
1251    }
1252
1253    @Override
1254    public void initialize() {
1255        this.readUserPreferences();
1256    }
1257
1258    /**
1259     * Holds details about the specific class.
1260     */
1261    protected static final class ClassPreferences {
1262
1263        String classDescription;
1264
1265        ArrayList<MultipleChoice> multipleChoiceList = new ArrayList<>();
1266        ArrayList<PreferenceList> preferenceList = new ArrayList<>();
1267
1268        ClassPreferences() {
1269        }
1270
1271        ClassPreferences(String classDescription) {
1272            this.classDescription = classDescription;
1273        }
1274
1275        String getDescription() {
1276            return classDescription;
1277        }
1278
1279        void setDescription(String description) {
1280            classDescription = description;
1281        }
1282
1283        ArrayList<PreferenceList> getPreferenceList() {
1284            return preferenceList;
1285        }
1286
1287        int getPreferenceListSize() {
1288            return preferenceList.size();
1289        }
1290
1291        ArrayList<MultipleChoice> getMultipleChoiceList() {
1292            return multipleChoiceList;
1293        }
1294
1295        int getPreferencesSize() {
1296            return multipleChoiceList.size() + preferenceList.size();
1297        }
1298
1299        public String getPreferenceName(int n) {
1300            try {
1301                return preferenceList.get(n).getItem();
1302            } catch (IndexOutOfBoundsException ioob) {
1303                return null;
1304            }
1305        }
1306
1307        int getMultipleChoiceListSize() {
1308            return multipleChoiceList.size();
1309        }
1310
1311        public String getChoiceName(int n) {
1312            try {
1313                return multipleChoiceList.get(n).getItem();
1314            } catch (IndexOutOfBoundsException ioob) {
1315                return null;
1316            }
1317        }
1318    }
1319
1320    protected static final class MultipleChoice {
1321
1322        HashMap<Integer, String> options;
1323        String optionDescription;
1324        String item;
1325        int value = -1;
1326        int defaultOption = -1;
1327
1328        MultipleChoice(String description, String item, HashMap<Integer, String> options, int defaultOption) {
1329            this.item = item;
1330            setMessageItems(description, options, defaultOption);
1331        }
1332
1333        MultipleChoice(String item, int value) {
1334            this.item = item;
1335            this.value = value;
1336
1337        }
1338
1339        void setValue(int value) {
1340            this.value = value;
1341        }
1342
1343        void setValue(String value) {
1344            options.keySet().stream().filter(o -> (options.get(o).equals(value))).forEachOrdered(o -> this.value = o);
1345        }
1346
1347        void setMessageItems(String description, HashMap<Integer, String> options, int defaultOption) {
1348            optionDescription = description;
1349            this.options = options;
1350            this.defaultOption = defaultOption;
1351            if (value == -1) {
1352                value = defaultOption;
1353            }
1354        }
1355
1356        int getValue() {
1357            return value;
1358        }
1359
1360        int getDefaultValue() {
1361            return defaultOption;
1362        }
1363
1364        String getItem() {
1365            return item;
1366        }
1367
1368        String getOptionDescription() {
1369            return optionDescription;
1370        }
1371
1372        HashMap<Integer, String> getOptions() {
1373            return options;
1374        }
1375
1376    }
1377
1378    protected static final class PreferenceList {
1379
1380        // need to fill this with bits to get a meaning full description.
1381        boolean set = false;
1382        String item = "";
1383        String description = "";
1384
1385        PreferenceList(String item) {
1386            this.item = item;
1387        }
1388
1389        PreferenceList(String item, boolean state) {
1390            this.item = item;
1391            set = state;
1392        }
1393
1394        PreferenceList(String item, String description) {
1395            this.description = description;
1396            this.item = item;
1397        }
1398
1399        void setDescription(String desc) {
1400            description = desc;
1401        }
1402
1403        String getDescription() {
1404            return description;
1405        }
1406
1407        boolean getState() {
1408            return set;
1409        }
1410
1411        void setState(boolean state) {
1412            this.set = state;
1413        }
1414
1415        String getItem() {
1416            return item;
1417        }
1418
1419    }
1420
1421    protected static final class WindowLocations {
1422
1423        private Point xyLocation = new Point(0, 0);
1424        private Dimension size = new Dimension(0, 0);
1425        private boolean saveSize = false;
1426        private boolean saveLocation = false;
1427
1428        WindowLocations() {
1429        }
1430
1431        Point getLocation() {
1432            return xyLocation;
1433        }
1434
1435        Dimension getSize() {
1436            return size;
1437        }
1438
1439        void setSaveSize(boolean b) {
1440            saveSize = b;
1441        }
1442
1443        void setSaveLocation(boolean b) {
1444            saveLocation = b;
1445        }
1446
1447        boolean getSaveSize() {
1448            return saveSize;
1449        }
1450
1451        boolean getSaveLocation() {
1452            return saveLocation;
1453        }
1454
1455        void setLocation(Point xyLocation) {
1456            this.xyLocation = xyLocation;
1457            saveLocation = true;
1458        }
1459
1460        void setSize(Dimension size) {
1461            this.size = size;
1462            saveSize = true;
1463        }
1464
1465        void setProperty(@Nonnull String key, @CheckForNull Object value) {
1466            if (value == null) {
1467                parameters.remove(key);
1468            } else {
1469                parameters.put(key, value);
1470            }
1471        }
1472
1473        @CheckForNull
1474        Object getProperty(String key) {
1475            return parameters.get(key);
1476        }
1477
1478        Set<String> getPropertyKeys() {
1479            return parameters.keySet();
1480        }
1481
1482        final ConcurrentHashMap<String, Object> parameters = new ConcurrentHashMap<>();
1483
1484    }
1485
1486    @ServiceProvider(service = InstanceInitializer.class)
1487    public static class Initializer extends AbstractInstanceInitializer {
1488
1489        @Override
1490        public <T> Object getDefault(Class<T> type) {
1491            if (type.equals(UserPreferencesManager.class)) {
1492                return new JmriUserPreferencesManager();
1493            }
1494            return super.getDefault(type);
1495        }
1496
1497        @Override
1498        public Set<Class<?>> getInitalizes() {
1499            Set<Class<?>> set = super.getInitalizes();
1500            set.add(UserPreferencesManager.class);
1501            return set;
1502        }
1503    }
1504
1505    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JmriUserPreferencesManager.class);
1506
1507}