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