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