Ticket #13309: patch-preferences-listenable-2.patch

File patch-preferences-listenable-2.patch, 86.8 KB (added by michael2402, 8 years ago)
  • src/org/openstreetmap/josm/data/Preferences.java

    diff --git a/src/org/openstreetmap/josm/data/Preferences.java b/src/org/openstreetmap/josm/data/Preferences.java
    index f79ba40..68f4cf3 100644
    a b import java.util.ResourceBundle;  
    3636import java.util.Set;
    3737import java.util.SortedMap;
    3838import java.util.TreeMap;
    39 import java.util.concurrent.CopyOnWriteArrayList;
    4039import java.util.function.Predicate;
    4140import java.util.regex.Matcher;
    4241import java.util.regex.Pattern;
     42import java.util.stream.Stream;
    4343
    4444import javax.json.Json;
    4545import javax.json.JsonArray;
    import javax.xml.stream.XMLStreamException;  
    5555
    5656import org.openstreetmap.josm.Main;
    5757import org.openstreetmap.josm.data.preferences.ColorProperty;
     58import org.openstreetmap.josm.data.preferences.DoubleProperty;
     59import org.openstreetmap.josm.data.preferences.IntegerProperty;
    5860import org.openstreetmap.josm.data.preferences.ListListSetting;
    5961import org.openstreetmap.josm.data.preferences.ListSetting;
     62import org.openstreetmap.josm.data.preferences.LongProperty;
    6063import org.openstreetmap.josm.data.preferences.MapListSetting;
    6164import org.openstreetmap.josm.data.preferences.PreferencesReader;
    6265import org.openstreetmap.josm.data.preferences.PreferencesWriter;
    import org.openstreetmap.josm.io.OfflineAccessException;  
    6669import org.openstreetmap.josm.io.OnlineResource;
    6770import org.openstreetmap.josm.tools.CheckParameterUtil;
    6871import org.openstreetmap.josm.tools.ColorHelper;
    69 import org.openstreetmap.josm.tools.FilteredCollection;
    7072import org.openstreetmap.josm.tools.I18n;
     73import org.openstreetmap.josm.tools.ListenerList;
    7174import org.openstreetmap.josm.tools.MultiMap;
    7275import org.openstreetmap.josm.tools.Utils;
    7376import org.xml.sax.SAXException;
    public class Preferences {  
    214217        }
    215218    }
    216219
     220    /**
     221     * Old color interface
     222     * <p>
     223     * To be removed: end of 2016
     224     * @deprecated Use a {@link ColorProperty} instead.
     225     */
     226    @Deprecated
    217227    public interface ColorKey {
    218228        String getColorName();
    219229
    public class Preferences {  
    222232        Color getDefaultValue();
    223233    }
    224234
    225     private final CopyOnWriteArrayList<PreferenceChangedListener> listeners = new CopyOnWriteArrayList<>();
     235    private final ListenerList<PreferenceChangedListener> listeners = ListenerList.create();
     236
     237    private final HashMap<String, ListenerList<PreferenceChangedListener>> keyListeners = new HashMap<>();
    226238
    227239    /**
    228240     * Adds a new preferences listener.
    public class Preferences {  
    230242     */
    231243    public void addPreferenceChangeListener(PreferenceChangedListener listener) {
    232244        if (listener != null) {
    233             listeners.addIfAbsent(listener);
     245            listeners.addListener(listener);
    234246        }
    235247    }
    236248
    public class Preferences {  
    239251     * @param listener The listener to remove
    240252     */
    241253    public void removePreferenceChangeListener(PreferenceChangedListener listener) {
    242         listeners.remove(listener);
     254        listeners.removeListener(listener);
     255    }
     256
     257    /**
     258     * Adds a listener that only listens to changes in one preference
     259     * @param key The preference key to listen to
     260     * @param listener The listener to add.
     261     */
     262    public void addKeyPreferenceChangeListener(String key, PreferenceChangedListener listener) {
     263        listenersForKey(key).addListener(listener);
     264    }
     265
     266    /**
     267     * Adds a weak listener that only listens to changes in one preference
     268     * @param key The preference key to listen to
     269     * @param listener The listener to add.
     270     */
     271    public void addWeakKeyPreferenceChangeListener(String key, PreferenceChangedListener listener) {
     272        listenersForKey(key).addWeakListener(listener);
     273    }
     274
     275    private ListenerList<PreferenceChangedListener> listenersForKey(String key) {
     276        ListenerList<PreferenceChangedListener> keyListener = keyListeners.get(key);
     277        if (keyListener == null) {
     278            keyListener = ListenerList.create();
     279            keyListeners.put(key, keyListener);
     280        }
     281        return keyListener;
     282    }
     283
     284    /**
     285     * Removes a listener that only listens to changes in one preference
     286     * @param key The preference key to listen to
     287     * @param listener The listener to add.
     288     */
     289    public void removeKeyPreferenceChangeListener(String key, PreferenceChangedListener listener) {
     290        ListenerList<PreferenceChangedListener> keyListener = keyListeners.get(key);
     291        if (keyListener == null) {
     292            throw new IllegalArgumentException("There are no listeners registered for " + key);
     293        }
     294        keyListener.removeListener(listener);
    243295    }
    244296
    245297    protected void firePreferenceChanged(String key, Setting<?> oldValue, Setting<?> newValue) {
    246         PreferenceChangeEvent evt = new DefaultPreferenceChangeEvent(key, oldValue, newValue);
    247         for (PreferenceChangedListener l : listeners) {
    248             l.preferenceChanged(evt);
     298        final PreferenceChangeEvent evt = new DefaultPreferenceChangeEvent(key, oldValue, newValue);
     299        listeners.fireEvent(listener -> listener.preferenceChanged(evt));
     300
     301        ListenerList<PreferenceChangedListener> forKey = keyListeners.get(key);
     302        if (forKey != null) {
     303            forKey.fireEvent(listener -> listener.preferenceChanged(evt));
    249304        }
    250305    }
    251306
    public class Preferences {  
    478533     * @return {@code true}, if something has changed (i.e. value is different than before)
    479534     */
    480535    public boolean put(final String key, String value) {
    481         if (value != null && value.isEmpty()) {
    482             value = null;
    483         }
    484         return putSetting(key, value == null ? null : new StringSetting(value));
     536        return putSetting(key, value == null || value.isEmpty() ? null : new StringSetting(value));
    485537    }
    486538
     539    /**
     540     * Set a boolean value for a certain setting.
     541     * @param key the unique identifier for the setting
     542     * @param value The new value
     543     * @return {@code true}, if something has changed (i.e. value is different than before)
     544     * @see BooleanProperty
     545     */
    487546    public boolean put(final String key, final boolean value) {
    488547        return put(key, Boolean.toString(value));
    489548    }
    490549
     550    /**
     551     * Set a boolean value for a certain setting.
     552     * @param key the unique identifier for the setting
     553     * @param value The new value
     554     * @return {@code true}, if something has changed (i.e. value is different than before)
     555     * @see IntegerProperty
     556     */
    491557    public boolean putInteger(final String key, final Integer value) {
    492558        return put(key, Integer.toString(value));
    493559    }
    494560
     561    /**
     562     * Set a boolean value for a certain setting.
     563     * @param key the unique identifier for the setting
     564     * @param value The new value
     565     * @return {@code true}, if something has changed (i.e. value is different than before)
     566     * @see DoubleProperty
     567     */
    495568    public boolean putDouble(final String key, final Double value) {
    496569        return put(key, Double.toString(value));
    497570    }
    498571
     572    /**
     573     * Set a boolean value for a certain setting.
     574     * @param key the unique identifier for the setting
     575     * @param value The new value
     576     * @return {@code true}, if something has changed (i.e. value is different than before)
     577     * @see LongProperty
     578     */
    499579    public boolean putLong(final String key, final Long value) {
    500580        return put(key, Long.toString(value));
    501581    }
    public class Preferences {  
    505585     * @throws IOException if any I/O error occurs
    506586     */
    507587    public synchronized void save() throws IOException {
    508         save(getPreferenceFile(),
    509                 new FilteredCollection<>(settingsMap.entrySet(), NO_DEFAULT_SETTINGS_ENTRY), false);
     588        save(getPreferenceFile(), settingsMap.entrySet().stream().filter(NO_DEFAULT_SETTINGS_ENTRY), false);
    510589    }
    511590
    512591    public synchronized void saveDefaults() throws IOException {
    513         save(getDefaultsCacheFile(), defaultsMap.entrySet(), true);
     592        save(getDefaultsCacheFile(), defaultsMap.entrySet().stream(), true);
    514593    }
    515594
    516     protected void save(File prefFile, Collection<Entry<String, Setting<?>>> settings, boolean defaults) throws IOException {
    517 
     595    protected void save(File prefFile, Stream<Entry<String, Setting<?>>> settings, boolean defaults) throws IOException {
    518596        if (!defaults) {
    519597            /* currently unused, but may help to fix configuration issues in future */
    520598            putInteger("josm.version", Version.getInstance().getVersion());
    public class Preferences {  
    718796
    719797    /**
    720798     * Convenience method for accessing colour preferences.
     799     * <p>
     800     * To be removed: end of 2016
    721801     *
    722802     * @param colName name of the colour
    723803     * @param def default value
    724804     * @return a Color object for the configured colour, or the default value if none configured.
     805     * @deprecated Use a {@link ColorProperty} instead.
    725806     */
     807    @Deprecated
    726808    public synchronized Color getColor(String colName, Color def) {
    727809        return getColor(colName, null, def);
    728810    }
    public class Preferences {  
    742824
    743825    /**
    744826     * Returns the color for the given key.
     827     * <p>
     828     * To be removed: end of 2016
    745829     * @param key The color key
    746830     * @return the color
     831     * @deprecated Use a {@link ColorProperty} instead.
    747832     */
     833    @Deprecated
    748834    public Color getColor(ColorKey key) {
    749835        return getColor(key.getColorName(), key.getSpecialName(), key.getDefaultValue());
    750836    }
    751837
    752838    /**
    753839     * Convenience method for accessing colour preferences.
    754      *
     840     * <p>
     841     * To be removed: end of 2016
    755842     * @param colName name of the colour
    756843     * @param specName name of the special colour settings
    757844     * @param def default value
    758845     * @return a Color object for the configured colour, or the default value if none configured.
     846     * @deprecated Use a {@link ColorProperty} instead. You can replace this by: <code>new ColorProperty(colName, def).getChildColor(specName)</code>
    759847     */
     848    @Deprecated
    760849    public synchronized Color getColor(String colName, String specName, Color def) {
    761850        String colKey = ColorProperty.getColorKey(colName);
    762         if (!colKey.equals(colName)) {
    763             colornames.put(colKey, colName);
    764         }
     851        registerColor(colKey, colName);
    765852        String colStr = specName != null ? get("color."+specName) : "";
    766853        if (colStr.isEmpty()) {
    767854            colStr = get("color." + colKey, ColorHelper.color2html(def, true));
    public class Preferences {  
    773860        }
    774861    }
    775862
     863    /**
     864     * Registers a color name conversion for the global color registry.
     865     * @param colKey The key
     866     * @param colName The name of the color.
     867     */
     868    public void registerColor(String colKey, String colName) {
     869        if (!colKey.equals(colName)) {
     870            colornames.put(colKey, colName);
     871        }
     872    }
     873
    776874    public synchronized Color getDefaultColor(String colKey) {
    777875        StringSetting col = Utils.cast(defaultsMap.get("color."+colKey), StringSetting.class);
    778876        String colStr = col == null ? null : col.getValue();
  • src/org/openstreetmap/josm/data/osm/visitor/paint/PaintColors.java

    diff --git a/src/org/openstreetmap/josm/data/osm/visitor/paint/PaintColors.java b/src/org/openstreetmap/josm/data/osm/visitor/paint/PaintColors.java
    index fb9f625..7406ff5 100644
    a b import static org.openstreetmap.josm.tools.I18n.marktr;  
    66import java.awt.Color;
    77import java.util.List;
    88
    9 import org.openstreetmap.josm.Main;
    10 import org.openstreetmap.josm.data.Preferences.ColorKey;
     9import org.openstreetmap.josm.data.preferences.CachingProperty;
     10import org.openstreetmap.josm.data.preferences.ColorProperty;
    1111import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
    1212import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.MapPaintSylesUpdateListener;
    1313import org.openstreetmap.josm.gui.mappaint.StyleSource;
    1414
    15 public enum PaintColors implements ColorKey {
     15public enum PaintColors {
    1616
    1717    INACTIVE(marktr("inactive"), Color.darkGray),
    1818    SELECTED(marktr("selected"), Color.red),
    public enum PaintColors implements ColorKey {  
    3333
    3434    private final String name;
    3535    private final Color defaultColor;
     36    private final CachingProperty<Color> property;
    3637
    3738    private static volatile Color backgroundColorCache;
    3839
    3940    private static final MapPaintSylesUpdateListener styleOverrideListener = new MapPaintSylesUpdateListener() {
    40 
     41        //TODO: Listen to wireframe map mode changes.
    4142        @Override
    4243        public void mapPaintStylesUpdated() {
    4344            backgroundColorCache = null;
    public enum PaintColors implements ColorKey {  
    5455    }
    5556
    5657    PaintColors(String name, Color defaultColor) {
     58        property = new ColorProperty(name, defaultColor).cached();
    5759        this.name = name;
    5860        this.defaultColor = defaultColor;
    5961    }
    6062
    61     @Override
    62     public String getColorName() {
    63         return name;
    64     }
    65 
    66     @Override
    6763    public Color getDefaultValue() {
    68         return defaultColor;
    69     }
    70 
    71     @Override
    72     public String getSpecialName() {
    73         return null;
     64        return property.getDefaultValue();
    7465    }
    7566
    7667    public Color get() {
    77         return Main.pref.getColor(this);
    78     }
    79 
    80     public static void getColors() {
    81         for (PaintColors c:values()) {
    82             c.get();
    83         }
     68        return property.get();
    8469    }
    8570
    8671    public static Color getBackgroundColor() {
    public enum PaintColors implements ColorKey {  
    9782            }
    9883        }
    9984        if (backgroundColorCache == null) {
    100             backgroundColorCache = BACKGROUND.get();
     85            return BACKGROUND.get();
     86        } else {
     87            return backgroundColorCache;
    10188        }
    102         return backgroundColorCache;
    10389    }
    10490}
  • src/org/openstreetmap/josm/data/preferences/AbstractProperty.java

    diff --git a/src/org/openstreetmap/josm/data/preferences/AbstractProperty.java b/src/org/openstreetmap/josm/data/preferences/AbstractProperty.java
    index 1516d42..2a5bff8 100644
    a b  
    22package org.openstreetmap.josm.data.preferences;
    33
    44import org.openstreetmap.josm.Main;
     5import org.openstreetmap.josm.data.Preferences;
     6import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
     7import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
    58
    69/**
    710 * Captures the common functionality of preference properties
    811 * @param <T> The type of object accessed by this property
    912 */
    1013public abstract class AbstractProperty<T> {
     14
     15    private final class PreferenceChangedListenerAdapter implements PreferenceChangedListener {
     16        private ValueChangeListener<? super T> listener;
     17
     18        public PreferenceChangedListenerAdapter(ValueChangeListener<? super T> listener) {
     19            this.listener = listener;
     20        }
     21
     22        @Override
     23        public void preferenceChanged(PreferenceChangeEvent e) {
     24            listener.valueChanged(new ValueChangeEvent<>(e, AbstractProperty.this));
     25        }
     26
     27        /* (non-Javadoc)
     28         * @see java.lang.Object#hashCode()
     29         */
     30        @Override
     31        public int hashCode() {
     32            final int prime = 31;
     33            int result = 1;
     34            result = prime * result + getOuterType().hashCode();
     35            result = prime * result + ((listener == null) ? 0 : listener.hashCode());
     36            return result;
     37        }
     38
     39        /* (non-Javadoc)
     40         * @see java.lang.Object#equals(java.lang.Object)
     41         */
     42        @Override
     43        public boolean equals(Object obj) {
     44            if (this == obj)
     45                return true;
     46            if (obj == null)
     47                return false;
     48            if (getClass() != obj.getClass())
     49                return false;
     50            PreferenceChangedListenerAdapter other = (PreferenceChangedListenerAdapter) obj;
     51            if (!getOuterType().equals(other.getOuterType()))
     52                return false;
     53            if (listener == null) {
     54                if (other.listener != null)
     55                    return false;
     56            } else if (!listener.equals(other.listener))
     57                return false;
     58            return true;
     59        }
     60
     61        private AbstractProperty<T> getOuterType() {
     62            return AbstractProperty.this;
     63        }
     64
     65        @Override
     66        public String toString() {
     67            return "PreferenceChangedListenerAdapter [listener=" + listener + "]";
     68        }
     69    }
     70
     71    /**
     72     * A listener that listens to changes in the properties value.
     73     * @author michael
     74     *
     75     */
     76    public interface ValueChangeListener<T> {
     77        public void valueChanged(ValueChangeEvent<? extends T> e);
     78    }
     79
     80    /**
     81     * An event that is triggered if the value of a property changes.
     82     * @author Michael Zangl
     83     * @param <T>
     84     * @since xxx
     85     */
     86    public static class ValueChangeEvent<T> {
     87        private final PreferenceChangeEvent base;
     88
     89        private final AbstractProperty<T> source;
     90
     91        ValueChangeEvent(PreferenceChangeEvent base, AbstractProperty<T> source) {
     92            this.base = base;
     93            this.source = source;
     94        }
     95
     96        /**
     97         * Get the property that was changed
     98         * @return The property.
     99         */
     100        public AbstractProperty<T> getProperty() {
     101            return source;
     102        }
     103    }
     104
     105    /**
     106     * An exception that is thrown if a preference value is invalid.
     107     * @author Michael Zangl
     108     */
     109    public static class InvalidPreferenceValueException extends RuntimeException {
     110
     111        public InvalidPreferenceValueException() {
     112            super();
     113        }
     114
     115        public InvalidPreferenceValueException(String message, Throwable cause) {
     116            super(message, cause);
     117        }
     118
     119        public InvalidPreferenceValueException(String message) {
     120            super(message);
     121        }
     122
     123        public InvalidPreferenceValueException(Throwable cause) {
     124            super(cause);
     125        }
     126    }
     127
     128    /**
     129     * The preferences object this property is for.
     130     */
     131    protected final Preferences preferences;
    11132    protected final String key;
    12133    protected final T defaultValue;
    13134
    public abstract class AbstractProperty<T> {  
    18139     * @since 5464
    19140     */
    20141    public AbstractProperty(String key, T defaultValue) {
     142        // Main.pref should not change in production but may change during tests.
     143        preferences = Main.pref;
    21144        this.key = key;
    22145        this.defaultValue = defaultValue;
    23146    }
    24147
    25148    /**
     149     * Store the default value to {@link Preferences}.
     150     */
     151    protected void storeDefaultValue() {
     152        if (getPreferences() != null) {
     153            get();
     154        }
     155    }
     156
     157    /**
    26158     * Replies the property key.
    27159     * @return The property key
    28160     */
    public abstract class AbstractProperty<T> {  
    35167     * @return true if {@code Main.pref} contains this property.
    36168     */
    37169    public boolean isSet() {
    38         return !Main.pref.get(key).isEmpty();
     170        return !getPreferences().get(key).isEmpty();
    39171    }
    40172
    41173    /**
    public abstract class AbstractProperty<T> {  
    50182     * Removes this property from JOSM preferences (i.e replace it by its default value).
    51183     */
    52184    public void remove() {
    53         Main.pref.put(getKey(), String.valueOf(getDefaultValue()));
     185        put(getDefaultValue());
    54186    }
    55187
    56188    /**
    public abstract class AbstractProperty<T> {  
    67199     * @since 5464
    68200     */
    69201    public abstract boolean put(T value);
     202
     203    /**
     204     * Gets the preferences used for this property.
     205     * @return The preferences for this property.
     206     * @since xxx
     207     */
     208    protected Preferences getPreferences() {
     209        return preferences;
     210    }
     211
     212    /**
     213     * Adds a listener that listens only for changes to this preference key.
     214     * @param listener The listener to add.
     215     */
     216    public void addListener(ValueChangeListener<? super T> listener) {
     217        addListenerImpl(new PreferenceChangedListenerAdapter(listener));
     218    }
     219
     220    protected void addListenerImpl(PreferenceChangedListener adapter) {
     221        getPreferences().addKeyPreferenceChangeListener(getKey(), adapter);
     222    }
     223
     224    /**
     225     * Adds a weak listener that listens only for changes to this preference key.
     226     * @param listener The listener to add.
     227     */
     228    public void addWeakListener(ValueChangeListener<? super T> listener) {
     229        addWeakListenerImpl(new PreferenceChangedListenerAdapter(listener));
     230    }
     231
     232    protected void addWeakListenerImpl(PreferenceChangedListener adapter) {
     233        getPreferences().addWeakKeyPreferenceChangeListener(getKey(), adapter);
     234    }
     235
     236    /**
     237     * Removes a listener that listens only for changes to this preference key.
     238     * @param listener The listener to add.
     239     */
     240    public void removeListener(ValueChangeListener<? super T> listener) {
     241        removeListenerImpl(new PreferenceChangedListenerAdapter(listener));
     242    }
     243
     244    protected void removeListenerImpl(PreferenceChangedListener adapter) {
     245        getPreferences().removeKeyPreferenceChangeListener(getKey(), adapter);
     246    }
     247
     248    @Override
     249    public int hashCode() {
     250        final int prime = 31;
     251        int result = 1;
     252        result = prime * result + ((key == null) ? 0 : key.hashCode());
     253        result = prime * result + ((preferences == null) ? 0 : preferences.hashCode());
     254        return result;
     255    }
     256
     257    @Override
     258    public boolean equals(Object obj) {
     259        if (this == obj)
     260            return true;
     261        if (obj == null)
     262            return false;
     263        if (getClass() != obj.getClass())
     264            return false;
     265        AbstractProperty other = (AbstractProperty) obj;
     266        if (key == null) {
     267            if (other.key != null)
     268                return false;
     269        } else if (!key.equals(other.key))
     270            return false;
     271        if (preferences == null) {
     272            if (other.preferences != null)
     273                return false;
     274        } else if (!preferences.equals(other.preferences))
     275            return false;
     276        return true;
     277    }
    70278}
  • new file src/org/openstreetmap/josm/data/preferences/AbstractToStringProperty.java

    diff --git a/src/org/openstreetmap/josm/data/preferences/AbstractToStringProperty.java b/src/org/openstreetmap/josm/data/preferences/AbstractToStringProperty.java
    new file mode 100644
    index 0000000..6a3a0b9
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.preferences;
     3
     4import org.openstreetmap.josm.Main;
     5import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
     6import org.openstreetmap.josm.tools.CheckParameterUtil;
     7import org.openstreetmap.josm.tools.bugreport.BugReport;
     8
     9/**
     10 * This class represents a property that can be represented as String.
     11 *
     12 * @author Michael Zangl
     13 *
     14 * @param <T> The property content type.
     15 * @since xxx
     16 */
     17public abstract class AbstractToStringProperty<T> extends AbstractProperty<T> {
     18
     19    /**
     20     * This is a version of this property that attempts to get the property with a more specialized key and - if that fails - uses the property
     21     * value as default.
     22     *
     23     * @author Michael Zangl
     24     * @param <T> The content type
     25     * @since xxx
     26     */
     27    public static class ChildProperty<T> extends AbstractToStringProperty<T> {
     28        private AbstractToStringProperty<T> parent;
     29
     30        ChildProperty(AbstractToStringProperty<T> parent, String key) {
     31            super(key, null);
     32            CheckParameterUtil.ensureParameterNotNull(parent, "parent");
     33            this.parent = parent;
     34        }
     35
     36        @Override
     37        protected void storeDefaultValue() {
     38            // Default value hidden in preferences.
     39        }
     40
     41        @Override
     42        public T getDefaultValue() {
     43            return parent.get();
     44        }
     45
     46        @Override
     47        protected T fromString(String string) {
     48            return parent.fromString(string);
     49        }
     50
     51        @Override
     52        protected String toString(T t) {
     53            return parent.toString(t);
     54        }
     55
     56        @Override
     57        protected void addListenerImpl(PreferenceChangedListener adapter) {
     58            super.addListenerImpl(adapter);
     59            parent.addListenerImpl(adapter);
     60        }
     61
     62        @Override
     63        protected void addWeakListenerImpl(PreferenceChangedListener adapter) {
     64            super.addWeakListenerImpl(adapter);
     65            parent.addWeakListenerImpl(adapter);
     66        }
     67
     68        @Override
     69        protected void removeListenerImpl(PreferenceChangedListener adapter) {
     70            super.removeListenerImpl(adapter);
     71            parent.removeListenerImpl(adapter);
     72        }
     73
     74        @Override
     75        public CachingProperty<T> cached() {
     76            throw new UnsupportedOperationException("Not implemented yet.");
     77        }
     78
     79    }
     80
     81    /**
     82     * Create a new property and store the default value.
     83     * @param key The key
     84     * @param defaultValue The default value.
     85     * @see AbstractProperty#AbstractProperty(String, Object)
     86     */
     87    public AbstractToStringProperty(String key, T defaultValue) {
     88        super(key, defaultValue);
     89        storeDefaultValue();
     90    }
     91
     92    @Override
     93    public T get() {
     94        String string = getAsString();
     95        if (!string.isEmpty()) {
     96            try {
     97                return fromString(string);
     98            } catch (InvalidPreferenceValueException e) {
     99                Main.warn(BugReport.intercept(e).put("key", key).put("value", string));
     100            }
     101        }
     102        return getDefaultValue();
     103    }
     104
     105    /**
     106     * Converts the string to an object of the given type.
     107     * @param string The string
     108     * @return The object.
     109     * @throws InvalidPreferenceValueException If the value could not be converted.
     110     * @since xxx
     111     */
     112    protected abstract T fromString(String string);
     113
     114    @Override
     115    public boolean put(T value) {
     116        String string = value == null ? null : toString(value);
     117        return getPreferences().put(getKey(), string);
     118    }
     119
     120    /**
     121     * Converts the string to an object of the given type.
     122     * @param t The object.
     123     * @return The string representing the object
     124     * @throws InvalidPreferenceValueException If the value could not be converted.
     125     * @since xxx
     126     */
     127    protected abstract String toString(T t);
     128
     129    /**
     130     * Gets the preference value as String.
     131     * @return The string preference value.
     132     */
     133    protected String getAsString() {
     134        T def = getDefaultValue();
     135        return getPreferences().get(key, def == null ? "" : toString(def));
     136    }
     137
     138    /**
     139     * Gets a specialized setting value that has the current value as default
     140     * <p>
     141     * The key will be getKey().spec
     142     * @param spec The key specialization
     143     * @return The property
     144     */
     145    public AbstractToStringProperty<T> getSpecialized(String spec) {
     146        return getChildProperty(getKey() + "." + spec);
     147    }
     148
     149    /**
     150     * Gets a setting that defaults to this setting if the key is not set.
     151     * @param key The more specialized key.
     152     * @return The new setting.
     153     */
     154    protected AbstractToStringProperty<T> getChildProperty(String key) {
     155        return new ChildProperty<>(this, key);
     156    }
     157
     158    /**
     159     * Creates a new {@link CachingProperty} instance for this property.
     160     * @return The new caching property instance.
     161     */
     162    public CachingProperty<T> cached() {
     163        return new CachingProperty<>(this);
     164    }
     165}
  • src/org/openstreetmap/josm/data/preferences/BooleanProperty.java

    diff --git a/src/org/openstreetmap/josm/data/preferences/BooleanProperty.java b/src/org/openstreetmap/josm/data/preferences/BooleanProperty.java
    index 578edca..93ab3b1 100644
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.preferences;
    33
    4 import org.openstreetmap.josm.Main;
    5 
    64/**
    75 * A property containing a {@code Boolean} value.
    86 */
    9 public class BooleanProperty extends AbstractProperty<Boolean> {
     7public class BooleanProperty extends AbstractToStringProperty<Boolean> {
    108
    119    /**
    1210     * Constructs a new {@code BooleanProperty}.
    public class BooleanProperty extends AbstractProperty<Boolean> {  
    1513     */
    1614    public BooleanProperty(String key, boolean defaultValue) {
    1715        super(key, defaultValue);
    18         if (Main.pref != null) {
    19             get();
    20         }
    2116    }
    2217
    2318    @Override
    2419    public Boolean get() {
    25         return Main.pref.getBoolean(getKey(), defaultValue);
     20        // Removing this implementation breaks binary compatibility
     21        return super.get();
    2622    }
    2723
    2824    @Override
    2925    public boolean put(Boolean value) {
    30         return Main.pref.put(getKey(), value);
     26        // Removing this implementation breaks binary compatibility
     27        return super.put(value);
     28    }
     29
     30    @Override
     31    protected Boolean fromString(String string) {
     32        return Boolean.valueOf(string);
     33    }
     34
     35    @Override
     36    protected String toString(Boolean t) {
     37        return t.toString();
    3138    }
    3239}
  • src/org/openstreetmap/josm/data/preferences/CachedProperty.java

    diff --git a/src/org/openstreetmap/josm/data/preferences/CachedProperty.java b/src/org/openstreetmap/josm/data/preferences/CachedProperty.java
    index d8e36a4..c3459e7 100644
    a b public abstract class CachedProperty<T> extends AbstractProperty<T> implements P  
    1313
    1414    protected CachedProperty(String key, String defaultValueAsString) {
    1515        super(key, null);
    16         Main.pref.addPreferenceChangeListener(this);
     16        Main.pref.addKeyPreferenceChangeListener(key, this);
    1717        this.defaultValueAsString = defaultValueAsString;
    1818        updateValue();
    1919    }
    public abstract class CachedProperty<T> extends AbstractProperty<T> implements P  
    6060    }
    6161
    6262    public String getAsString() {
    63         return Main.pref.get(getKey(), getDefaultValueAsString());
     63        return getPreferences().get(getKey(), getDefaultValueAsString());
    6464    }
    6565
    6666    @Override
  • new file src/org/openstreetmap/josm/data/preferences/CachingProperty.java

    diff --git a/src/org/openstreetmap/josm/data/preferences/CachingProperty.java b/src/org/openstreetmap/josm/data/preferences/CachingProperty.java
    new file mode 100644
    index 0000000..d8644ee
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.preferences;
     3
     4import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeListener;
     5
     6/**
     7 * This is a special wrapper of {@link AbstractProperty}. The current preference value is cached. The value is invalidated if the preference was
     8 * changed.
     9 * @author Michael Zangl
     10 *
     11 * @param <T>
     12 * @since xxx
     13 */
     14public class CachingProperty<T> extends AbstractProperty<T> implements ValueChangeListener<T> {
     15
     16    private T cache;
     17    private boolean cacheActive;
     18    private final AbstractProperty<T> toCache;
     19
     20    /**
     21     * Create a new caching property.
     22     * @param toCache The property to cache.
     23     */
     24    CachingProperty(AbstractProperty<T> toCache) {
     25        super(toCache.getKey(), toCache.getDefaultValue());
     26        this.toCache = toCache;
     27        addWeakListener(this);
     28    }
     29
     30    @Override
     31    public synchronized T get() {
     32        if (!cacheActive) {
     33            cache = toCache.get();
     34            cacheActive = true;
     35        }
     36        return cache;
     37    }
     38
     39    @Override
     40    public boolean put(T value) {
     41        return toCache.put(cache);
     42    }
     43
     44    @Override
     45    public synchronized void valueChanged(org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeEvent<? extends T> e) {
     46        cacheActive = false;
     47    }
     48}
  • src/org/openstreetmap/josm/data/preferences/CollectionProperty.java

    diff --git a/src/org/openstreetmap/josm/data/preferences/CollectionProperty.java b/src/org/openstreetmap/josm/data/preferences/CollectionProperty.java
    index 0b5c854..44f6864 100644
    a b public class CollectionProperty extends AbstractProperty<Collection<String>> {  
    2424
    2525    @Override
    2626    public Collection<String> get() {
    27         return Main.pref.getCollection(getKey(), getDefaultValue());
     27        return getPreferences().getCollection(getKey(), getDefaultValue());
    2828    }
    2929
    3030    @Override
    3131    public boolean put(Collection<String> value) {
    32         return Main.pref.putCollection(getKey(), value);
     32        return getPreferences().putCollection(getKey(), value);
    3333    }
    3434}
  • src/org/openstreetmap/josm/data/preferences/ColorProperty.java

    diff --git a/src/org/openstreetmap/josm/data/preferences/ColorProperty.java b/src/org/openstreetmap/josm/data/preferences/ColorProperty.java
    index b597bb2..873dba8 100644
    a b package org.openstreetmap.josm.data.preferences;  
    44import java.awt.Color;
    55import java.util.Locale;
    66
    7 import org.openstreetmap.josm.Main;
    8 import org.openstreetmap.josm.data.Preferences.ColorKey;
     7import org.openstreetmap.josm.tools.ColorHelper;
    98
    109/**
    1110 * A property containing a {@link Color} value.
    1211 * @since 5464
    1312 */
    14 public class ColorProperty extends AbstractProperty<Color> implements ColorKey {
     13public class ColorProperty extends AbstractToStringProperty<Color> {
    1514
    1615    private final String name;
    1716
    1817    /**
    1918     * Constructs a new {@code ColorProperty}.
    2019     * @param colName The color name
     20     * @param defaultValue The default value as HTML string
     21     */
     22    public ColorProperty(String colName, String defaultValue) {
     23        this(colName, ColorHelper.html2color(defaultValue));
     24    }
     25
     26    /**
     27     * Constructs a new {@code ColorProperty}.
     28     * @param colName The color name
    2129     * @param defaultValue The default value
    2230     */
    2331    public ColorProperty(String colName, Color defaultValue) {
    2432        super(getColorKey(colName), defaultValue);
    2533        this.name = colName;
    26         if (Main.pref != null) {
    27             get();
    28         }
     34        getPreferences().registerColor(getColorKey(colName), colName);
    2935    }
    3036
    3137    @Override
    3238    public Color get() {
    33         return Main.pref.getColor(this);
     39        // Removing this implementation breaks binary compatibility due to the way generics work
     40        return super.get();
    3441    }
    3542
    3643    @Override
    3744    public boolean put(Color value) {
    38         return Main.pref.putColor(getColorKey(name), value);
     45        // Removing this implementation breaks binary compatibility due to the way generics work
     46        return super.put(value);
     47    }
     48
     49    @Override
     50    protected Color fromString(String string) {
     51        return ColorHelper.html2color(string);
     52    }
     53
     54    @Override
     55    protected String toString(Color t) {
     56        return ColorHelper.color2html(t, true);
     57    }
     58
     59    /**
     60     * Gets a color of which the value can be set.
     61     * @param colorName the name of the color.
     62     * @return The child property that inherits this value if it is not set.
     63     */
     64    public AbstractToStringProperty<Color> getChildColor(String colorName) {
     65        return getChildProperty(getColorKey(colorName));
     66    }
     67
     68    /**
     69     * Gets the name this color was registered with.
     70     * @return The name.
     71     */
     72    public String getName() {
     73        return name;
    3974    }
    4075
    4176    /**
    public class ColorProperty extends AbstractProperty<Color> implements ColorKey {  
    4479     * @return The color key for this property
    4580     */
    4681    public static String getColorKey(String colName) {
    47         return colName == null ? null : colName.toLowerCase(Locale.ENGLISH).replaceAll("[^a-z0-9]+", ".");
    48     }
    49 
    50     @Override
    51     public String getColorName() {
    52         return name;
     82        return colName == null ? null : "color." + colName.toLowerCase(Locale.ENGLISH).replaceAll("[^a-z0-9]+", ".");
    5383    }
    5484
    5585    @Override
    56     public String getSpecialName() {
    57         return null;
     86    public String toString() {
     87        return "ColorProperty [name=" + name + ", defaultValue=" + getDefaultValue() + "]";
    5888    }
    5989}
  • src/org/openstreetmap/josm/data/preferences/DoubleProperty.java

    diff --git a/src/org/openstreetmap/josm/data/preferences/DoubleProperty.java b/src/org/openstreetmap/josm/data/preferences/DoubleProperty.java
    index 44ca4c2..a44066d 100644
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.preferences;
    33
    4 import org.openstreetmap.josm.Main;
    5 
    64/**
    75 * A property containing an {@code Double} value.
    86 * @since 3246
    97 */
    10 public class DoubleProperty extends AbstractProperty<Double> {
     8public class DoubleProperty extends AbstractToStringProperty<Double> {
    119
    1210    /**
    1311     * Constructs a new {@code DoubleProperty}.
    public class DoubleProperty extends AbstractProperty<Double> {  
    2018
    2119    @Override
    2220    public Double get() {
    23         return Main.pref.getDouble(getKey(), getDefaultValue());
     21        // Removing this implementation breaks binary compatibility
     22        return super.get();
    2423    }
    2524
    2625    @Override
    2726    public boolean put(Double value) {
    28         return Main.pref.putDouble(getKey(), value);
     27        // Removing this implementation breaks binary compatibility
     28        return super.put(value);
     29    }
     30
     31    @Override
     32    protected Double fromString(String string) {
     33        try {
     34            return Double.valueOf(string);
     35        } catch (NumberFormatException e) {
     36            throw new InvalidPreferenceValueException(e);
     37        }
     38    }
     39
     40    @Override
     41    protected String toString(Double t) {
     42        return t.toString();
    2943    }
    3044
    3145    /**
  • src/org/openstreetmap/josm/data/preferences/IntegerProperty.java

    diff --git a/src/org/openstreetmap/josm/data/preferences/IntegerProperty.java b/src/org/openstreetmap/josm/data/preferences/IntegerProperty.java
    index f15d082..f0cb061 100644
    a b import org.openstreetmap.josm.Main;  
    77 * A property containing an {@code Integer} value.
    88 * @since 3246
    99 */
    10 public class IntegerProperty extends AbstractProperty<Integer> {
     10public class IntegerProperty extends AbstractToStringProperty<Integer> {
    1111
    1212    /**
    1313     * Constructs a new {@code IntegerProperty}.
    public class IntegerProperty extends AbstractProperty<Integer> {  
    2323
    2424    @Override
    2525    public Integer get() {
    26         return Main.pref.getInteger(getKey(), getDefaultValue());
     26        // Removing this implementation breaks binary compatibility
     27        return super.get();
    2728    }
    2829
    2930    @Override
    3031    public boolean put(Integer value) {
    31         return Main.pref.putInteger(getKey(), value);
     32        // Removing this implementation breaks binary compatibility
     33        return super.put(value);
    3234    }
    3335
     36    @Override
     37    protected Integer fromString(String string) {
     38        try {
     39            return Integer.valueOf(string);
     40        } catch (NumberFormatException e) {
     41            throw new InvalidPreferenceValueException(e);
     42        }
     43    }
     44
     45    @Override
     46    protected String toString(Integer t) {
     47        return t.toString();
     48    }
     49
     50
    3451    /**
    3552     * parses and saves an integer value
    3653     * @param value the value to be parsed
  • src/org/openstreetmap/josm/data/preferences/LongProperty.java

    diff --git a/src/org/openstreetmap/josm/data/preferences/LongProperty.java b/src/org/openstreetmap/josm/data/preferences/LongProperty.java
    index d480e9e..689f354 100644
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.preferences;
    33
    4 import org.openstreetmap.josm.Main;
    5 
    64/**
    75 * A property containing an {@code Long} value.
    86 * @since 10087
    97 *
    108 */
    11 public class LongProperty extends AbstractProperty<Long> {
     9public class LongProperty extends AbstractToStringProperty<Long> {
    1210
    1311    /**
    1412     * Constructs a new {@code LongProperty}
    public class LongProperty extends AbstractProperty<Long> {  
    1715     */
    1816    public LongProperty(String key, long defaultValue) {
    1917        super(key, defaultValue);
    20         if (Main.pref != null) {
    21             get();
    22         }
    2318    }
    2419
    2520    @Override
    2621    public Long get() {
    27         return Main.pref.getLong(getKey(), getDefaultValue());
     22        // Removing this implementation breaks binary compatibility
     23        return super.get();
    2824    }
    2925
    3026    @Override
    3127    public boolean put(Long value) {
    32         return Main.pref.putLong(getKey(), value);
     28        // Removing this implementation breaks binary compatibility
     29        return super.put(value);
     30    }
     31
     32    @Override
     33    protected Long fromString(String string) {
     34        try {
     35            return Long.valueOf(string);
     36        } catch (NumberFormatException e) {
     37            throw new InvalidPreferenceValueException(e);
     38        }
    3339    }
    3440
     41    @Override
     42    protected String toString(Long t) {
     43        return t.toString();
     44    }
    3545}
  • src/org/openstreetmap/josm/data/preferences/PreferencesWriter.java

    diff --git a/src/org/openstreetmap/josm/data/preferences/PreferencesWriter.java b/src/org/openstreetmap/josm/data/preferences/PreferencesWriter.java
    index f247cde..80fd8d9 100644
    a b import java.io.PrintWriter;  
    55import java.util.Collection;
    66import java.util.List;
    77import java.util.Map;
     8import java.util.stream.Stream;
    89
    910import org.openstreetmap.josm.Main;
    1011import org.openstreetmap.josm.data.Version;
    public class PreferencesWriter extends XmlWriter implements SettingVisitor {  
    3738     * @param settings preferences settings to write
    3839     */
    3940    public void write(Collection<Map.Entry<String, Setting<?>>> settings) {
     41        write(settings.stream());
     42    }
     43
     44    /**
     45     * Write preferences.
     46     *
     47     * @param settings preferences settings to write as stream.
     48     */
     49    public void write(Stream<Map.Entry<String, Setting<?>>> settings) {
    4050        out.write(String.format("<?xml version=\"1.0\" encoding=\"UTF-8\"?>%n"));
    4151        String rootElement = defaults ? "preferences-defaults" : "preferences";
    4252        out.write(String.format("<%s xmlns='%s/preferences-1.0'", rootElement, Main.getXMLBase()));
    public class PreferencesWriter extends XmlWriter implements SettingVisitor {  
    4454            out.write(" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'");
    4555        }
    4656        out.write(String.format(" version='%d'>%n", Version.getInstance().getVersion()));
    47         for (Map.Entry<String, Setting<?>> e : settings) {
     57        settings.forEachOrdered(e -> {
    4858            setKey(e.getKey());
    4959            e.getValue().visit(this);
    50         }
     60        });
    5161        out.write(String.format("</%s>%n", rootElement));
    5262    }
    5363
  • src/org/openstreetmap/josm/data/preferences/StringProperty.java

    diff --git a/src/org/openstreetmap/josm/data/preferences/StringProperty.java b/src/org/openstreetmap/josm/data/preferences/StringProperty.java
    index 25e42a1..cab4539 100644
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.preferences;
    33
    4 import org.openstreetmap.josm.Main;
    5 
    64/**
    75 * A property containing an {@code String} value.
    86 */
    9 public class StringProperty extends AbstractProperty<String> {
     7public class StringProperty extends AbstractToStringProperty<String> {
    108
    119    /**
    1210     * Constructs a new {@code StringProperty}.
    public class StringProperty extends AbstractProperty<String> {  
    1513     */
    1614    public StringProperty(String key, String defaultValue) {
    1715        super(key, defaultValue);
    18         if (Main.pref != null) {
    19             get();
    20         }
    2116    }
    2217
    2318    @Override
    2419    public String get() {
    25         return Main.pref.get(getKey(), getDefaultValue());
     20        // Removing this implementation breaks binary compatibility
     21        return super.get();
    2622    }
    2723
    2824    @Override
    2925    public boolean put(String value) {
    30         return Main.pref.put(getKey(), value);
     26        // Removing this implementation breaks binary compatibility
     27        return super.put(value);
     28    }
     29
     30    @Override
     31    protected String fromString(String string) {
     32        return string;
     33    }
     34
     35    @Override
     36    protected String toString(String string) {
     37        return string;
    3138    }
    3239}
  • src/org/openstreetmap/josm/gui/MainApplication.java

    diff --git a/src/org/openstreetmap/josm/gui/MainApplication.java b/src/org/openstreetmap/josm/gui/MainApplication.java
    index f58a661..520bfa4 100644
    a b public class MainApplication extends Main {  
    314314            I18n.set(args.get(Option.LANGUAGE).iterator().next());
    315315        }
    316316
     317        if (args.containsKey(Option.TRACE)) {
     318            // Enable JOSM debug level
     319            logLevel = 5;
     320            // Enable debug in OAuth signpost via system preference, but only at trace level
     321            Utils.updateSystemProperty("debug", "true");
     322            Main.info(tr("Enabled detailed debug level (trace)"));
     323        } else if (args.containsKey(Option.DEBUG)) {
     324            // Enable JOSM debug level
     325            logLevel = 4;
     326            Main.info(tr("Printing debugging messages to console"));
     327        }
     328
    317329        initApplicationPreferences();
    318330
    319331        Policy.setPolicy(new Policy() {
    public class MainApplication extends Main {  
    345357            System.exit(0);
    346358        }
    347359
    348         if (args.containsKey(Option.DEBUG) || args.containsKey(Option.TRACE)) {
    349             // Enable JOSM debug level
    350             logLevel = 4;
    351             Main.info(tr("Printing debugging messages to console"));
    352         }
    353 
    354360        boolean skipLoadingPlugins = false;
    355361        if (args.containsKey(Option.SKIP_PLUGINS)) {
    356362            skipLoadingPlugins = true;
    357363            Main.info(tr("Plugin loading skipped"));
    358364        }
    359365
    360         if (args.containsKey(Option.TRACE)) {
    361             // Enable JOSM debug level
    362             logLevel = 5;
    363             // Enable debug in OAuth signpost via system preference, but only at trace level
    364             Utils.updateSystemProperty("debug", "true");
    365             Main.info(tr("Enabled detailed debug level (trace)"));
    366         }
    367 
    368366        Main.pref.init(args.containsKey(Option.RESET_PREFERENCES));
    369367
    370368        if (args.containsKey(Option.SET)) {
  • src/org/openstreetmap/josm/gui/MapFrame.java

    diff --git a/src/org/openstreetmap/josm/gui/MapFrame.java b/src/org/openstreetmap/josm/gui/MapFrame.java
    index 54d8c2d..7613e0d 100644
    a b public class MapFrame extends JPanel implements Destroyable, ActiveLayerChangeLi  
    759759                selectMapMode(newMapMode, newLayer);
    760760            } else if (mapMode != null) {
    761761                mapMode.exitMode(); // if new mode is null - simply exit from previous mode
     762                mapMode = null;
    762763            }
    763764        }
    764765        // if this is really a change (and not the first active layer)
  • src/org/openstreetmap/josm/gui/MapStatus.java

    diff --git a/src/org/openstreetmap/josm/gui/MapStatus.java b/src/org/openstreetmap/josm/gui/MapStatus.java
    index 50ef27c..d59d8fe 100644
    a b import org.openstreetmap.josm.data.coor.LatLon;  
    6464import org.openstreetmap.josm.data.osm.DataSet;
    6565import org.openstreetmap.josm.data.osm.OsmPrimitive;
    6666import org.openstreetmap.josm.data.osm.Way;
     67import org.openstreetmap.josm.data.preferences.AbstractProperty;
     68import org.openstreetmap.josm.data.preferences.BooleanProperty;
    6769import org.openstreetmap.josm.data.preferences.ColorProperty;
     70import org.openstreetmap.josm.data.preferences.DoubleProperty;
    6871import org.openstreetmap.josm.gui.help.Helpful;
    6972import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
    7073import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
    import org.openstreetmap.josm.tools.ImageProvider;  
    9295public final class MapStatus extends JPanel implements Helpful, Destroyable, PreferenceChangedListener, SoMChangeListener {
    9396
    9497    private final DecimalFormat DECIMAL_FORMAT = new DecimalFormat(Main.pref.get("statusbar.decimal-format", "0.0"));
    95     private final double DISTANCE_THRESHOLD = Main.pref.getDouble("statusbar.distance-threshold", 0.01);
     98    private static final AbstractProperty<Double> DISTANCE_THRESHOLD = new DoubleProperty("statusbar.distance-threshold", 0.01).cached();
     99
     100    private static final AbstractProperty<Boolean> SHOW_ID = new BooleanProperty("osm-primitives.showid", false);
    96101
    97102    /**
    98103     * Property for map status background color.
    99104     * @since 6789
    100105     */
    101106    public static final ColorProperty PROP_BACKGROUND_COLOR = new ColorProperty(
    102             marktr("Status bar background"), Color.decode("#b8cfe5"));
     107            marktr("Status bar background"), "#b8cfe5");
    103108
    104109    /**
    105110     * Property for map status background color (active state).
    106111     * @since 6789
    107112     */
    108113    public static final ColorProperty PROP_ACTIVE_BACKGROUND_COLOR = new ColorProperty(
    109             marktr("Status bar background: active"), Color.decode("#aaff5e"));
     114            marktr("Status bar background: active"), "#aaff5e");
    110115
    111116    /**
    112117     * Property for map status foreground color.
    public final class MapStatus extends JPanel implements Helpful, Destroyable, Pre  
    567572            }
    568573            text.append(name);
    569574
    570             boolean idShown = Main.pref.getBoolean("osm-primitives.showid");
     575            boolean idShown = SHOW_ID.get();
    571576            // fix #7557 - do not show ID twice
    572577
    573578            if (!osm.isNew() && !idShown) {
    public final class MapStatus extends JPanel implements Helpful, Destroyable, Pre  
    10201025     */
    10211026    public void setDist(double dist) {
    10221027        distValue = dist;
    1023         distText.setText(dist < 0 ? "--" : NavigatableComponent.getDistText(dist, DECIMAL_FORMAT, DISTANCE_THRESHOLD));
     1028        distText.setText(dist < 0 ? "--" : NavigatableComponent.getDistText(dist, DECIMAL_FORMAT, DISTANCE_THRESHOLD.get()));
    10241029    }
    10251030
    10261031    /**
  • src/org/openstreetmap/josm/gui/conflict/ConflictColors.java

    diff --git a/src/org/openstreetmap/josm/gui/conflict/ConflictColors.java b/src/org/openstreetmap/josm/gui/conflict/ConflictColors.java
    index 2d5e7e3..96e5a29 100644
    a b import static org.openstreetmap.josm.tools.I18n.marktr;  
    55
    66import java.awt.Color;
    77
    8 import org.openstreetmap.josm.Main;
    9 import org.openstreetmap.josm.data.Preferences.ColorKey;
     8import org.openstreetmap.josm.data.preferences.ColorProperty;
    109
    1110/**
    1211 * Conflict color constants.
    1312 * @since 4162
    1413 */
    15 public enum ConflictColors implements ColorKey {
     14public enum ConflictColors {
    1615
    1716    /** Conflict background: no conflict */
    1817    BGCOLOR_NO_CONFLICT(marktr("Conflict background: no conflict"), new Color(234, 234, 234)),
    public enum ConflictColors implements ColorKey {  
    8281    /** Conflict foreground: remove member */
    8382    FGCOLOR_MEMBER_REMOVE(marktr("Conflict foreground: remove member"), Color.black);
    8483
    85     private final String name;
    86     private final Color defaultColor;
     84    private final ColorProperty property;
    8785
    8886    ConflictColors(String name, Color defaultColor) {
    89         this.name = name;
    90         this.defaultColor = defaultColor;
    91     }
    92 
    93     @Override
    94     public String getColorName() {
    95         return name;
    96     }
    97 
    98     @Override
    99     public Color getDefaultValue() {
    100         return defaultColor;
    101     }
    102 
    103     @Override
    104     public String getSpecialName() {
    105         return null;
     87        property = new ColorProperty(name, defaultColor);
    10688    }
    10789
    10890    /**
    public enum ConflictColors implements ColorKey {  
    11092     * @return the color
    11193     */
    11294    public Color get() {
    113         return Main.pref.getColor(this);
     95        return property.get();
    11496    }
    11597
    11698    /**
  • src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java

    diff --git a/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java b/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java
    index 2b7798b..9dbc439 100644
    a b import java.util.ArrayList;  
    2020import java.util.Arrays;
    2121import java.util.Collections;
    2222import java.util.List;
     23import java.util.Objects;
    2324import java.util.concurrent.CopyOnWriteArrayList;
    2425
    2526import javax.swing.AbstractAction;
    import javax.swing.table.TableModel;  
    4445
    4546import org.openstreetmap.josm.Main;
    4647import org.openstreetmap.josm.actions.MergeLayerAction;
     48import org.openstreetmap.josm.data.preferences.AbstractProperty;
    4749import org.openstreetmap.josm.gui.MapFrame;
    4850import org.openstreetmap.josm.gui.MapView;
    4951import org.openstreetmap.josm.gui.SideButton;
    public class LayerListDialog extends ToggleDialog {  
    545547                label.setFont(label.getFont().deriveFont(Font.BOLD));
    546548            }
    547549            if (Main.pref.getBoolean("dialog.layer.colorname", true)) {
    548                 Color c = layer.getColor(false);
    549                 if (c != null) {
    550                     Color oc = null;
    551                     for (Layer l : model.getLayers()) {
    552                         oc = l.getColor(false);
    553                         if (oc != null) {
    554                             if (oc.equals(c)) {
    555                                 oc = null;
    556                             } else {
    557                                 break;
    558                             }
    559                         }
    560                     }
     550                AbstractProperty<Color> prop = layer.getColorProperty();
     551                Color c = prop == null ? null : prop.get();
     552                if (c == null || !model.getLayers().stream()
     553                        .map(Layer::getColorProperty)
     554                        .filter(Objects::nonNull)
     555                        .map(AbstractProperty::get)
     556                        .anyMatch(oc -> oc != null && !oc.equals(c))) {
    561557                    /* not more than one color, don't use coloring */
    562                     if (oc == null) {
    563                         c = null;
    564                     }
    565                 }
    566                 if (c == null) {
    567                     c = UIManager.getColor(isSelected ? "Table.selectionForeground" : "Table.foreground");
     558                    label.setForeground(UIManager.getColor(isSelected ? "Table.selectionForeground" : "Table.foreground"));
     559                } else {
     560                    label.setForeground(c);
    568561                }
    569                 label.setForeground(c);
    570562            }
    571563            label.setIcon(layer.getIcon());
    572564            label.setToolTipText(layer.getToolTipText());
  • src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java

    diff --git a/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java b/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java
    index 70c3a4f..f7fa2af 100644
    a b import org.openstreetmap.josm.actions.search.SearchCompiler;  
    6363import org.openstreetmap.josm.command.ChangeCommand;
    6464import org.openstreetmap.josm.command.ChangePropertyCommand;
    6565import org.openstreetmap.josm.command.Command;
    66 import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
     66import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
    6767import org.openstreetmap.josm.data.SelectionChangedListener;
    6868import org.openstreetmap.josm.data.osm.IRelation;
    6969import org.openstreetmap.josm.data.osm.Node;
    implements SelectionChangedListener, ActiveLayerChangeListener, DataSetListenerA  
    225225    private final JLabel selectSth = new JLabel("<html><p>"
    226226            + tr("Select objects for which to change tags.") + "</p></html>");
    227227
     228    private final PreferenceChangedListener preferenceListener = e -> {
     229                if (Main.getLayerManager().getEditDataSet() != null) {
     230                    // Re-load data when display preference change
     231                    updateSelection();
     232                }};
     233
    228234    private final transient TaggingPresetHandler presetHandler = new TaggingPresetHandler() {
    229235        @Override
    230236        public void updateTags(List<Tag> tags) {
    implements SelectionChangedListener, ActiveLayerChangeListener, DataSetListenerA  
    297303
    298304        editHelper.loadTagsIfNeeded();
    299305
    300         Main.pref.addPreferenceChangeListener(this);
     306        Main.pref.addKeyPreferenceChangeListener("display.discardable-keys", preferenceListener);
    301307    }
    302308
    303309    private void buildTagsTable() {
    implements SelectionChangedListener, ActiveLayerChangeListener, DataSetListenerA  
    605611    @Override
    606612    public void destroy() {
    607613        super.destroy();
    608         Main.pref.removePreferenceChangeListener(this);
     614        Main.pref.removeKeyPreferenceChangeListener("display.discardable-keys", preferenceListener);
    609615        Container parent = pluginHook.getParent();
    610616        if (parent != null) {
    611617            parent.remove(pluginHook);
    implements SelectionChangedListener, ActiveLayerChangeListener, DataSetListenerA  
    13931399        return ss;
    13941400    }
    13951401
    1396     @Override
    1397     public void preferenceChanged(PreferenceChangeEvent e) {
    1398         super.preferenceChanged(e);
    1399         if ("display.discardable-keys".equals(e.getKey()) && Main.getLayerManager().getEditDataSet() != null) {
    1400             // Re-load data when display preference change
    1401             updateSelection();
    1402         }
    1403     }
    1404 
    14051402    /**
    14061403     * Clears the row selection when it is filtered away by the row sorter.
    14071404     */
  • src/org/openstreetmap/josm/gui/layer/CustomizeColor.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/CustomizeColor.java b/src/org/openstreetmap/josm/gui/layer/CustomizeColor.java
    index e39f575..1d53e28 100644
    a b import static org.openstreetmap.josm.tools.I18n.tr;  
    77import java.awt.Color;
    88import java.awt.Component;
    99import java.awt.event.ActionEvent;
    10 import java.util.LinkedList;
     10import java.util.Collections;
    1111import java.util.List;
     12import java.util.Objects;
     13import java.util.stream.Collectors;
    1214
    1315import javax.swing.AbstractAction;
    1416import javax.swing.Action;
    import javax.swing.JMenuItem;  
    1719import javax.swing.JOptionPane;
    1820
    1921import org.openstreetmap.josm.Main;
     22import org.openstreetmap.josm.data.preferences.AbstractProperty;
     23import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
    2024import org.openstreetmap.josm.gui.layer.Layer.LayerAction;
    2125import org.openstreetmap.josm.gui.layer.Layer.MultiLayerAction;
     26import org.openstreetmap.josm.tools.CheckParameterUtil;
    2227import org.openstreetmap.josm.tools.ImageProvider;
    2328
    2429public class CustomizeColor extends AbstractAction implements LayerAction, MultiLayerAction {
    25     private final transient List<Layer> layers;
     30    private final transient List<AbstractProperty<Color>> colors;
    2631
    2732    /**
    2833     * Constructs a new {@code CustomizeColor} for a given list of layers.
    public class CustomizeColor extends AbstractAction implements LayerAction, Multi  
    3035     */
    3136    public CustomizeColor(List<Layer> l) {
    3237        super(tr("Customize Color"), ImageProvider.get("colorchooser"));
     38        colors = l.stream().map(Layer::getColorProperty).collect(Collectors.toList());
     39        CheckParameterUtil.ensureThat(colors.stream().allMatch(Objects::nonNull), "All layers must have colors.");
    3340        putValue("help", ht("/Action/LayerCustomizeColor"));
    34         layers = l;
    3541    }
    3642
    3743    /**
    public class CustomizeColor extends AbstractAction implements LayerAction, Multi  
    3945     * @param l layer
    4046     */
    4147    public CustomizeColor(Layer l) {
    42         this(new LinkedList<Layer>());
    43         layers.add(l);
     48        this(Collections.singletonList(l));
    4449    }
    4550
    4651    @Override
    4752    public boolean supportLayers(List<Layer> layers) {
    48         for (Layer layer: layers) {
    49             if (layer.getColor(false) == null)
    50                 return false;
    51         }
    52         return true;
     53        return layers.stream().allMatch(l -> l.getColorProperty() != null);
    5354    }
    5455
    5556    @Override
    public class CustomizeColor extends AbstractAction implements LayerAction, Multi  
    6465
    6566    @Override
    6667    public void actionPerformed(ActionEvent e) {
    67         Color cl = layers.get(0).getColor(false);
    68         if (cl == null)
    69             cl = Color.gray;
     68        Color cl = colors.stream().map(c -> c.get()).filter(Objects::nonNull).findAny().orElse(Color.GRAY);
    7069        JColorChooser c = new JColorChooser(cl);
    7170        Object[] options = new Object[]{tr("OK"), tr("Cancel"), tr("Default")};
    7271        int answer = JOptionPane.showOptionDialog(
    public class CustomizeColor extends AbstractAction implements LayerAction, Multi  
    8180        );
    8281        switch (answer) {
    8382        case 0:
    84             for (Layer layer : layers) {
    85                 Main.pref.putColor("layer "+layer.getName(), c.getColor());
    86             }
     83            colors.stream().forEach(prop -> prop.put(c.getColor()));
    8784            break;
    8885        case 1:
    8986            return;
    9087        case 2:
    91             for (Layer layer : layers) {
    92                 Main.pref.putColor("layer "+layer.getName(), null);
    93             }
     88            colors.stream().forEach(prop -> prop.put(null));
    9489            break;
    9590        }
    96         Main.map.repaint();
     91        // TODO: Make the layer dialog listen to property change events so that this is not needed any more.
     92        LayerListDialog.getInstance().repaint();
    9793    }
    9894}
  • src/org/openstreetmap/josm/gui/layer/GpxLayer.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/GpxLayer.java b/src/org/openstreetmap/josm/gui/layer/GpxLayer.java
    index 482d7f7..a10d1b8 100644
    a b package org.openstreetmap.josm.gui.layer;  
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55import static org.openstreetmap.josm.tools.I18n.trn;
    66
    7 import java.awt.Color;
    87import java.awt.Dimension;
    98import java.awt.Graphics2D;
    109import java.io.File;
    import org.openstreetmap.josm.data.gpx.GpxData;  
    3130import org.openstreetmap.josm.data.gpx.GpxTrack;
    3231import org.openstreetmap.josm.data.gpx.WayPoint;
    3332import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
     33import org.openstreetmap.josm.data.preferences.ColorProperty;
    3434import org.openstreetmap.josm.data.projection.Projection;
    3535import org.openstreetmap.josm.gui.MapView;
    3636import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
    public class GpxLayer extends Layer {  
    8888    public GpxLayer(GpxData d, String name, boolean isLocal) {
    8989        super(d.getString(GpxConstants.META_NAME));
    9090        data = d;
    91         drawHelper = new GpxDrawHelper(data);
     91        drawHelper = new GpxDrawHelper(data, getColorProperty());
    9292        SystemOfMeasurement.addSoMChangeListener(drawHelper);
    9393        ensureTrackVisibilityLength();
    9494        setName(name);
    public class GpxLayer extends Layer {  
    9696    }
    9797
    9898    @Override
    99     public Color getColor(boolean ignoreCustom) {
    100         return drawHelper.getColor(getName(), ignoreCustom);
     99    protected ColorProperty getBaseColorProperty() {
     100        return GpxDrawHelper.DEFAULT_COLOR;
    101101    }
    102102
    103103    /**
  • src/org/openstreetmap/josm/gui/layer/Layer.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/Layer.java b/src/org/openstreetmap/josm/gui/layer/Layer.java
    index 7347110..7c85a6c 100644
    a b import org.openstreetmap.josm.actions.SaveActionBase;  
    2525import org.openstreetmap.josm.actions.SaveAsAction;
    2626import org.openstreetmap.josm.data.ProjectionBounds;
    2727import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
     28import org.openstreetmap.josm.data.preferences.AbstractProperty;
     29import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeListener;
     30import org.openstreetmap.josm.data.preferences.ColorProperty;
    2831import org.openstreetmap.josm.data.projection.Projection;
    2932import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
    3033import org.openstreetmap.josm.tools.Destroyable;
    public abstract class Layer extends AbstractMapViewPaintable implements Destroya  
    145148     */
    146149    private File associatedFile;
    147150
     151    private final ValueChangeListener<Object> invalidateListener = change -> invalidate();
     152
    148153    /**
    149154     * Create the layer and fill in the necessary components.
    150155     * @param name Layer name
    public abstract class Layer extends AbstractMapViewPaintable implements Destroya  
    181186     *      is used. When this is true, then even for custom coloring the base
    182187     *      color is returned - mainly for layer internal use.
    183188     * @return layer color
     189     * @deprecated Use the new {@link #getColorProperty()}. To be removed end of 2016.
    184190     */
     191    @Deprecated
    185192    public Color getColor(boolean ignoreCustom) {
    186193        return null;
    187194    }
    188195
    189196    /**
     197     * Gets the color property to use for this layer.
     198     * @return The color property.
     199     */
     200    public AbstractProperty<Color> getColorProperty() {
     201        ColorProperty base = getBaseColorProperty();
     202        if (base != null) {
     203            // cannot cache this - name may change.
     204            return base.getChildColor("layer " + getName());
     205        } else {
     206            return null;
     207        }
     208    }
     209
     210    /**
     211     * Gets the color property that stores the default color for this layer.
     212     * @return The property or <code>null</code> if this layer is not colored.
     213     */
     214    protected ColorProperty getBaseColorProperty() {
     215        return null;
     216    }
     217
     218    private void addColorPropertyListener() {
     219        AbstractProperty<Color> colorProperty = getColorProperty();
     220        if (colorProperty != null) {
     221            colorProperty.addWeakListener(invalidateListener);
     222        }
     223    }
     224
     225    private void removeColorPropertyListener() {
     226        AbstractProperty<Color> colorProperty = getColorProperty();
     227        if (colorProperty != null) {
     228            colorProperty.removeListener(invalidateListener);
     229        }
     230    }
     231
     232    /**
    190233     * @return A small tooltip hint about some statistics for this layer.
    191234     */
    192235    public abstract String getToolTipText();
    public abstract class Layer extends AbstractMapViewPaintable implements Destroya  
    264307     * @param name the name. If null, the name is set to the empty string.
    265308     */
    266309    public final void setName(String name) {
     310        if (this.name != null) {
     311            removeColorPropertyListener();
     312        }
    267313        if (name == null) {
    268314            name = "";
    269315        }
     316
    270317        String oldValue = this.name;
    271318        this.name = name;
    272319        if (!this.name.equals(oldValue)) {
    273320            propertyChangeSupport.firePropertyChange(NAME_PROP, oldValue, this.name);
    274321        }
     322
     323        // re-add listener
     324        addColorPropertyListener();
     325        invalidate();
    275326    }
    276327
    277328    /**
  • src/org/openstreetmap/josm/gui/layer/gpx/GpxDrawHelper.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/gpx/GpxDrawHelper.java b/src/org/openstreetmap/josm/gui/layer/gpx/GpxDrawHelper.java
    index 8e138db..03b537a 100644
    a b import org.openstreetmap.josm.data.coor.LatLon;  
    2424import org.openstreetmap.josm.data.gpx.GpxConstants;
    2525import org.openstreetmap.josm.data.gpx.GpxData;
    2626import org.openstreetmap.josm.data.gpx.WayPoint;
     27import org.openstreetmap.josm.data.preferences.AbstractProperty;
     28import org.openstreetmap.josm.data.preferences.ColorProperty;
    2729import org.openstreetmap.josm.gui.MapView;
    2830import org.openstreetmap.josm.tools.ColorScale;
    2931
    import org.openstreetmap.josm.tools.ColorScale;  
    3234 * @since 7319
    3335 */
    3436public class GpxDrawHelper implements SoMChangeListener {
     37
     38    /**
     39     * The color that is used for drawing GPX points.
     40     */
     41    public static final ColorProperty DEFAULT_COLOR = new ColorProperty(marktr("gps point"), Color.magenta);
     42
    3543    private final GpxData data;
    3644
    3745    // draw lines between points belonging to different segments
    public class GpxDrawHelper implements SoMChangeListener {  
    8391    /** Opacity for hdop points **/
    8492    private int hdopAlpha;
    8593
    86     private static final Color DEFAULT_COLOR = Color.magenta;
    87 
    8894    // lookup array to draw arrows without doing any math
    8995    private static final int ll0 = 9;
    9096    private static final int sl4 = 5;
    public class GpxDrawHelper implements SoMChangeListener {  
    133139    /**
    134140     * Constructs a new {@code GpxDrawHelper}.
    135141     * @param gpxData GPX data
     142     * @param abstractProperty The color to draw with
    136143     */
    137     public GpxDrawHelper(GpxData gpxData) {
     144    public GpxDrawHelper(GpxData gpxData, AbstractProperty<Color> abstractProperty) {
    138145        data = gpxData;
    139146        setupColors();
    140147    }
    public class GpxDrawHelper implements SoMChangeListener {  
    150157     * @return the color or null if the color is not constant
    151158     */
    152159    public Color getColor(String layerName, boolean ignoreCustom) {
    153         Color c = Main.pref.getColor(marktr("gps point"), specName(layerName), DEFAULT_COLOR);
    154         return ignoreCustom || getColorMode(layerName) == ColorMode.NONE ? c : null;
     160        if (ignoreCustom || getColorMode(layerName) == ColorMode.NONE) {
     161            return DEFAULT_COLOR.getChildColor(specName(layerName)).get();
     162        } else {
     163            return null;
     164        }
    155165    }
    156166
    157167    /**
    public class GpxDrawHelper implements SoMChangeListener {  
    173183     * @return the color
    174184     **/
    175185    public static Color getGenericColor() {
    176         return Main.pref.getColor(marktr("gps point"), DEFAULT_COLOR);
     186        return DEFAULT_COLOR.get();
    177187    }
    178188
    179189    /**
  • src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java b/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java
    index 15abf70..43c265b 100644
    a b import org.openstreetmap.josm.data.gpx.GpxData;  
    3737import org.openstreetmap.josm.data.gpx.GpxLink;
    3838import org.openstreetmap.josm.data.gpx.WayPoint;
    3939import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
     40import org.openstreetmap.josm.data.preferences.ColorProperty;
    4041import org.openstreetmap.josm.gui.MapView;
    4142import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
    4243import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
    public class MarkerLayer extends Layer implements JumpToMarkerLayer {  
    7374    public AudioMarker syncAudioMarker;
    7475
    7576    private static final Color DEFAULT_COLOR = Color.magenta;
     77    private static final ColorProperty COLOR_PROPERTY = new ColorProperty(marktr("gps marker"), DEFAULT_COLOR);
    7678
    7779    /**
    7880     * Constructs a new {@code MarkerLayer}.
    public class MarkerLayer extends Layer implements JumpToMarkerLayer {  
    194196    }
    195197
    196198    @Override
    197     public Color getColor(boolean ignoreCustom) {
    198         return Main.pref.getColor(marktr("gps marker"), "layer "+getName(), DEFAULT_COLOR);
     199    protected ColorProperty getBaseColorProperty() {
     200        return COLOR_PROPERTY;
    199201    }
    200202
    201203    /* for preferences */
    202204    public static Color getGenericColor() {
    203         return Main.pref.getColor(marktr("gps marker"), DEFAULT_COLOR);
     205        return COLOR_PROPERTY.get();
    204206    }
    205207
    206208    @Override
    207209    public void paint(Graphics2D g, MapView mv, Bounds box) {
    208210        boolean showTextOrIcon = isTextOrIconShown();
    209         g.setColor(getColor(true));
     211        g.setColor(getColorProperty().get());
    210212
    211213        if (mousePressed) {
    212214            boolean mousePressedTmp = mousePressed;
  • src/org/openstreetmap/josm/gui/preferences/display/ColorPreference.java

    diff --git a/src/org/openstreetmap/josm/gui/preferences/display/ColorPreference.java b/src/org/openstreetmap/josm/gui/preferences/display/ColorPreference.java
    index ca7c4aa..f7ca354 100644
    a b public class ColorPreference implements SubPreferenceSetting {  
    256256     * Add all missing color entries.
    257257     */
    258258    private static void fixColorPrefixes() {
    259         PaintColors.getColors();
     259        PaintColors.values();
    260260        ConflictColors.getColors();
    261261        Severity.getColors();
    262262        MarkerLayer.getGenericColor();
  • new file src/org/openstreetmap/josm/tools/ListenerList.java

    diff --git a/src/org/openstreetmap/josm/tools/ListenerList.java b/src/org/openstreetmap/josm/tools/ListenerList.java
    new file mode 100644
    index 0000000..761e675
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.tools;
     3
     4import java.lang.ref.WeakReference;
     5import java.text.MessageFormat;
     6import java.util.HashMap;
     7import java.util.Iterator;
     8import java.util.Objects;
     9import java.util.concurrent.CopyOnWriteArrayList;
     10import java.util.stream.Stream;
     11
     12import org.openstreetmap.josm.Main;
     13
     14/**
     15 * This is a list of listeners. It does error checking and allows you to fire all listeners.
     16 *
     17 * @author Michael Zangl
     18 * @param <T> The type of listener contained in this list.
     19 */
     20public class ListenerList<T> {
     21    /**
     22     * This is a function that can be invoked for every listener.
     23     * @param <T> the listener type.
     24     */
     25    @FunctionalInterface
     26    public interface EventFirerer<T> {
     27        /**
     28         * Should fire the event for the given listener.
     29         * @param listener The listener to fire the event for.
     30         */
     31        void fire(T listener);
     32    }
     33
     34    private static final class WeakListener<T> {
     35
     36        private WeakReference<T> listener;
     37
     38        WeakListener(T listener) {
     39            this.listener = new WeakReference<>(listener);
     40        }
     41
     42        @Override
     43        public boolean equals(Object obj) {
     44            if (obj != null && obj.getClass() == WeakListener.class) {
     45                return Objects.equals(listener.get(), ((WeakListener<?>) obj).listener.get());
     46            } else {
     47                return false;
     48            }
     49        }
     50
     51        @Override
     52        public int hashCode() {
     53            T l = listener.get();
     54            if (l == null) {
     55                return 0;
     56            } else {
     57                return l.hashCode();
     58            }
     59        }
     60
     61        @Override
     62        public String toString() {
     63            return "WeakListener [listener=" + listener + "]";
     64        }
     65    }
     66
     67    private final CopyOnWriteArrayList<T> listeners = new CopyOnWriteArrayList<>();
     68    private final CopyOnWriteArrayList<WeakListener<T>> weakListeners = new CopyOnWriteArrayList<>();
     69
     70    protected ListenerList() {
     71        // hide
     72    }
     73
     74    /**
     75     * Adds a listener. The listener will not prevent the object from being garbage collected.
     76     *
     77     * This should be used with care. It is better to add good cleanup code.
     78     * @param listener The listener.
     79     */
     80    public synchronized void addWeakListener(T listener) {
     81        ensureNotInList(listener);
     82        // clean the weak listeners, just to be sure...
     83        while (weakListeners.remove(new WeakListener<T>(null))) {
     84            // continue
     85        }
     86        weakListeners.add(new WeakListener<>(listener));
     87    }
     88
     89    /**
     90     * Adds a listener.
     91     * @param listener The listener to add.
     92     */
     93    public synchronized void addListener(T listener) {
     94        ensureNotInList(listener);
     95        listeners.add(listener);
     96    }
     97
     98    private void ensureNotInList(T listener) {
     99        CheckParameterUtil.ensureParameterNotNull(listener, "listener");
     100        if (containsListener(listener)) {
     101            failAdd(listener);
     102        }
     103    }
     104
     105    protected void failAdd(T listener) {
     106        throw new IllegalArgumentException(
     107                MessageFormat.format("Listener {0} (instance of {1}) was already registered.", listener.toString(),
     108                        listener.getClass().getName()));
     109    }
     110
     111    private boolean containsListener(T listener) {
     112        return listeners.contains(listener) || weakListeners.contains(new WeakListener<>(listener));
     113    }
     114
     115    /**
     116     * Removes a listener.
     117     * @param listener The listener to remove.
     118     * @throws IllegalArgumentException if the listener was not registered before
     119     */
     120    public synchronized void removeListener(T listener) {
     121        if (!listeners.remove(listener) && !weakListeners.remove(new WeakListener<>(listener))) {
     122            failRemove(listener);
     123        }
     124    }
     125
     126    protected void failRemove(T listener) {
     127        throw new IllegalArgumentException(
     128                MessageFormat.format("Listener {0} (instance of {1}) was not registered before or already removed.",
     129                        listener.toString(), listener.getClass().getName()));
     130    }
     131
     132    /**
     133     * Check if any listeners are registered.
     134     * @return <code>true</code> if any are registered.
     135     */
     136    public boolean hasListeners() {
     137        return !listeners.isEmpty();
     138    }
     139
     140    /**
     141     * Fires an event to every listener.
     142     * @param eventFirerer The firerer to invoke the event method of the listener.
     143     */
     144    public void fireEvent(EventFirerer<T> eventFirerer) {
     145        for (T l : listeners) {
     146            eventFirerer.fire(l);
     147        }
     148        for (Iterator<WeakListener<T>> iterator = weakListeners.iterator(); iterator.hasNext();) {
     149            WeakListener<T> weakLink = iterator.next();
     150            T l = weakLink.listener.get();
     151            if (l == null) {
     152                iterator.remove();
     153            } else {
     154                eventFirerer.fire(l);
     155            }
     156        }
     157    }
     158
     159    /**
     160     * This is a special {@link ListenerList} that traces calls to the add/remove methods. This may cause memory leaks.
     161     * @author Michael Zangl
     162     *
     163     * @param <T>
     164     */
     165    public static class TracingListenerList<T> extends ListenerList<T> {
     166        private HashMap<T, StackTraceElement[]> listenersAdded = new HashMap<>();
     167        private HashMap<T, StackTraceElement[]> listenersRemoved = new HashMap<>();
     168
     169        protected TracingListenerList() {
     170            // hidden
     171        }
     172
     173        @Override
     174        public synchronized void addListener(T listener) {
     175            super.addListener(listener);
     176            listenersRemoved.remove(listener);
     177            listenersAdded.put(listener, Thread.currentThread().getStackTrace());
     178        }
     179
     180        @Override
     181        public synchronized void addWeakListener(T listener) {
     182            super.addWeakListener(listener);
     183            listenersRemoved.remove(listener);
     184            listenersAdded.put(listener, Thread.currentThread().getStackTrace());
     185        }
     186
     187        @Override
     188        public synchronized void removeListener(T listener) {
     189            super.removeListener(listener);
     190            listenersAdded.remove(listener);
     191            listenersRemoved.put(listener, Thread.currentThread().getStackTrace());
     192        }
     193
     194        @Override
     195        protected void failAdd(T listener) {
     196            Main.trace("Previous addition of the listener");
     197            dumpStack(listenersAdded.get(listener));
     198            super.failAdd(listener);
     199        }
     200
     201        @Override
     202        protected void failRemove(T listener) {
     203            Main.trace("Previous removal of the listener");
     204            dumpStack(listenersRemoved.get(listener));
     205            super.failRemove(listener);
     206        }
     207
     208        private static void dumpStack(StackTraceElement[] stackTraceElements) {
     209            if (stackTraceElements == null) {
     210                Main.trace("  - (no trace recorded)");
     211            } else {
     212                Stream.of(stackTraceElements).limit(20).forEach(
     213                        e -> Main.trace(e.getClassName() + "." + e.getMethodName() + " line " + e.getLineNumber()));
     214            }
     215        }
     216    }
     217
     218    /**
     219     * Create a new listener list
     220     * @param <T> The listener type the list should hold.
     221     * @return A new list. A tracing list is created if trace is enabled.
     222     */
     223    public static <T> ListenerList<T> create() {
     224        if (Main.isTraceEnabled()) {
     225            return new TracingListenerList<>();
     226        } else {
     227            return new ListenerList<>();
     228        }
     229    }
     230}
  • new file test/unit/org/openstreetmap/josm/data/preferences/ColorPropertyTest.java

    diff --git a/test/unit/org/openstreetmap/josm/data/preferences/ColorPropertyTest.java b/test/unit/org/openstreetmap/josm/data/preferences/ColorPropertyTest.java
    new file mode 100644
    index 0000000..d76ad8f
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.preferences;
     3
     4import static org.junit.Assert.assertEquals;
     5
     6import java.awt.Color;
     7
     8import org.junit.Before;
     9import org.junit.Rule;
     10import org.junit.Test;
     11import org.openstreetmap.josm.Main;
     12import org.openstreetmap.josm.testutils.JOSMTestRules;
     13
     14import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     15
     16/**
     17 * Test {@link ColorProperty}
     18 * @author Michael Zangl
     19 * @since xxx
     20 */
     21public class ColorPropertyTest {
     22    /**
     23     * This is a preference test.
     24     */
     25    @Rule
     26    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
     27    public JOSMTestRules test = new JOSMTestRules().preferences();
     28    private ColorProperty base;
     29
     30    /**
     31     * Set up test case
     32     */
     33    @Before
     34    public void createTestProperty() {
     35        base = new ColorProperty("test", Color.RED);
     36    }
     37
     38    /**
     39     * Test {@link ColorProperty#get()}
     40     */
     41    @Test
     42    public void testGet() {
     43        assertEquals(Color.RED, base.get());
     44
     45        Main.pref.put("color.test", "#00ff00");
     46        assertEquals(new Color(0xff00ff00), base.get());
     47    }
     48
     49    /**
     50     * Test {@link ColorProperty#put()}
     51     */
     52    @Test
     53    public void testPut() {
     54        assertEquals(Color.RED, base.get());
     55
     56        base.put(new Color(0xff00ff00));
     57        assertEquals(new Color(0xff00ff00), base.get());
     58        assertEquals("#00ff00", Main.pref.get("color.test").toLowerCase());
     59
     60        base.put(null);
     61        assertEquals(Color.RED, base.get());
     62    }
     63
     64    /**
     65     * Test {@link ColorProperty#getChildColor(String)}
     66     */
     67    @Test
     68    public void testGetChildColor() {
     69        AbstractToStringProperty<Color> child = base.getChildColor("test2");
     70
     71        assertEquals(Color.RED, child.get());
     72
     73        base.put(Color.GREEN);
     74        assertEquals(Color.GREEN, child.get());
     75
     76        child.put(Color.YELLOW);
     77        assertEquals(Color.YELLOW, child.get());
     78        assertEquals(Color.GREEN, base.get());
     79
     80        child.put(null);
     81        assertEquals(Color.GREEN, child.get());
     82    }
     83}
  • test/unit/org/openstreetmap/josm/gui/layer/GpxLayerTest.java

    diff --git a/test/unit/org/openstreetmap/josm/gui/layer/GpxLayerTest.java b/test/unit/org/openstreetmap/josm/gui/layer/GpxLayerTest.java
    index 1d8acb0..b151773 100644
    a b public class GpxLayerTest {  
    7373        GpxLayer layer = new GpxLayer(new GpxData(), "foo", false);
    7474        assertEquals("foo", layer.getName());
    7575        assertFalse(layer.isLocalFile());
    76         assertEquals(Color.MAGENTA, layer.getColor(false));
     76        assertEquals(Color.MAGENTA, layer.getColorProperty().get());
    7777        assertEquals("<html>0 tracks, 0 routes, 0 waypoints<br>Length: < 0.01 m<br></html>", layer.getToolTipText());
    7878
    7979        GpxLayer layer2 = new GpxLayer(new GpxData(), "bar", true);
    8080        assertEquals("bar", layer2.getName());
    8181        assertTrue(layer2.isLocalFile());
    82         assertEquals(Color.MAGENTA, layer2.getColor(true));
     82        assertEquals(Color.MAGENTA, layer2.getColorProperty().get());
    8383        assertEquals("<html>0 tracks, 0 routes, 0 waypoints<br>Length: < 0.01 m<br></html>", layer2.getToolTipText());
    8484
    8585        assertFalse(layer.isChanged());
  • test/unit/org/openstreetmap/josm/gui/layer/gpx/GpxDrawHelperTest.java

    diff --git a/test/unit/org/openstreetmap/josm/gui/layer/gpx/GpxDrawHelperTest.java b/test/unit/org/openstreetmap/josm/gui/layer/gpx/GpxDrawHelperTest.java
    index ed17db6..9e83236 100644
    a b package org.openstreetmap.josm.gui.layer.gpx;  
    33
    44import static org.junit.Assert.assertEquals;
    55
     6import java.awt.Color;
    67import java.io.FileNotFoundException;
    78import java.io.IOException;
    89import java.util.ArrayList;
    import org.openstreetmap.josm.Main;  
    1617import org.openstreetmap.josm.TestUtils;
    1718import org.openstreetmap.josm.data.gpx.GpxData;
    1819import org.openstreetmap.josm.data.gpx.WayPoint;
     20import org.openstreetmap.josm.data.preferences.ColorProperty;
    1921import org.openstreetmap.josm.io.GpxReaderTest;
    2022import org.openstreetmap.josm.tools.ColorHelper;
    2123import org.xml.sax.SAXException;
    public class GpxDrawHelperTest {  
    124126     */
    125127    static List<String> calculateColors(String fileName, String layerName, int n) throws IOException, SAXException {
    126128        final GpxData data = GpxReaderTest.parseGpxData(fileName);
    127         final GpxDrawHelper gdh = new GpxDrawHelper(data);
     129        final GpxDrawHelper gdh = new GpxDrawHelper(data, new ColorProperty("x", Color.MAGENTA));
    128130        gdh.readPreferences(layerName);
    129131        gdh.calculateColors();
    130132        final Iterator<WayPoint> wayPointIterator = data.tracks.iterator().next().getSegments().iterator().next().getWayPoints().iterator();
  • test/unit/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayerTest.java

    diff --git a/test/unit/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayerTest.java b/test/unit/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayerTest.java
    index 19dfbb0..a5596eb 100644
    a b public class MarkerLayerTest {  
    4141        MarkerLayer layer = new MarkerLayer(new GpxData(), "foo", null, null);
    4242
    4343        assertEquals("foo", layer.getName());
    44         assertEquals(Color.magenta, layer.getColor(false));
     44        assertEquals(Color.magenta, layer.getColorProperty().get());
    4545        assertNotNull(layer.getIcon());
    4646        assertEquals("0 markers", layer.getToolTipText());
    4747        assertEquals("<html>foo consists of 0 markers</html>", layer.getInfoComponent());
    public class MarkerLayerTest {  
    5858        layer = new MarkerLayer(gpx, "bar", null, null);
    5959
    6060        assertEquals("bar", layer.getName());
    61         assertEquals(Color.magenta, layer.getColor(false));
     61        assertEquals(Color.magenta, layer.getColorProperty().get());
    6262        assertNotNull(layer.getIcon());
    6363        assertEquals("3 markers", layer.getToolTipText());
    6464        assertEquals("<html>bar consists of 3 markers</html>", layer.getInfoComponent());
  • test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java

    diff --git a/test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java b/test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java
    index 634b625..246bbff 100644
    a b public class JOSMTestRules implements TestRule {  
    159159        Statement statement = base;
    160160        if (timeout > 0) {
    161161            // TODO: new DisableOnDebug(timeout)
    162             statement = new FailOnTimeoutStatement(statement, timeout);
     162           // statement = new FailOnTimeoutStatement(statement, timeout);
    163163        }
    164164        statement = new CreateJosmEnvironment(statement);
    165165        if (josmHome != null) {