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    @Override
258    public void setPreferenceState(String strClass, String item, boolean state) {
259        // convert old manager preferences to new manager preferences
260        if (strClass.equals("jmri.managers.DefaultUserMessagePreferences")) {
261            this.setPreferenceState("jmri.managers.JmriUserPreferencesManager", item, state);
262            return;
263        }
264        if (!classPreferenceList.containsKey(strClass)) {
265            classPreferenceList.put(strClass, new ClassPreferences());
266            setClassDescription(strClass);
267        }
268        ArrayList<PreferenceList> a = classPreferenceList.get(strClass).getPreferenceList();
269        boolean found = false;
270        for (int i = 0; i < a.size(); i++) {
271            if (a.get(i).getItem().equals(item)) {
272                a.get(i).setState(state);
273                found = true;
274            }
275        }
276        if (!found) {
277            a.add(new PreferenceList(item, state));
278        }
279        displayRememberMsg();
280        this.savePreferencesState();
281    }
282
283    @Override
284    public boolean getPreferenceState(String strClass, String item) {
285        if (classPreferenceList.containsKey(strClass)) {
286            ArrayList<PreferenceList> a = classPreferenceList.get(strClass).getPreferenceList();
287            for (int i = 0; i < a.size(); i++) {
288                if (a.get(i).getItem().equals(item)) {
289                    return a.get(i).getState();
290                }
291            }
292        }
293        return false;
294    }
295
296    @Override
297    public final void setPreferenceItemDetails(String strClass, String item, String description) {
298        if (!classPreferenceList.containsKey(strClass)) {
299            classPreferenceList.put(strClass, new ClassPreferences());
300        }
301        ArrayList<PreferenceList> a = classPreferenceList.get(strClass).getPreferenceList();
302        for (int i = 0; i < a.size(); i++) {
303            if (a.get(i).getItem().equals(item)) {
304                a.get(i).setDescription(description);
305                return;
306            }
307        }
308        a.add(new PreferenceList(item, description));
309    }
310
311    @Nonnull
312    @Override
313    public ArrayList<String> getPreferenceList(String strClass) {
314        if (classPreferenceList.containsKey(strClass)) {
315            ArrayList<PreferenceList> a = classPreferenceList.get(strClass).getPreferenceList();
316            ArrayList<String> list = new ArrayList<>();
317            for (int i = 0; i < a.size(); i++) {
318                list.add(a.get(i).getItem());
319            }
320            return list;
321        }
322        //Just return a blank array list will save call code checking for null
323        return new ArrayList<>();
324    }
325
326    @Override
327    @CheckForNull
328    public String getPreferenceItemName(String strClass, int n) {
329        if (classPreferenceList.containsKey(strClass)) {
330            return classPreferenceList.get(strClass).getPreferenceName(n);
331        }
332        return null;
333    }
334
335    @Override
336    @CheckForNull
337    public String getPreferenceItemDescription(String strClass, String item) {
338        if (classPreferenceList.containsKey(strClass)) {
339            ArrayList<PreferenceList> a = classPreferenceList.get(strClass).getPreferenceList();
340            for (int i = 0; i < a.size(); i++) {
341                if (a.get(i).getItem().equals(item)) {
342                    return a.get(i).getDescription();
343                }
344            }
345        }
346        return null;
347
348    }
349
350    /**
351     * Used to surpress messages for a particular session, the information is
352     * not stored, can not be changed via the GUI.
353     * <p>
354     * This can be used to help prevent over loading the user with repetitive
355     * error messages such as turnout not found while loading a panel file due
356     * to a connection failing. The name is free-form, but to avoid ambiguity it
357     * should start with the package name (package.Class) for the primary using
358     * class.
359     *
360     * @param name A unique identifier for preference.
361     */
362    @Override
363    public void setSessionPreferenceState(String name, boolean state) {
364        if (state) {
365            if (!sessionPreferenceList.contains(name)) {
366                sessionPreferenceList.add(name);
367            }
368        } else {
369            sessionPreferenceList.remove(name);
370        }
371    }
372
373    /**
374     * {@inheritDoc}
375     */
376    @Override
377    public boolean getSessionPreferenceState(String name) {
378        return sessionPreferenceList.contains(name);
379    }
380
381    /**
382     * {@inheritDoc}
383     */
384    @Override
385    public void showInfoMessage(String title, String message, String strClass, String item) {
386        showInfoMessage(title, message, strClass, item, false, true);
387    }
388
389    /**
390     * {@inheritDoc}
391     */
392    @Override
393    public void showInfoMessage(@CheckForNull Component parentComponent, String title, String message, String strClass, String item) {
394        showInfoMessage(parentComponent, title, message, strClass, item, false, true);
395    }
396
397    /**
398     * {@inheritDoc}
399     */
400    @Override
401    public void showErrorMessage(String title, String message, final String strClass, final String item, final boolean sessionOnly, final boolean alwaysRemember) {
402        this.showMessage(null, title, message, strClass, item, sessionOnly, alwaysRemember, JmriJOptionPane.ERROR_MESSAGE);
403    }
404
405    /**
406     * {@inheritDoc}
407     */
408    @Override
409    public void showErrorMessage(@CheckForNull Component parentComponent, String title, String message, final String strClass, final String item, final boolean sessionOnly, final boolean alwaysRemember) {
410        this.showMessage(parentComponent, title, message, strClass, item, sessionOnly, alwaysRemember, JmriJOptionPane.ERROR_MESSAGE);
411    }
412
413    /**
414     * {@inheritDoc}
415     */
416    @Override
417    public void showInfoMessage(String title, String message, final String strClass, final String item, final boolean sessionOnly, final boolean alwaysRemember) {
418        this.showMessage(null, title, message, strClass, item, sessionOnly, alwaysRemember, JmriJOptionPane.INFORMATION_MESSAGE);
419    }
420
421    /**
422     * {@inheritDoc}
423     */
424    @Override
425    public void showInfoMessage(@CheckForNull Component parentComponent, String title, String message, final String strClass, final String item, final boolean sessionOnly, final boolean alwaysRemember) {
426        this.showMessage(parentComponent, title, message, strClass, item, sessionOnly, alwaysRemember, JmriJOptionPane.INFORMATION_MESSAGE);
427    }
428
429    /**
430     * {@inheritDoc}
431     */
432    @Override
433    public void showWarningMessage(String title, String message, final String strClass, final String item, final boolean sessionOnly, final boolean alwaysRemember) {
434        this.showMessage(null, title, message, strClass, item, sessionOnly, alwaysRemember, JmriJOptionPane.WARNING_MESSAGE);
435    }
436
437    /**
438     * {@inheritDoc}
439     */
440    @Override
441    public void showWarningMessage(@CheckForNull Component parentComponent, String title, String message, final String strClass, final String item, final boolean sessionOnly, final boolean alwaysRemember) {
442        this.showMessage(parentComponent, title, message, strClass, item, sessionOnly, alwaysRemember, JmriJOptionPane.WARNING_MESSAGE);
443    }
444
445    protected void showMessage(@CheckForNull Component parentComponent, String title, String message, final String strClass,
446        final String item, final boolean sessionOnly, final boolean alwaysRemember, int type) {
447        final String preference = strClass + "." + item;
448
449        if (this.getSessionPreferenceState(preference)) {
450            return;
451        }
452        if (!this.getPreferenceState(strClass, item)) {
453            JPanel container = new JPanel();
454            container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS));
455            container.add(new JLabel(message));
456            //I18N in ManagersBundle.properties
457            final JCheckBox rememberSession = new JCheckBox(Bundle.getMessage("SkipMessageSession")); // NOI18N
458            if (sessionOnly) {
459                rememberSession.setFont(rememberSession.getFont().deriveFont(10f));
460                container.add(rememberSession);
461            }
462            //I18N in ManagersBundle.properties
463            final JCheckBox remember = new JCheckBox(Bundle.getMessage("SkipMessageFuture")); // NOI18N
464            if (alwaysRemember) {
465                remember.setFont(remember.getFont().deriveFont(10f));
466                container.add(remember);
467            }
468            JmriJOptionPane.showMessageDialog(parentComponent, // center over parent component if present
469                    container,
470                    title,
471                    type);
472            if (remember.isSelected()) {
473                this.setPreferenceState(strClass, item, true);
474            }
475            if (rememberSession.isSelected()) {
476                this.setSessionPreferenceState(preference, true);
477            }
478
479        }
480    }
481
482    @Override
483    @CheckForNull
484    public String getComboBoxLastSelection(String comboBoxName) {
485        return this.comboBoxLastSelection.get(comboBoxName);
486    }
487
488    @Override
489    public void setComboBoxLastSelection(String comboBoxName, String lastValue) {
490        comboBoxLastSelection.put(comboBoxName, lastValue);
491        setChangeMade(false);
492        this.saveComboBoxLastSelections();
493    }
494
495    @Override
496    public boolean getCheckboxPreferenceState(String name, boolean defaultState) {
497        return this.checkBoxLastSelection.getOrDefault(name, defaultState);
498    }
499
500    @Override
501    public void setCheckboxPreferenceState(String name, boolean state) {
502        checkBoxLastSelection.put(name, state);
503        setChangeMade(false);
504        this.saveCheckBoxLastSelections();
505    }
506
507    public synchronized boolean getChangeMade() {
508        return dirty;
509    }
510
511    public synchronized void setChangeMade(boolean fireUpdate) {
512        dirty = true;
513        if (fireUpdate) {
514            this.firePropertyChange(UserPreferencesManager.PREFERENCES_UPDATED, null, null);
515        }
516    }
517
518    //The reset is used after the preferences have been loaded for the first time
519    @Override
520    public synchronized void resetChangeMade() {
521        dirty = false;
522    }
523
524    /**
525     * Check if this object is loading preferences from storage.
526     *
527     * @return true if loading preferences; false otherwise
528     */
529    protected boolean isLoading() {
530        return loading;
531    }
532
533    @Override
534    public void setLoading() {
535        loading = true;
536    }
537
538    @Override
539    public void finishLoading() {
540        loading = false;
541        resetChangeMade();
542    }
543
544    public void displayRememberMsg() {
545        if (loading) {
546            return;
547        }
548        showInfoMessage(Bundle.getMessage("Reminder"), Bundle.getMessage("ReminderLine"), getClassName(), REMINDER); // NOI18N
549    }
550
551    @Override
552    public Point getWindowLocation(String strClass) {
553        if (windowDetails.containsKey(strClass)) {
554            return windowDetails.get(strClass).getLocation();
555        }
556        return null;
557    }
558
559    @Override
560    public Dimension getWindowSize(String strClass) {
561        if (windowDetails.containsKey(strClass)) {
562            return windowDetails.get(strClass).getSize();
563        }
564        return null;
565    }
566
567    @Override
568    public boolean getSaveWindowSize(String strClass) {
569        if (windowDetails.containsKey(strClass)) {
570            return windowDetails.get(strClass).getSaveSize();
571        }
572        return false;
573    }
574
575    @Override
576    public boolean getSaveWindowLocation(String strClass) {
577        if (windowDetails.containsKey(strClass)) {
578            return windowDetails.get(strClass).getSaveLocation();
579        }
580        return false;
581    }
582
583    @Override
584    public void setSaveWindowSize(String strClass, boolean b) {
585        if ((strClass == null) || (strClass.equals(JMRI_UTIL_JMRI_JFRAME))) {
586            return;
587        }
588        if (!windowDetails.containsKey(strClass)) {
589            windowDetails.put(strClass, new WindowLocations());
590        }
591        windowDetails.get(strClass).setSaveSize(b);
592        this.saveWindowDetails();
593    }
594
595    @Override
596    public void setSaveWindowLocation(String strClass, boolean b) {
597        if ((strClass == null) || (strClass.equals(JMRI_UTIL_JMRI_JFRAME))) {
598            return;
599        }
600        if (!windowDetails.containsKey(strClass)) {
601            windowDetails.put(strClass, new WindowLocations());
602        }
603        windowDetails.get(strClass).setSaveLocation(b);
604        this.saveWindowDetails();
605    }
606
607    @Override
608    public void setWindowLocation(String strClass, Point location) {
609        if ((strClass == null) || (strClass.equals(JMRI_UTIL_JMRI_JFRAME))) {
610            return;
611        }
612        if (!windowDetails.containsKey(strClass)) {
613            windowDetails.put(strClass, new WindowLocations());
614        }
615        windowDetails.get(strClass).setLocation(location);
616        this.saveWindowDetails();
617    }
618
619    @Override
620    public void setWindowSize(String strClass, Dimension dim) {
621        if ((strClass == null) || (strClass.equals(JMRI_UTIL_JMRI_JFRAME))) {
622            return;
623        }
624        if (!windowDetails.containsKey(strClass)) {
625            windowDetails.put(strClass, new WindowLocations());
626        }
627        windowDetails.get(strClass).setSize(dim);
628        this.saveWindowDetails();
629    }
630
631    @Override
632    public ArrayList<String> getWindowList() {
633        return new ArrayList<>(windowDetails.keySet());
634    }
635
636    @Override
637    public void setProperty(String strClass, String key, Object value) {
638        if (strClass.equals(JmriJFrame.class.getName())) {
639            return;
640        }
641        if (!windowDetails.containsKey(strClass)) {
642            windowDetails.put(strClass, new WindowLocations());
643        }
644        windowDetails.get(strClass).setProperty(key, value);
645        this.saveWindowDetails();
646    }
647
648    @Override
649    public Object getProperty(String strClass, String key) {
650        if (windowDetails.containsKey(strClass)) {
651            return windowDetails.get(strClass).getProperty(key);
652        }
653        return null;
654    }
655
656    @Override
657    public Set<String> getPropertyKeys(String strClass) {
658        if (windowDetails.containsKey(strClass)) {
659            return windowDetails.get(strClass).getPropertyKeys();
660        }
661        return null;
662    }
663
664    @Override
665    public boolean hasProperties(String strClass) {
666        return windowDetails.containsKey(strClass);
667    }
668
669    @Nonnull
670    @Override
671    public String getClassDescription(String strClass) {
672        if (classPreferenceList.containsKey(strClass)) {
673            return classPreferenceList.get(strClass).getDescription();
674        }
675        return "";
676    }
677
678    @Nonnull
679    @Override
680    public ArrayList<String> getPreferencesClasses() {
681        return new ArrayList<>(this.classPreferenceList.keySet());
682    }
683
684    /**
685     * Given that we know the class as a string, we will try and attempt to
686     * gather details about the preferences that has been added, so that we can
687     * make better sense of the details in the preferences window.
688     * <p>
689     * This looks for specific methods within the class called
690     * "getClassDescription" and "setMessagePreferencesDetails". If found it
691     * will invoke the methods, this will then trigger the class to send details
692     * about its preferences back to this code.
693     */
694    @Override
695    public void setClassDescription(String strClass) {
696        try {
697            Class<?> cl = Class.forName(strClass);
698            Object t;
699            try {
700                t = cl.getDeclaredConstructor().newInstance();
701            } catch (IllegalArgumentException | NullPointerException | ExceptionInInitializerError | NoSuchMethodException | java.lang.reflect.InvocationTargetException ex) {
702                log.error("setClassDescription({}) failed in newInstance", strClass, ex);
703                return;
704            }
705            boolean classDesFound;
706            boolean classSetFound;
707            String desc = null;
708            Method method;
709            //look through declared methods first, then all methods
710            try {
711                method = cl.getDeclaredMethod("getClassDescription");
712                desc = (String) method.invoke(t);
713                classDesFound = true;
714            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException | ExceptionInInitializerError | NoSuchMethodException ex) {
715                log.debug("Unable to call declared method \"getClassDescription\" with exception", ex);
716                classDesFound = false;
717            }
718            if (!classDesFound) {
719                try {
720                    method = cl.getMethod("getClassDescription");
721                    desc = (String) method.invoke(t);
722                } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException | ExceptionInInitializerError | NoSuchMethodException ex) {
723                    log.debug("Unable to call undeclared method \"getClassDescription\" with exception", ex);
724                    classDesFound = false;
725                }
726            }
727            if (classDesFound) {
728                if (!classPreferenceList.containsKey(strClass)) {
729                    classPreferenceList.put(strClass, new ClassPreferences(desc));
730                } else {
731                    classPreferenceList.get(strClass).setDescription(desc);
732                }
733                this.savePreferencesState();
734            }
735
736            try {
737                method = cl.getDeclaredMethod("setMessagePreferencesDetails");
738                method.invoke(t);
739                classSetFound = true;
740            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException | ExceptionInInitializerError | NoSuchMethodException ex) {
741                // TableAction.setMessagePreferencesDetails() method is routinely not present in multiple classes
742                log.debug("Unable to call declared method \"setMessagePreferencesDetails\" with exception", ex);
743                classSetFound = false;
744            }
745            if (!classSetFound) {
746                try {
747                    method = cl.getMethod("setMessagePreferencesDetails");
748                    method.invoke(t);
749                } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException | ExceptionInInitializerError | NoSuchMethodException ex) {
750                    log.debug("Unable to call undeclared method \"setMessagePreferencesDetails\" with exception", ex);
751                }
752            }
753
754        } catch (ClassNotFoundException ex) {
755            log.warn("class name \"{}\" cannot be found, perhaps an expected plugin is missing?", strClass);
756        } catch (IllegalAccessException ex) {
757            log.error("unable to access class \"{}\"", strClass, ex);
758        } catch (InstantiationException ex) {
759            log.error("unable to get a class name \"{}\"", strClass, ex);
760        }
761    }
762
763    /**
764     * Add descriptive details about a specific message box, so that if it needs
765     * to be reset in the preferences, then it is easily identifiable. displayed
766     * to the user in the preferences GUI.
767     *
768     * @param strClass      String value of the calling class/group
769     * @param item          String value of the specific item this is used for.
770     * @param description   A meaningful description that can be used in a label
771     *                      to describe the item
772     * @param options       A map of the integer value of the option against a
773     *                      meaningful description.
774     * @param defaultOption The default option for the given item.
775     */
776    @Override
777    public void setMessageItemDetails(String strClass, String item, String description, HashMap<Integer, String> options, int defaultOption) {
778        if (!classPreferenceList.containsKey(strClass)) {
779            classPreferenceList.put(strClass, new ClassPreferences());
780        }
781        ArrayList<MultipleChoice> a = classPreferenceList.get(strClass).getMultipleChoiceList();
782        for (int i = 0; i < a.size(); i++) {
783            if (a.get(i).getItem().equals(item)) {
784                a.get(i).setMessageItems(description, options, defaultOption);
785                return;
786            }
787        }
788        a.add(new MultipleChoice(description, item, options, defaultOption));
789    }
790
791    @Override
792    public HashMap<Integer, String> getChoiceOptions(String strClass, String item) {
793        if (classPreferenceList.containsKey(strClass)) {
794            ArrayList<MultipleChoice> a = classPreferenceList.get(strClass).getMultipleChoiceList();
795            for (int i = 0; i < a.size(); i++) {
796                if (a.get(i).getItem().equals(item)) {
797                    return a.get(i).getOptions();
798                }
799            }
800        }
801        return new HashMap<>();
802    }
803
804    @Override
805    public int getMultipleChoiceSize(String strClass) {
806        if (classPreferenceList.containsKey(strClass)) {
807            return classPreferenceList.get(strClass).getMultipleChoiceListSize();
808        }
809        return 0;
810    }
811
812    @Override
813    public ArrayList<String> getMultipleChoiceList(String strClass) {
814        if (classPreferenceList.containsKey(strClass)) {
815            ArrayList<MultipleChoice> a = classPreferenceList.get(strClass).getMultipleChoiceList();
816            ArrayList<String> list = new ArrayList<>();
817            for (int i = 0; i < a.size(); i++) {
818                list.add(a.get(i).getItem());
819            }
820            return list;
821        }
822        return new ArrayList<>();
823    }
824
825    @Override
826    public String getChoiceName(String strClass, int n) {
827        if (classPreferenceList.containsKey(strClass)) {
828            return classPreferenceList.get(strClass).getChoiceName(n);
829        }
830        return null;
831    }
832
833    @Override
834    public String getChoiceDescription(String strClass, String item) {
835        if (classPreferenceList.containsKey(strClass)) {
836            ArrayList<MultipleChoice> a = classPreferenceList.get(strClass).getMultipleChoiceList();
837            for (int i = 0; i < a.size(); i++) {
838                if (a.get(i).getItem().equals(item)) {
839                    return a.get(i).getOptionDescription();
840                }
841            }
842        }
843        return null;
844    }
845
846    @Override
847    public int getMultipleChoiceOption(String strClass, String item) {
848        if (classPreferenceList.containsKey(strClass)) {
849            ArrayList<MultipleChoice> a = classPreferenceList.get(strClass).getMultipleChoiceList();
850            for (int i = 0; i < a.size(); i++) {
851                if (a.get(i).getItem().equals(item)) {
852                    return a.get(i).getValue();
853                }
854            }
855        }
856        return 0;
857    }
858
859    @Override
860    public int getMultipleChoiceDefaultOption(String strClass, String choice) {
861        if (classPreferenceList.containsKey(strClass)) {
862            ArrayList<MultipleChoice> a = classPreferenceList.get(strClass).getMultipleChoiceList();
863            for (int i = 0; i < a.size(); i++) {
864                if (a.get(i).getItem().equals(choice)) {
865                    return a.get(i).getDefaultValue();
866                }
867            }
868        }
869        return 0;
870    }
871
872    @Override
873    public void setMultipleChoiceOption(String strClass, String choice, String value) {
874        if (!classPreferenceList.containsKey(strClass)) {
875            classPreferenceList.put(strClass, new ClassPreferences());
876        }
877        classPreferenceList.get(strClass).getMultipleChoiceList().stream()
878                .filter(mc -> (mc.getItem().equals(choice))).forEachOrdered(mc -> mc.setValue(value));
879        this.savePreferencesState();
880    }
881
882    @Override
883    public void setMultipleChoiceOption(String strClass, String choice, int value) {
884
885        // LogixNG bug fix:
886        // The class 'strClass' must have a default constructor. Otherwise,
887        // an error is logged to the log. Early versions of LogixNG used
888        // AbstractLogixNGTableAction and ??? as strClass, which didn't work.
889        // Now, LogixNG uses the class jmri.jmrit.logixng.LogixNG_UserPreferences
890        // for this purpose.
891        if ("jmri.jmrit.beantable.AbstractLogixNGTableAction".equals(strClass)) return;
892        if ("jmri.jmrit.logixng.tools.swing.TreeEditor".equals(strClass)) return;
893
894        if (!classPreferenceList.containsKey(strClass)) {
895            classPreferenceList.put(strClass, new ClassPreferences());
896        }
897        boolean set = false;
898        for (MultipleChoice mc : classPreferenceList.get(strClass).getMultipleChoiceList()) {
899            if (mc.getItem().equals(choice)) {
900                mc.setValue(value);
901                set = true;
902            }
903        }
904        if (!set) {
905            classPreferenceList.get(strClass).getMultipleChoiceList().add(new MultipleChoice(choice, value));
906            setClassDescription(strClass);
907        }
908        displayRememberMsg();
909        this.savePreferencesState();
910    }
911
912    public String getClassDescription() {
913        return "Preference Manager";
914    }
915
916    protected final String getClassName() {
917        return this.getClass().getName();
918    }
919
920    protected final ClassPreferences getClassPreferences(String strClass) {
921        return this.classPreferenceList.get(strClass);
922    }
923
924    @Override
925    public int getPreferencesSize(String strClass) {
926        if (classPreferenceList.containsKey(strClass)) {
927            return classPreferenceList.get(strClass).getPreferencesSize();
928        }
929        return 0;
930    }
931
932    public final void readUserPreferences() {
933        log.trace("starting readUserPreferences");
934        this.allowSave = false;
935        this.loading = true;
936        File perNodeConfig = null;
937        try {
938            perNodeConfig = FileUtil.getFile(FileUtil.PROFILE + Profile.PROFILE + "/" + NodeIdentity.storageIdentity() + "/" + Profile.UI_CONFIG); // NOI18N
939            if (!perNodeConfig.canRead()) {
940                perNodeConfig = null;
941                log.trace("    sharedConfig can't be read");
942            }
943        } catch (FileNotFoundException ex) {
944            // ignore - this only means that sharedConfig does not exist.
945            log.trace("    FileNotFoundException: sharedConfig does not exist");
946        }
947        if (perNodeConfig != null) {
948            file = perNodeConfig;
949            log.debug("  start perNodeConfig file: {}", file.getPath());
950            this.readComboBoxLastSelections();
951            this.readCheckBoxLastSelections();
952            this.readPreferencesState();
953            this.readSimplePreferenceState();
954            this.readWindowDetails();
955        } else {
956            try {
957                file = FileUtil.getFile(FileUtil.PROFILE + Profile.UI_CONFIG_FILENAME);
958                if (file.exists()) {
959                    log.debug("start load user pref file: {}", file.getPath());
960                    try {
961                        InstanceManager.getDefault(ConfigureManager.class).load(file, true);
962                        this.allowSave = true;
963                        this.savePreferences(); // write new preferences format immediately
964                    } catch (JmriException e) {
965                        log.error("Unhandled problem loading configuration: {}", e.getMessage());
966                    } catch (NullPointerException e) {
967                        log.error("NPE when trying to load user pref {}", file);
968                    }
969                } else {
970                    // if we got here, there is no saved user preferences
971                    log.info("No saved user preferences file");
972                }
973            } catch (FileNotFoundException ex) {
974                // ignore - this only means that UserPrefsProfileConfig.xml does not exist.
975                log.debug("UserPrefsProfileConfig.xml does not exist");
976            }
977        }
978        this.loading = false;
979        this.allowSave = true;
980        log.trace("  ending readUserPreferences");
981    }
982
983    private void readComboBoxLastSelections() {
984        Element element = this.readElement(COMBOBOX_ELEMENT, COMBOBOX_NAMESPACE);
985        if (element != null) {
986            element.getChildren("comboBox").stream().forEach(combo ->
987                comboBoxLastSelection.put(combo.getAttributeValue("name"), combo.getAttributeValue("lastSelected")));
988        }
989    }
990
991    private void saveComboBoxLastSelections() {
992        this.setChangeMade(false);
993        if (this.allowSave && !comboBoxLastSelection.isEmpty()) {
994            Element element = new Element(COMBOBOX_ELEMENT, COMBOBOX_NAMESPACE);
995            // Do not store blank last entered/selected values
996            comboBoxLastSelection.entrySet().stream().
997                    filter(cbls -> (cbls.getValue() != null && !cbls.getValue().isEmpty())).map(cbls -> {
998                Element combo = new Element("comboBox");
999                combo.setAttribute("name", cbls.getKey());
1000                combo.setAttribute("lastSelected", cbls.getValue());
1001                return combo;
1002            }).forEach(element::addContent);
1003            this.saveElement(element);
1004            this.resetChangeMade();
1005        }
1006    }
1007
1008    private void readCheckBoxLastSelections() {
1009        Element element = this.readElement(CHECKBOX_ELEMENT, CHECKBOX_NAMESPACE);
1010        if (element != null) {
1011            element.getChildren("checkBox").stream().forEach(checkbox ->
1012                checkBoxLastSelection.put(checkbox.getAttributeValue("name"), "yes".equals(checkbox.getAttributeValue("lastChecked"))));
1013        }
1014    }
1015
1016    private void saveCheckBoxLastSelections() {
1017        this.setChangeMade(false);
1018        if (this.allowSave && !checkBoxLastSelection.isEmpty()) {
1019            Element element = new Element(CHECKBOX_ELEMENT, CHECKBOX_NAMESPACE);
1020            // Do not store blank last entered/selected values
1021            checkBoxLastSelection.entrySet().stream().
1022                    filter(cbls -> (cbls.getValue() != null)).map(cbls -> {
1023                Element checkbox = new Element("checkBox");
1024                checkbox.setAttribute("name", cbls.getKey());
1025                checkbox.setAttribute("lastChecked", cbls.getValue() ? "yes" : "no");
1026                return checkbox;
1027            }).forEach(element::addContent);
1028            this.saveElement(element);
1029            this.resetChangeMade();
1030        }
1031    }
1032
1033    private void readPreferencesState() {
1034        Element element = this.readElement(CLASSPREFS_ELEMENT, CLASSPREFS_NAMESPACE);
1035        if (element != null) {
1036            element.getChildren("preferences").stream().forEach(preferences -> {
1037                String clazz = preferences.getAttributeValue(CLASS);
1038                log.debug("Reading class preferences for \"{}\"", clazz);
1039                preferences.getChildren("multipleChoice").stream().forEach(mc ->
1040                    mc.getChildren("option").stream().forEach(option -> {
1041                        int value = 0;
1042                        try {
1043                            value = option.getAttribute(VALUE).getIntValue();
1044                        } catch (DataConversionException ex) {
1045                            log.error("failed to convert positional attribute");
1046                        }
1047                        this.setMultipleChoiceOption(clazz, option.getAttributeValue("item"), value);
1048                    }));
1049                preferences.getChildren("reminderPrompts").stream().forEach(rp ->
1050                    rp.getChildren(REMINDER).stream().forEach(reminder -> {
1051                        log.debug("Setting preferences state \"true\" for \"{}\", \"{}\"", clazz, reminder.getText());
1052                        this.setPreferenceState(clazz, reminder.getText(), true);
1053                    }));
1054            });
1055        }
1056    }
1057
1058    private void savePreferencesState() {
1059        this.setChangeMade(true);
1060        if (this.allowSave) {
1061            Element element = new Element(CLASSPREFS_ELEMENT, CLASSPREFS_NAMESPACE);
1062            this.classPreferenceList.keySet().stream().forEach(name -> {
1063                ClassPreferences cp = this.classPreferenceList.get(name);
1064                if (!cp.multipleChoiceList.isEmpty() || !cp.preferenceList.isEmpty()) {
1065                    Element clazz = new Element("preferences");
1066                    clazz.setAttribute(CLASS, name);
1067                    if (!cp.multipleChoiceList.isEmpty()) {
1068                        Element choices = new Element("multipleChoice");
1069                        // only save non-default values
1070                        cp.multipleChoiceList.stream().filter(mc -> (mc.getDefaultValue() != mc.getValue())).forEach(mc ->
1071                            choices.addContent(new Element("option")
1072                                    .setAttribute("item", mc.getItem())
1073                                    .setAttribute(VALUE, Integer.toString(mc.getValue()))));
1074                        if (!choices.getChildren().isEmpty()) {
1075                            clazz.addContent(choices);
1076                        }
1077                    }
1078                    if (!cp.preferenceList.isEmpty()) {
1079                        Element reminders = new Element("reminderPrompts");
1080                        cp.preferenceList.stream().filter(pl -> (pl.getState())).forEach(pl ->
1081                            reminders.addContent(new Element(REMINDER).addContent(pl.getItem())));
1082                        if (!reminders.getChildren().isEmpty()) {
1083                            clazz.addContent(reminders);
1084                        }
1085                    }
1086                    element.addContent(clazz);
1087                }
1088            });
1089            if (!element.getChildren().isEmpty()) {
1090                this.saveElement(element);
1091            }
1092        }
1093    }
1094
1095    private void readSimplePreferenceState() {
1096        Element element = this.readElement(SETTINGS_ELEMENT, SETTINGS_NAMESPACE);
1097        if (element != null) {
1098            element.getChildren("setting").stream().forEach(setting ->
1099                this.simplePreferenceList.add(setting.getText()));
1100        }
1101    }
1102
1103    private void saveSimplePreferenceState() {
1104        this.setChangeMade(false);
1105        if (this.allowSave) {
1106            Element element = new Element(SETTINGS_ELEMENT, SETTINGS_NAMESPACE);
1107            getSimplePreferenceStateList().stream().forEach(setting ->
1108                element.addContent(new Element("setting").addContent(setting)));
1109            this.saveElement(element);
1110            this.resetChangeMade();
1111        }
1112    }
1113
1114    private void readWindowDetails() {
1115        // TODO: COMPLETE!
1116        Element element = this.readElement(WINDOWS_ELEMENT, WINDOWS_NAMESPACE);
1117        if (element != null) {
1118            element.getChildren("window").stream().forEach(window -> {
1119                String reference = window.getAttributeValue(CLASS);
1120                log.debug("Reading window details for {}", reference);
1121                try {
1122                    if (window.getAttribute("locX") != null && window.getAttribute("locY") != null) {
1123                        double x = window.getAttribute("locX").getDoubleValue();
1124                        double y = window.getAttribute("locY").getDoubleValue();
1125                        this.setWindowLocation(reference, new java.awt.Point((int) x, (int) y));
1126                    }
1127                    if (window.getAttribute(WIDTH) != null && window.getAttribute(HEIGHT) != null) {
1128                        double width = window.getAttribute(WIDTH).getDoubleValue();
1129                        double height = window.getAttribute(HEIGHT).getDoubleValue();
1130                        this.setWindowSize(reference, new java.awt.Dimension((int) width, (int) height));
1131                    }
1132                } catch (DataConversionException ex) {
1133                    log.error("Unable to read dimensions of window \"{}\"", reference);
1134                }
1135                if (window.getChild(PROPERTIES) != null) {
1136                    window.getChild(PROPERTIES).getChildren().stream().forEach(property -> {
1137                        String key = property.getChild("key").getText();
1138                        try {
1139                            Class<?> cl = Class.forName(property.getChild(VALUE).getAttributeValue(CLASS));
1140                            Constructor<?> ctor = cl.getConstructor(new Class<?>[]{String.class});
1141                            Object value = ctor.newInstance(new Object[]{property.getChild(VALUE).getText()});
1142                            log.debug("Setting property {} for {} to {}", key, reference, value);
1143                            this.setProperty(reference, key, value);
1144                        } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
1145                            log.error("Unable to retrieve property \"{}\" for window \"{}\"", key, reference);
1146                        } catch (NullPointerException ex) {
1147                            // null properties do not get set
1148                            log.debug("Property \"{}\" for window \"{}\" is null", key, reference);
1149                        }
1150                    });
1151                }
1152            });
1153        }
1154    }
1155
1156    @SuppressFBWarnings(value = "DMI_ENTRY_SETS_MAY_REUSE_ENTRY_OBJECTS",
1157            justification = "needs to copy the items of the hashmap windowDetails")
1158    private void saveWindowDetails() {
1159        this.setChangeMade(false);
1160        if (this.allowSave) {
1161            if (!windowDetails.isEmpty()) {
1162                Element element = new Element(WINDOWS_ELEMENT, WINDOWS_NAMESPACE);
1163                // Copy the entries before iterate over them since
1164                // ConcurrentModificationException may happen otherwise
1165                Set<Entry<String, WindowLocations>> entries = new HashSet<>(windowDetails.entrySet());
1166                for (Entry<String, WindowLocations> entry : entries) {
1167                    Element window = new Element("window");
1168                    window.setAttribute(CLASS, entry.getKey());
1169                    if (entry.getValue().getSaveLocation()) {
1170                        try {
1171                            window.setAttribute("locX", Double.toString(entry.getValue().getLocation().getX()));
1172                            window.setAttribute("locY", Double.toString(entry.getValue().getLocation().getY()));
1173                        } catch (NullPointerException ex) {
1174                            // Expected if the location has not been set or the window is open
1175                        }
1176                    }
1177                    if (entry.getValue().getSaveSize()) {
1178                        try {
1179                            double height = entry.getValue().getSize().getHeight();
1180                            double width = entry.getValue().getSize().getWidth();
1181                            // Do not save the width or height if set to zero
1182                            if (!(height == 0.0 && width == 0.0)) {
1183                                window.setAttribute(WIDTH, Double.toString(width));
1184                                window.setAttribute(HEIGHT, Double.toString(height));
1185                            }
1186                        } catch (NullPointerException ex) {
1187                            // Expected if the size has not been set or the window is open
1188                        }
1189                    }
1190                    if (!entry.getValue().parameters.isEmpty()) {
1191                        Element properties = new Element(PROPERTIES);
1192                        entry.getValue().parameters.entrySet().stream().map(property -> {
1193                            Element propertyElement = new Element("property");
1194                            propertyElement.addContent(new Element("key").setText(property.getKey()));
1195                            Object value = property.getValue();
1196                            if (value != null) {
1197                                propertyElement.addContent(new Element(VALUE)
1198                                        .setAttribute(CLASS, value.getClass().getName())
1199                                        .setText(value.toString()));
1200                            }
1201                            return propertyElement;
1202                        }).forEach(properties::addContent);
1203                        window.addContent(properties);
1204                    }
1205                    element.addContent(window);
1206                }
1207                this.saveElement(element);
1208                this.resetChangeMade();
1209            }
1210        }
1211    }
1212
1213    /**
1214     *
1215     * @return an Element or null if the requested element does not exist
1216     */
1217    @CheckForNull
1218    private Element readElement(@Nonnull String elementName, @Nonnull String namespace) {
1219        org.w3c.dom.Element element = ProfileUtils.getUserInterfaceConfiguration(ProfileManager.getDefault().getActiveProfile()).getConfigurationFragment(elementName, namespace, false);
1220        if (element != null) {
1221            return JDOMUtil.toJDOMElement(element);
1222        }
1223        return null;
1224    }
1225
1226    protected void saveElement(@Nonnull Element element) {
1227        log.trace("Saving {} element.", element.getName());
1228        try {
1229            ProfileUtils.getUserInterfaceConfiguration(ProfileManager.getDefault().getActiveProfile()).putConfigurationFragment(JDOMUtil.toW3CElement(element), false);
1230        } catch (JDOMException ex) {
1231            log.error("Unable to save user preferences", ex);
1232        }
1233    }
1234
1235    private void savePreferences() {
1236        this.saveComboBoxLastSelections();
1237        this.saveCheckBoxLastSelections();
1238        this.savePreferencesState();
1239        this.saveSimplePreferenceState();
1240        this.saveWindowDetails();
1241        this.resetChangeMade();
1242        InstanceManager.getOptionalDefault(JmriJTablePersistenceManager.class).ifPresent(manager ->
1243            manager.savePreferences(ProfileManager.getDefault().getActiveProfile()));
1244    }
1245
1246    @Override
1247    public void initialize() {
1248        this.readUserPreferences();
1249    }
1250
1251    /**
1252     * Holds details about the specific class.
1253     */
1254    protected static final class ClassPreferences {
1255
1256        String classDescription;
1257
1258        ArrayList<MultipleChoice> multipleChoiceList = new ArrayList<>();
1259        ArrayList<PreferenceList> preferenceList = new ArrayList<>();
1260
1261        ClassPreferences() {
1262        }
1263
1264        ClassPreferences(String classDescription) {
1265            this.classDescription = classDescription;
1266        }
1267
1268        String getDescription() {
1269            return classDescription;
1270        }
1271
1272        void setDescription(String description) {
1273            classDescription = description;
1274        }
1275
1276        ArrayList<PreferenceList> getPreferenceList() {
1277            return preferenceList;
1278        }
1279
1280        int getPreferenceListSize() {
1281            return preferenceList.size();
1282        }
1283
1284        ArrayList<MultipleChoice> getMultipleChoiceList() {
1285            return multipleChoiceList;
1286        }
1287
1288        int getPreferencesSize() {
1289            return multipleChoiceList.size() + preferenceList.size();
1290        }
1291
1292        public String getPreferenceName(int n) {
1293            try {
1294                return preferenceList.get(n).getItem();
1295            } catch (IndexOutOfBoundsException ioob) {
1296                return null;
1297            }
1298        }
1299
1300        int getMultipleChoiceListSize() {
1301            return multipleChoiceList.size();
1302        }
1303
1304        public String getChoiceName(int n) {
1305            try {
1306                return multipleChoiceList.get(n).getItem();
1307            } catch (IndexOutOfBoundsException ioob) {
1308                return null;
1309            }
1310        }
1311    }
1312
1313    protected static final class MultipleChoice {
1314
1315        HashMap<Integer, String> options;
1316        String optionDescription;
1317        String item;
1318        int value = -1;
1319        int defaultOption = -1;
1320
1321        MultipleChoice(String description, String item, HashMap<Integer, String> options, int defaultOption) {
1322            this.item = item;
1323            setMessageItems(description, options, defaultOption);
1324        }
1325
1326        MultipleChoice(String item, int value) {
1327            this.item = item;
1328            this.value = value;
1329
1330        }
1331
1332        void setValue(int value) {
1333            this.value = value;
1334        }
1335
1336        void setValue(String value) {
1337            options.keySet().stream().filter(o -> (options.get(o).equals(value))).forEachOrdered(o -> this.value = o);
1338        }
1339
1340        void setMessageItems(String description, HashMap<Integer, String> options, int defaultOption) {
1341            optionDescription = description;
1342            this.options = options;
1343            this.defaultOption = defaultOption;
1344            if (value == -1) {
1345                value = defaultOption;
1346            }
1347        }
1348
1349        int getValue() {
1350            return value;
1351        }
1352
1353        int getDefaultValue() {
1354            return defaultOption;
1355        }
1356
1357        String getItem() {
1358            return item;
1359        }
1360
1361        String getOptionDescription() {
1362            return optionDescription;
1363        }
1364
1365        HashMap<Integer, String> getOptions() {
1366            return options;
1367        }
1368
1369    }
1370
1371    protected static final class PreferenceList {
1372
1373        // need to fill this with bits to get a meaning full description.
1374        boolean set = false;
1375        String item = "";
1376        String description = "";
1377
1378        PreferenceList(String item) {
1379            this.item = item;
1380        }
1381
1382        PreferenceList(String item, boolean state) {
1383            this.item = item;
1384            set = state;
1385        }
1386
1387        PreferenceList(String item, String description) {
1388            this.description = description;
1389            this.item = item;
1390        }
1391
1392        void setDescription(String desc) {
1393            description = desc;
1394        }
1395
1396        String getDescription() {
1397            return description;
1398        }
1399
1400        boolean getState() {
1401            return set;
1402        }
1403
1404        void setState(boolean state) {
1405            this.set = state;
1406        }
1407
1408        String getItem() {
1409            return item;
1410        }
1411
1412    }
1413
1414    protected static final class WindowLocations {
1415
1416        private Point xyLocation = new Point(0, 0);
1417        private Dimension size = new Dimension(0, 0);
1418        private boolean saveSize = false;
1419        private boolean saveLocation = false;
1420
1421        WindowLocations() {
1422        }
1423
1424        Point getLocation() {
1425            return xyLocation;
1426        }
1427
1428        Dimension getSize() {
1429            return size;
1430        }
1431
1432        void setSaveSize(boolean b) {
1433            saveSize = b;
1434        }
1435
1436        void setSaveLocation(boolean b) {
1437            saveLocation = b;
1438        }
1439
1440        boolean getSaveSize() {
1441            return saveSize;
1442        }
1443
1444        boolean getSaveLocation() {
1445            return saveLocation;
1446        }
1447
1448        void setLocation(Point xyLocation) {
1449            this.xyLocation = xyLocation;
1450            saveLocation = true;
1451        }
1452
1453        void setSize(Dimension size) {
1454            this.size = size;
1455            saveSize = true;
1456        }
1457
1458        void setProperty(@Nonnull String key, @CheckForNull Object value) {
1459            if (value == null) {
1460                parameters.remove(key);
1461            } else {
1462                parameters.put(key, value);
1463            }
1464        }
1465
1466        @CheckForNull
1467        Object getProperty(String key) {
1468            return parameters.get(key);
1469        }
1470
1471        Set<String> getPropertyKeys() {
1472            return parameters.keySet();
1473        }
1474
1475        final ConcurrentHashMap<String, Object> parameters = new ConcurrentHashMap<>();
1476
1477    }
1478
1479    @ServiceProvider(service = InstanceInitializer.class)
1480    public static class Initializer extends AbstractInstanceInitializer {
1481
1482        @Override
1483        public <T> Object getDefault(Class<T> type) {
1484            if (type.equals(UserPreferencesManager.class)) {
1485                return new JmriUserPreferencesManager();
1486            }
1487            return super.getDefault(type);
1488        }
1489
1490        @Override
1491        public Set<Class<?>> getInitalizes() {
1492            Set<Class<?>> set = super.getInitalizes();
1493            set.add(UserPreferencesManager.class);
1494            return set;
1495        }
1496    }
1497
1498    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JmriUserPreferencesManager.class);
1499
1500}