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