001package jmri.util.gui;
002
003import java.awt.Font;
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.Enumeration;
007import java.util.HashSet;
008import java.util.List;
009import java.util.Locale;
010import java.util.Set;
011import java.util.prefs.BackingStoreException;
012import java.util.prefs.Preferences;
013import javax.annotation.Nonnull;
014import javax.swing.ToolTipManager;
015import javax.swing.UIManager;
016import javax.swing.UIManager.LookAndFeelInfo;
017import javax.swing.UnsupportedLookAndFeelException;
018import jmri.InstanceManagerAutoDefault;
019import jmri.beans.Bean;
020import jmri.profile.Profile;
021import jmri.profile.ProfileUtils;
022import jmri.spi.PreferencesManager;
023import jmri.util.prefs.InitializationException;
024import org.openide.util.lookup.ServiceProvider;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028/**
029 * Manage GUI Look and Feel (LAF) preferences.
030 *
031 * @author Randall Wood (C) 2015, 2020
032 */
033@ServiceProvider(service = PreferencesManager.class)
034public class GuiLafPreferencesManager extends Bean implements PreferencesManager, InstanceManagerAutoDefault {
035
036    public static final String FONT_NAME = "fontName";
037    public static final String FONT_SIZE = "fontSize";
038    public static final String LOCALE = "locale";
039    public static final String LOOK_AND_FEEL = "lookAndFeel";
040    public static final String NONSTANDARD_MOUSE_EVENT = "nonstandardMouseEvent";
041    // Display state in bean tables as icon.
042    public static final String GRAPHIC_TABLE_STATE = "graphicTableState";
043    // Classic OBlock editor or tabbed tables
044    public static final String OBLOCK_EDIT_TABBED = "oblockEditTabbed";
045    public static final String VERTICAL_TOOLBAR = "verticalToolBar";
046    public static final String SHOW_TOOL_TIP_TIME = "showToolTipDismissDelay";
047    public static final String EDITOR_USE_OLD_LOC_SIZE = "editorUseOldLocSize";
048    public static final String JFILECHOOSER_FORMAT = "jfilechooserformat";
049    public static final String MAX_COMBO_ROWS = "maxComboRows";
050    /**
051     * Smallest font size a user can set the font size to other than zero
052     * ({@value}). A font size of 0 indicates that the system default font size
053     * will be used.
054     *
055     * @see apps.GuiLafConfigPane#MIN_DISPLAYED_FONT_SIZE
056     */
057    public static final int MIN_FONT_SIZE = 9;
058    /**
059     * Largest font size a user can set the font size to ({@value}).
060     *
061     * @see apps.GuiLafConfigPane#MAX_DISPLAYED_FONT_SIZE
062     */
063    public static final int MAX_FONT_SIZE = 36;
064    public static final String PROP_DIRTY = "dirty";
065    public static final String PROP_RESTARTREQUIRED = "restartRequired";
066    public static final String DEFAULT_FONT = "List.font";
067
068    // preferences with default values
069    private Locale locale = Locale.getDefault();
070    private Font currentFont = null;
071    private Font defaultFont = null;
072    private int fontSize = 0;
073    private int defaultFontSize = 11; 
074    private boolean nonStandardMouseEvent = false;
075    private boolean graphicTableState = false;
076    private boolean oblockEditTabbed = false;
077    private boolean editorUseOldLocSize = false;
078    private int jFileChooserFormat = 0;
079    private String lookAndFeel = UIManager.getLookAndFeel().getClass().getName();
080    private int toolTipDismissDelay = ToolTipManager.sharedInstance().getDismissDelay();
081    private int maxComboRows = 0;
082    private boolean dirty = false;
083    private boolean restartRequired = false;
084
085    /*
086     * Unlike most PreferencesProviders, the GUI Look & Feel preferences should
087     * be per-application instead of per-profile.
088     */
089    private boolean initialized = false;
090    private final List<InitializationException> exceptions = new ArrayList<>();
091    private static final Logger log = LoggerFactory.getLogger(GuiLafPreferencesManager.class);
092
093    @Override
094    public void initialize(Profile profile) throws InitializationException {
095        if (!this.initialized) {
096            Preferences preferences = ProfileUtils.getPreferences(profile, this.getClass(), true);
097            this.setLocale(Locale.forLanguageTag(preferences.get(LOCALE, this.getLocale().toLanguageTag())));
098
099            var lookAndFeelClassname = preferences.get(LOOK_AND_FEEL, this.getLookAndFeel());
100            this.setLookAndFeel(lookAndFeelClassname);
101            
102            this.setDefaultFontSize(); // before we change anything
103            this.setFontSize(preferences.getInt(FONT_SIZE, this.getDefaultFontSize()));
104            if (this.getFontSize() == 0) {
105                this.setFontSize(this.getDefaultFontSize());
106            }
107
108            this.setFontByName(preferences.get(FONT_NAME, this.getDefaultFont().getFontName()));
109            if (this.getFont() == null) {
110                this.setFont(this.getDefaultFont());
111            }
112
113            this.setNonStandardMouseEvent(
114                    preferences.getBoolean(NONSTANDARD_MOUSE_EVENT, this.isNonStandardMouseEvent()));
115            this.setGraphicTableState(preferences.getBoolean(GRAPHIC_TABLE_STATE, this.isGraphicTableState()));
116            this.setOblockEditTabbed(preferences.getBoolean(OBLOCK_EDIT_TABBED, this.isOblockEditTabbed()));
117            this.setEditorUseOldLocSize(preferences.getBoolean(EDITOR_USE_OLD_LOC_SIZE, this.isEditorUseOldLocSize()));
118            this.setJFileChooserFormat(preferences.getInt(JFILECHOOSER_FORMAT, this.getJFileChooserFormat()));
119            this.setMaxComboRows(preferences.getInt(MAX_COMBO_ROWS, this.getMaxComboRows()));
120            this.setToolTipDismissDelay(preferences.getInt(SHOW_TOOL_TIP_TIME, this.getToolTipDismissDelay()));
121
122            log.debug("About to setDefault Locale");
123            Locale.setDefault(this.getLocale());
124            javax.swing.JComponent.setDefaultLocale(this.getLocale());
125
126            this.applyLookAndFeel();
127            this.applyFontSize();
128            this.initialized = true;
129        }
130    }
131
132    @Override
133    public boolean isInitialized(Profile profile) {
134        return this.initialized && this.exceptions.isEmpty();
135    }
136
137    @Override
138    @Nonnull
139    public Collection<Class<? extends PreferencesManager>> getRequires() {
140        return new HashSet<>();
141    }
142
143    @Override
144    @Nonnull
145    public Iterable<Class<?>> getProvides() {
146        Set<Class<?>> provides = new HashSet<>();
147        provides.add(this.getClass());
148        return provides;
149    }
150
151    @Override
152    public void savePreferences(Profile profile) {
153        Preferences preferences = ProfileUtils.getPreferences(profile, this.getClass(), true);
154        preferences.put(LOCALE, this.getLocale().toLanguageTag());
155        preferences.put(LOOK_AND_FEEL, this.getLookAndFeel());
156
157        if (currentFont == null) {
158            currentFont = this.getDefaultFont();
159        }
160
161        String currentFontName = currentFont.getFontName();
162        if (currentFontName != null) {
163            String prefFontName = preferences.get(FONT_NAME, currentFontName);
164            if ((prefFontName == null) || (!prefFontName.equals(currentFontName))) {
165                preferences.put(FONT_NAME, currentFontName);
166            }
167        }
168
169        int temp = this.getFontSize();
170        if (temp == this.getDefaultFontSize()) {
171            temp = 0;
172        }
173        if (temp != preferences.getInt(FONT_SIZE, -1)) {
174            preferences.putInt(FONT_SIZE, temp);
175        }
176        preferences.putBoolean(NONSTANDARD_MOUSE_EVENT, this.isNonStandardMouseEvent());
177        preferences.putBoolean(GRAPHIC_TABLE_STATE, this.isGraphicTableState());
178        preferences.putBoolean(OBLOCK_EDIT_TABBED, this.isOblockEditTabbed());
179        preferences.putBoolean(EDITOR_USE_OLD_LOC_SIZE, this.isEditorUseOldLocSize());
180        preferences.putInt(JFILECHOOSER_FORMAT, this.jFileChooserFormat);
181        preferences.putInt(MAX_COMBO_ROWS, this.getMaxComboRows());
182        preferences.putInt(SHOW_TOOL_TIP_TIME, this.getToolTipDismissDelay());
183        try {
184            preferences.sync();
185        } catch (BackingStoreException ex) {
186            log.error("Unable to save preferences.", ex);
187        }
188        this.setDirty(false);
189    }
190
191    /**
192     * @return the locale
193     */
194    public Locale getLocale() {
195        return locale;
196    }
197
198    /**
199     * @param locale the locale to set
200     */
201    public void setLocale(Locale locale) {
202        Locale oldLocale = this.locale;
203        this.locale = locale;
204        firePropertyChange(LOCALE, oldLocale, locale);
205    }
206
207    /**
208     * @return the currently selected font
209     */
210    public Font getFont() {
211        return currentFont;
212    }
213
214    /**
215     * Sets a new font
216     *
217     * @param newFont the new font to set
218     */
219    public void setFont(Font newFont) {
220        Font oldFont = this.currentFont;
221        this.currentFont = newFont;
222        firePropertyChange(FONT_NAME, oldFont, this.currentFont);
223    }
224
225    /**
226     * Sets a new font by name
227     *
228     * @param newFontName the name of the new font to set
229     */
230    public void setFontByName(String newFontName) {
231        Font oldFont = getFont();
232        if (oldFont == null) {
233            oldFont = this.getDefaultFont();
234        }
235        setFont(new Font(newFontName, oldFont.getStyle(), fontSize));
236    }
237
238    /**
239     * @return the current Look and Feel default font
240     */
241    public Font getDefaultFont() {
242        if (defaultFont == null) {
243            setDefaultFont();
244        }
245        return defaultFont;
246    }
247
248    /**
249     * Called to load the current Look and Feel default font, based on
250     * looking up the {@value #DEFAULT_FONT}.
251     */
252    public void setDefaultFont() {
253        java.util.Enumeration<Object> keys = UIManager.getDefaults().keys();
254        while (keys.hasMoreElements()) {
255            Object key = keys.nextElement();
256            Object value = UIManager.get(key);
257
258            if (value instanceof javax.swing.plaf.FontUIResource && key.toString().equals(DEFAULT_FONT)) {
259                Font f = UIManager.getFont(key);
260                log.debug("Key:{} Font: {}", key, f.getName());
261                defaultFont = f;
262                return;
263            }
264        }
265        // couldn't find the default return a reasonable font
266        defaultFont = UIManager.getFont(DEFAULT_FONT);
267        if (defaultFont == null) {
268            // or maybe not quite as reasonable
269            defaultFont = UIManager.getFont("TextArea.font");
270        }
271    }
272
273    /**
274     * @return the currently selected font size
275     */
276    public int getFontSize() {
277        if (fontSize == 0) {
278            return defaultFontSize;
279        }
280        return fontSize;
281    }
282
283    /**
284     * Set the new font size. If newFontSize is non-zero and less than
285     * {@value #MIN_FONT_SIZE}, the font size is set to {@value #MIN_FONT_SIZE}
286     * or if greater than {@value #MAX_FONT_SIZE}, the font size is set to
287     * {@value #MAX_FONT_SIZE}.
288     *
289     * @param newFontSize the new font size to set
290     */
291    public void setFontSize(int newFontSize) {
292        int oldFontSize = this.fontSize;
293        if (newFontSize != 0 && newFontSize < MIN_FONT_SIZE) {
294            this.fontSize = MIN_FONT_SIZE;
295        } else if (newFontSize > MAX_FONT_SIZE) {
296            this.fontSize = MAX_FONT_SIZE;
297        } else {
298            this.fontSize = newFontSize;
299        }
300        firePropertyChange(FONT_SIZE, oldFontSize, this.fontSize);
301    }
302
303    /**
304     * Get the default font size for the current Look and Feel.
305     *
306     * @return the default font size
307     */
308    public int getDefaultFontSize() {
309        return defaultFontSize;
310    }
311
312    /**
313     * Get the default font size for the current Look and Feel, based
314     * on looking up the {@value #DEFAULT_FONT} size.
315     */
316    public void setDefaultFontSize() {
317        java.util.Enumeration<Object> keys = UIManager.getDefaults().keys();
318        while (keys.hasMoreElements()) {
319            Object key = keys.nextElement();
320            Object value = UIManager.get(key);
321
322            if (value instanceof javax.swing.plaf.FontUIResource && key.toString().equals(DEFAULT_FONT)) {
323                Font f = UIManager.getFont(key);
324                log.debug("Key:{} Font: {} size: {}", key, f.getName(), f.getSize());
325                defaultFontSize = f.getSize();
326                return;
327            }
328        }
329        defaultFontSize = 11; // couldn't find the default return a reasonable
330                              // font size
331    }
332
333    /**
334     * Logs LAF fonts at the TRACE level.
335     */
336    private void logAllFonts() {
337        // avoid any activity if logging at this level is disabled to avoid
338        // the unnecessary overhead of getting the fonts
339        if (log.isTraceEnabled()) {
340            log.trace("******** LAF={}", UIManager.getLookAndFeel().getClass().getName());
341            java.util.Enumeration<Object> keys = UIManager.getDefaults().keys();
342            while (keys.hasMoreElements()) {
343                Object key = keys.nextElement();
344                Object value = UIManager.get(key);
345                if (value != null &&
346                        (value instanceof javax.swing.plaf.FontUIResource ||
347                                value instanceof java.awt.Font ||
348                                key.toString().endsWith(".font"))) {
349                    Font f = UIManager.getFont(key);
350                    log.trace("Class={}; Key: {} Font: {} size: {}", value.getClass().getName(), key, f.getName(),
351                            f.getSize());
352                }
353            }
354        }
355    }
356
357    /**
358     * Sets the time a tooltip is displayed before it goes away.
359     * <p>
360     * Note that this preference takes effect immediately.
361     *
362     * @param time the delay in seconds.
363     */
364    public void setToolTipDismissDelay(int time) {
365        int old = this.toolTipDismissDelay;
366        this.toolTipDismissDelay = time;
367        ToolTipManager.sharedInstance().setDismissDelay(time);
368        firePropertyChange(SHOW_TOOL_TIP_TIME, old, time);
369    }
370
371    /**
372     * Get the time a tooltip is displayed before being dismissed.
373     *
374     * @return the delay in seconds
375     */
376    public int getToolTipDismissDelay() {
377        return this.toolTipDismissDelay;
378    }
379
380    /**
381     * @return the nonStandardMouseEvent
382     */
383    public boolean isNonStandardMouseEvent() {
384        return nonStandardMouseEvent;
385    }
386
387    /**
388     * @param nonStandardMouseEvent the nonStandardMouseEvent to set
389     */
390    public void setNonStandardMouseEvent(boolean nonStandardMouseEvent) {
391        boolean oldNonStandardMouseEvent = this.nonStandardMouseEvent;
392        this.nonStandardMouseEvent = nonStandardMouseEvent;
393        firePropertyChange(NONSTANDARD_MOUSE_EVENT, oldNonStandardMouseEvent, nonStandardMouseEvent);
394    }
395
396    /**
397     * @return the graphicTableState
398     */
399    public boolean isGraphicTableState() {
400        return graphicTableState;
401    }
402
403    /**
404     * @param graphicTableState the graphicTableState to set
405     */
406    public void setGraphicTableState(boolean graphicTableState) {
407        boolean oldGraphicTableState = this.graphicTableState;
408        this.graphicTableState = graphicTableState;
409        firePropertyChange(GRAPHIC_TABLE_STATE, oldGraphicTableState, graphicTableState);
410    }
411
412    /**
413     * @return the graphicTableState
414     */
415    public boolean isOblockEditTabbed() {
416        return oblockEditTabbed;
417    }
418
419    /**
420     * @param tabbed the Editor interface to set (fasle  = desktop)
421     */
422    public void setOblockEditTabbed(boolean tabbed) {
423        boolean oldOblockTabbed = this.oblockEditTabbed;
424        this.oblockEditTabbed = tabbed;
425        firePropertyChange(OBLOCK_EDIT_TABBED, oldOblockTabbed, tabbed);
426    }
427
428    /**
429     * @return the number of combo box rows to be displayed.
430     */
431    public int getMaxComboRows() {
432        return maxComboRows;
433    }
434
435    /**
436     * Set a new value for the number of combo box rows to be displayed.
437     * @param maxRows The new value, zero for no limit
438     */
439    public void setMaxComboRows(int maxRows) {
440        maxComboRows = maxRows;
441    }
442
443    /**
444     * @return the editorUseOldLocSize value
445     */
446    public boolean isEditorUseOldLocSize() {
447        return editorUseOldLocSize;
448    }
449
450    /**
451     * @param editorUseOldLocSize the editorUseOldLocSize value to set
452     */
453    public void setEditorUseOldLocSize(boolean editorUseOldLocSize) {
454        boolean oldEditorUseOldLocSize = this.editorUseOldLocSize;
455        this.editorUseOldLocSize = editorUseOldLocSize;
456        firePropertyChange(EDITOR_USE_OLD_LOC_SIZE, oldEditorUseOldLocSize, editorUseOldLocSize);
457    }
458
459    /**
460     * JFileChooser Type
461     * @return 0 default, 1 List 2 Detail
462     */
463    public int getJFileChooserFormat() {
464        return jFileChooserFormat;
465    }
466
467    /**
468     * @param jFileChooserFormat the JFileChooser 0 default, 1 list, 2 detail
469     */
470    public void setJFileChooserFormat( int jFileChooserFormat) {
471        int oldjFileChooserFormat = this.jFileChooserFormat;
472        this.jFileChooserFormat = jFileChooserFormat;
473        firePropertyChange(JFILECHOOSER_FORMAT, oldjFileChooserFormat, jFileChooserFormat);
474    }
475
476    /**
477     * Get the name of the class implementing the preferred look and feel. Note
478     * this may not be the in-use look and feel if the preferred look and feel
479     * is not available on the current platform; and will be overwritten if
480     * preferences are saved on a platform where the preferred look and feel is
481     * not available.
482     *
483     * @return the look and feel class name
484     */
485    public String getLookAndFeel() {
486        return lookAndFeel;
487    }
488
489    /**
490     * Set the name of the class implementing the preferred look and feel. Note
491     * this change only takes effect after the application is restarted, because
492     * Java has some issues setting the look and feel correctly on already open
493     * windows.
494     *
495     * @param lookAndFeel the look and feel class name
496     */
497    public void setLookAndFeel(String lookAndFeel) {
498        String oldLookAndFeel = this.lookAndFeel;
499        this.lookAndFeel = lookAndFeel;
500        firePropertyChange(LOOK_AND_FEEL, oldLookAndFeel, lookAndFeel);
501        // the actual change to the LAF will happen when that event reaches `applyLookAndFeel` below
502    }
503
504    /**
505     * Apply the existing look and feel.
506     */
507    public void applyLookAndFeel() {
508        String lafClassName = lookAndFeel;
509        for (LookAndFeelInfo LAF : UIManager.getInstalledLookAndFeels()) {
510            // accept either name or classname of look and feel
511            if (LAF.getClassName().equals(this.lookAndFeel) || LAF.getName().equals(this.lookAndFeel)) {
512                lafClassName = LAF.getClassName();
513                break; // use first match, not last match (unlikely to be
514                       // different, but you never know)
515            }
516        }
517        log.debug("Look and feel selection \"{}\" ({})", this.lookAndFeel, lafClassName);
518        if (lafClassName != null) {
519            if (!lafClassName.equals(UIManager.getLookAndFeel().getClass().getName())) {
520                log.debug("Apply look and feel \"{}\" ({})", this.lookAndFeel, lafClassName);
521                final String localLafClassName = lafClassName;  // final for thread invoke
522                jmri.util.ThreadingUtil.runOnGUI(() -> {
523                    try {
524                        if (localLafClassName.startsWith("com.github.weisj.darklaf") ) {
525                            // DarkLAF special case - will have to use reflection if we have more than one in GuiLafConfigPane
526                            com.github.weisj.darklaf.LafManager.install(new com.github.weisj.darklaf.theme.HighContrastDarkTheme());
527                        } else {
528                            // Swing-handled class name
529                            UIManager.setLookAndFeel(localLafClassName);
530                        }
531                    } catch (ClassNotFoundException ex) {
532                        log.error("Could not find look and feel \"{}\".", this.lookAndFeel);
533                    } catch (
534                            IllegalAccessException |
535                            InstantiationException ex) {
536                        log.error("Could not load look and feel \"{}\".", this.lookAndFeel);
537                    } catch (UnsupportedLookAndFeelException ex) {
538                        log.error("Look and feel \"{}\" is not supported on this platform.", this.lookAndFeel);
539                    }
540                });
541            } else {
542                log.debug("Not updating look and feel {} matching existing look and feel", lafClassName);
543            }
544        }
545    }
546
547    /**
548     * Applies a new calculated font size to all found fonts.
549     * <p>
550     * Calls {@link #getCalcFontSize(int) getCalcFontSize} to calculate new size
551     * for each.
552     */
553    private void applyFontSize() {
554        if (log.isTraceEnabled()) {
555            logAllFonts();
556        }
557        if (this.getFontSize() != this.getDefaultFontSize()) {
558            Enumeration<Object> keys = UIManager.getDefaults().keys();
559            while (keys.hasMoreElements()) {
560                Object key = keys.nextElement();
561                Object value = UIManager.get(key);
562                if (value != null &&
563                        (value instanceof javax.swing.plaf.FontUIResource ||
564                                value instanceof java.awt.Font ||
565                                key.toString().endsWith(".font"))) {
566                    UIManager.put(key, UIManager.getFont(key).deriveFont(((Font) value).getStyle(),
567                            getCalcFontSize(((Font) value).getSize())));
568                }
569            }
570            if (log.isTraceEnabled()) {
571                logAllFonts();
572            }
573        }
574    }
575
576    /**
577     * Stand-alone service routine to set the default Locale.
578     * <p>
579     * Intended to be invoked early, as soon as a profile is available, to
580     * ensure the correct language is set as startup proceeds. Must be followed
581     * eventually by a complete {@link #setLocale}.
582     *
583     * @param profile The profile to get the locale from
584     */
585    @SuppressWarnings("deprecation")    // The constructor Locale(String) is deprecated since version 19
586                                        // The replacement Locale.of(String) isn't available before version 19
587    public static void setLocaleMinimally(Profile profile) {
588        // en is default if a locale preference has not been set
589        String name = ProfileUtils.getPreferences(profile, GuiLafPreferencesManager.class, true).get("locale", "en");
590        log.debug("setLocaleMinimally found language {}, setting", name);
591        Locale.setDefault(new Locale(name));
592        javax.swing.JComponent.setDefaultLocale(new Locale(name));
593    }
594
595    /**
596     * @return a new calculated font size based on difference between default
597     *         size and selected size
598     * @param oldSize the old font size
599     */
600    private int getCalcFontSize(int oldSize) {
601        return oldSize + (this.getFontSize() - this.getDefaultFontSize());
602    }
603
604    /**
605     * Check if preferences need to be saved.
606     *
607     * @return true if preferences need to be saved
608     */
609    public boolean isDirty() {
610        return dirty;
611    }
612
613    /**
614     * Set dirty state.
615     *
616     * @param dirty true if preferences need to be saved
617     */
618    private void setDirty(boolean dirty) {
619        if (this.initialized) {
620            boolean oldDirty = this.dirty;
621            this.dirty = dirty;
622            super.firePropertyChange(PROP_DIRTY, oldDirty, dirty);
623        }
624    }
625
626    /**
627     * Check if application needs to restart to apply preferences.
628     *
629     * @return true if preferences are only applied on application start
630     */
631    public boolean isRestartRequired() {
632        return restartRequired;
633    }
634
635    /**
636     * Set restart required state. Sets the state to true if
637     * {@link #isInitialized(jmri.profile.Profile)} is true.
638     */
639    private void setRestartRequired() {
640        if (initialized && !restartRequired) {
641            restartRequired = true;
642            super.firePropertyChange(PROP_RESTARTREQUIRED, false, restartRequired);
643        }
644    }
645
646    /**
647     * {@inheritDoc}
648     */
649    @Override
650    public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) {
651        if (oldValue != newValue) {
652            setDirty(true);
653            setRestartRequired();
654            super.firePropertyChange(propertyName, oldValue, newValue);
655        }
656    }
657
658    /**
659     * {@inheritDoc}
660     */
661    @Override
662    public void firePropertyChange(String propertyName, int oldValue, int newValue) {
663        if (oldValue != newValue) {
664            setDirty(true);
665            setRestartRequired();
666            super.firePropertyChange(propertyName, oldValue, newValue);
667        }
668    }
669
670    /**
671     * {@inheritDoc}
672     */
673    @Override
674    public void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
675        if (oldValue == null || newValue == null || oldValue != newValue) {
676            setDirty(true);
677            setRestartRequired();
678            super.firePropertyChange(propertyName, oldValue, newValue);
679        }
680    }
681
682    @Override
683    public boolean isInitializedWithExceptions(Profile profile) {
684        return this.initialized && !this.exceptions.isEmpty();
685    }
686
687    @Override
688    @Nonnull
689    public List<Exception> getInitializationExceptions(Profile profile) {
690        return new ArrayList<>(this.exceptions);
691    }
692
693}