001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.event.ActionEvent;
008
009import org.openstreetmap.josm.data.preferences.BooleanProperty;
010import org.openstreetmap.josm.gui.MainApplication;
011import org.openstreetmap.josm.tools.ListenerList;
012
013/**
014 * This action toggles the Expert mode.
015 * @since 4840
016 */
017public class ExpertToggleAction extends ToggleAction {
018
019    /**
020     * This listener is notified whenever the expert mode setting changed.
021     */
022    @FunctionalInterface
023    public interface ExpertModeChangeListener {
024        /**
025         * The expert mode changed.
026         * @param isExpert <code>true</code> if expert mode was enabled, false otherwise.
027         */
028        void expertChanged(boolean isExpert);
029    }
030
031    // TODO: Switch to checked list. We can do this as soon as we do not see any more warnings.
032    private static final ListenerList<ExpertModeChangeListener> listeners = ListenerList.createUnchecked();
033    private static final ListenerList<Component> visibilityToggleListeners = ListenerList.createUnchecked();
034
035    private static final BooleanProperty PREF_EXPERT = new BooleanProperty("expert", false);
036
037    private static final ExpertToggleAction INSTANCE = new ExpertToggleAction();
038
039    private static synchronized void fireExpertModeChanged(boolean isExpert) {
040        listeners.fireEvent(listener -> listener.expertChanged(isExpert));
041        visibilityToggleListeners.fireEvent(c -> c.setVisible(isExpert));
042    }
043
044    /**
045     * Register a expert mode change listener.
046     *
047     * @param listener the listener. Ignored if null.
048     */
049    public static void addExpertModeChangeListener(ExpertModeChangeListener listener) {
050        addExpertModeChangeListener(listener, false);
051    }
052
053    /**
054     * Register a expert mode change listener, and optionnally fires it.
055     * @param listener the listener. Ignored if null.
056     * @param fireWhenAdding if true, the listener will be fired immediately after added
057     */
058    public static synchronized void addExpertModeChangeListener(ExpertModeChangeListener listener, boolean fireWhenAdding) {
059        if (listener == null) return;
060        listeners.addWeakListener(listener);
061        if (fireWhenAdding) {
062            listener.expertChanged(isExpert());
063        }
064    }
065
066    /**
067     * Removes a expert mode change listener
068     *
069     * @param listener the listener. Ignored if null.
070     */
071    public static synchronized void removeExpertModeChangeListener(ExpertModeChangeListener listener) {
072        if (listener == null) return;
073        listeners.removeListener(listener);
074    }
075
076    /**
077     * Marks a component to be only visible when expert mode is enabled. The visibility of the component is changed automatically.
078     * @param c The component.
079     */
080    public static synchronized void addVisibilitySwitcher(Component c) {
081        if (c == null) return;
082        visibilityToggleListeners.addWeakListener(c);
083        c.setVisible(isExpert());
084    }
085
086    /**
087     * Stops tracking visibility changes for the given component.
088     * @param c The component.
089     * @see #addVisibilitySwitcher(Component)
090     */
091    public static synchronized void removeVisibilitySwitcher(Component c) {
092        if (c == null) return;
093        visibilityToggleListeners.removeListener(c);
094    }
095
096    /**
097     * Determines if the given component tracks visibility changes.
098     * @param c The component.
099     * @return {@code true} if the given component tracks visibility changes
100     * @since 15649
101     */
102    public static synchronized boolean hasVisibilitySwitcher(Component c) {
103        if (c == null) return false;
104        return visibilityToggleListeners.containsListener(c);
105    }
106
107    /**
108     * Constructs a new {@code ExpertToggleAction}.
109     */
110    public ExpertToggleAction() {
111        super(tr("Expert Mode"),
112              "expert",
113              tr("Enable/disable expert mode"),
114              null,
115              false /* register toolbar */
116        );
117        putValue("toolbar", "expertmode");
118        if (MainApplication.getToolbar() != null) {
119            MainApplication.getToolbar().register(this);
120        }
121        setSelected(PREF_EXPERT.get());
122        notifySelectedState();
123    }
124
125    @Override
126    protected final void notifySelectedState() {
127        super.notifySelectedState();
128        PREF_EXPERT.put(isSelected());
129        fireExpertModeChanged(isSelected());
130    }
131
132    /**
133     * Forces the expert mode state to the given state.
134     * @param isExpert if expert mode should be used.
135     * @since 11224
136     */
137    public void setExpert(boolean isExpert) {
138        if (isSelected() != isExpert) {
139            setSelected(isExpert);
140            notifySelectedState();
141        }
142    }
143
144    @Override
145    public void actionPerformed(ActionEvent e) {
146        toggleSelectedState(e);
147        notifySelectedState();
148    }
149
150    /**
151     * Replies the unique instance of this action.
152     * @return The unique instance of this action
153     */
154    public static ExpertToggleAction getInstance() {
155        return INSTANCE;
156    }
157
158    /**
159     * Determines if expert mode is enabled.
160     * @return {@code true} if expert mode is enabled, {@code false} otherwise.
161     */
162    public static boolean isExpert() {
163        return INSTANCE.isSelected();
164    }
165}