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.event.KeyEvent;
007import java.util.Collection;
008import java.util.concurrent.CancellationException;
009import java.util.concurrent.ExecutionException;
010import java.util.concurrent.Future;
011
012import javax.swing.AbstractAction;
013
014import org.openstreetmap.josm.Main;
015import org.openstreetmap.josm.data.SelectionChangedListener;
016import org.openstreetmap.josm.data.osm.DataSet;
017import org.openstreetmap.josm.data.osm.OsmPrimitive;
018import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
019import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
020import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
021import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
022import org.openstreetmap.josm.gui.layer.MainLayerManager;
023import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
024import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
025import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
026import org.openstreetmap.josm.tools.Destroyable;
027import org.openstreetmap.josm.tools.ImageProvider;
028import org.openstreetmap.josm.tools.Shortcut;
029
030/**
031 * Base class helper for all Actions in JOSM. Just to make the life easier.
032 *
033 * This action allows you to set up an icon, a tooltip text, a globally registered shortcut, register it in the main toolbar and set up
034 * layer/selection listeners that call {@link #updateEnabledState()} whenever the global context is changed.
035 *
036 * A JosmAction can register a {@link LayerChangeListener} and a {@link SelectionChangedListener}. Upon
037 * a layer change event or a selection change event it invokes {@link #updateEnabledState()}.
038 * Subclasses can override {@link #updateEnabledState()} in order to update the {@link #isEnabled()}-state
039 * of a JosmAction depending on the {@link #getLayerManager()} state.
040 *
041 * destroy() from interface Destroyable is called e.g. for MapModes, when the last layer has
042 * been removed and so the mapframe will be destroyed. For other JosmActions, destroy() may never
043 * be called (currently).
044 *
045 * @author imi
046 */
047public abstract class JosmAction extends AbstractAction implements Destroyable {
048
049    protected transient Shortcut sc;
050    private transient LayerChangeAdapter layerChangeAdapter;
051    private transient ActiveLayerChangeAdapter activeLayerChangeAdapter;
052    private transient SelectionChangeAdapter selectionChangeAdapter;
053
054    /**
055     * Constructs a {@code JosmAction}.
056     *
057     * @param name the action's text as displayed on the menu (if it is added to a menu)
058     * @param icon the icon to use
059     * @param tooltip  a longer description of the action that will be displayed in the tooltip. Please note
060     *           that html is not supported for menu actions on some platforms.
061     * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
062     *            do want a shortcut, remember you can always register it with group=none, so you
063     *            won't be assigned a shortcut unless the user configures one. If you pass null here,
064     *            the user CANNOT configure a shortcut for your action.
065     * @param registerInToolbar register this action for the toolbar preferences?
066     * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null
067     * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
068     */
069    public JosmAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut, boolean registerInToolbar,
070            String toolbarId, boolean installAdapters) {
071        super(name);
072        if (icon != null)
073            icon.getResource().attachImageIcon(this, true);
074        setHelpId();
075        sc = shortcut;
076        if (sc != null) {
077            Main.registerActionShortcut(this, sc);
078        }
079        setTooltip(tooltip);
080        if (getValue("toolbar") == null) {
081            putValue("toolbar", toolbarId);
082        }
083        if (registerInToolbar && Main.toolbar != null) {
084            Main.toolbar.register(this);
085        }
086        if (installAdapters) {
087            installAdapters();
088        }
089    }
090
091    /**
092     * The new super for all actions.
093     *
094     * Use this super constructor to setup your action.
095     *
096     * @param name the action's text as displayed on the menu (if it is added to a menu)
097     * @param iconName the filename of the icon to use
098     * @param tooltip  a longer description of the action that will be displayed in the tooltip. Please note
099     *           that html is not supported for menu actions on some platforms.
100     * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
101     *            do want a shortcut, remember you can always register it with group=none, so you
102     *            won't be assigned a shortcut unless the user configures one. If you pass null here,
103     *            the user CANNOT configure a shortcut for your action.
104     * @param registerInToolbar register this action for the toolbar preferences?
105     * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null
106     * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
107     */
108    public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar,
109            String toolbarId, boolean installAdapters) {
110        this(name, iconName == null ? null : new ImageProvider(iconName), tooltip, shortcut, registerInToolbar,
111                toolbarId == null ? iconName : toolbarId, installAdapters);
112    }
113
114    /**
115     * Constructs a new {@code JosmAction}.
116     *
117     * Use this super constructor to setup your action.
118     *
119     * @param name the action's text as displayed on the menu (if it is added to a menu)
120     * @param iconName the filename of the icon to use
121     * @param tooltip  a longer description of the action that will be displayed in the tooltip. Please note
122     *           that html is not supported for menu actions on some platforms.
123     * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
124     *            do want a shortcut, remember you can always register it with group=none, so you
125     *            won't be assigned a shortcut unless the user configures one. If you pass null here,
126     *            the user CANNOT configure a shortcut for your action.
127     * @param registerInToolbar register this action for the toolbar preferences?
128     * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
129     */
130    public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar, boolean installAdapters) {
131        this(name, iconName, tooltip, shortcut, registerInToolbar, null, installAdapters);
132    }
133
134    /**
135     * Constructs a new {@code JosmAction}.
136     *
137     * Use this super constructor to setup your action.
138     *
139     * @param name the action's text as displayed on the menu (if it is added to a menu)
140     * @param iconName the filename of the icon to use
141     * @param tooltip  a longer description of the action that will be displayed in the tooltip. Please note
142     *           that html is not supported for menu actions on some platforms.
143     * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
144     *            do want a shortcut, remember you can always register it with group=none, so you
145     *            won't be assigned a shortcut unless the user configures one. If you pass null here,
146     *            the user CANNOT configure a shortcut for your action.
147     * @param registerInToolbar register this action for the toolbar preferences?
148     */
149    public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar) {
150        this(name, iconName, tooltip, shortcut, registerInToolbar, null, true);
151    }
152
153    /**
154     * Constructs a new {@code JosmAction}.
155     */
156    public JosmAction() {
157        this(true);
158    }
159
160    /**
161     * Constructs a new {@code JosmAction}.
162     *
163     * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
164     */
165    public JosmAction(boolean installAdapters) {
166        setHelpId();
167        if (installAdapters) {
168            installAdapters();
169        }
170    }
171
172    /**
173     * Installs the listeners to this action.
174     * <p>
175     * This should either never be called or only called in the constructor of this action.
176     * <p>
177     * All registered adapters should be removed in {@link #destroy()}
178     */
179    protected void installAdapters() {
180        // make this action listen to layer change and selection change events
181        if (listenToLayerChange()) {
182            layerChangeAdapter = new LayerChangeAdapter();
183            activeLayerChangeAdapter = new ActiveLayerChangeAdapter();
184            getLayerManager().addLayerChangeListener(layerChangeAdapter);
185            getLayerManager().addActiveLayerChangeListener(activeLayerChangeAdapter);
186        }
187        if (listenToSelectionChange()) {
188            selectionChangeAdapter = new SelectionChangeAdapter();
189            DataSet.addSelectionListener(selectionChangeAdapter);
190        }
191        initEnabledState();
192    }
193
194    /**
195     * Overwrite this if {@link #updateEnabledState()} should be called when the active / availabe layers change. Default is true.
196     * @return <code>true</code> if a {@link LayerChangeListener} and a {@link ActiveLayerChangeListener} should be registered.
197     * @since 10353
198     */
199    protected boolean listenToLayerChange() {
200        return true;
201    }
202
203    /**
204     * Overwrite this if {@link #updateEnabledState()} should be called when the selection changed. Default is true.
205     * @return <code>true</code> if a {@link SelectionChangedListener} should be registered.
206     * @since 10353
207     */
208    protected boolean listenToSelectionChange() {
209        return true;
210    }
211
212    @Override
213    public void destroy() {
214        if (sc != null) {
215            Main.unregisterActionShortcut(this);
216        }
217        if (layerChangeAdapter != null) {
218            getLayerManager().removeLayerChangeListener(layerChangeAdapter);
219            getLayerManager().removeActiveLayerChangeListener(activeLayerChangeAdapter);
220        }
221        if (selectionChangeAdapter != null) {
222            DataSet.removeSelectionListener(selectionChangeAdapter);
223        }
224    }
225
226    private void setHelpId() {
227        String helpId = "Action/"+getClass().getName().substring(getClass().getName().lastIndexOf('.')+1);
228        if (helpId.endsWith("Action")) {
229            helpId = helpId.substring(0, helpId.length()-6);
230        }
231        putValue("help", helpId);
232    }
233
234    /**
235     * Returns the shortcut for this action.
236     * @return the shortcut for this action, or "No shortcut" if none is defined
237     */
238    public Shortcut getShortcut() {
239        if (sc == null) {
240            sc = Shortcut.registerShortcut("core:none", tr("No Shortcut"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
241            // as this shortcut is shared by all action that don't want to have a shortcut,
242            // we shouldn't allow the user to change it...
243            // this is handled by special name "core:none"
244        }
245        return sc;
246    }
247
248    /**
249     * Sets the tooltip text of this action.
250     * @param tooltip The text to display in tooltip. Can be {@code null}
251     */
252    public final void setTooltip(String tooltip) {
253        if (tooltip != null) {
254            putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
255        }
256    }
257
258    /**
259     * Gets the layer manager used for this action. Defaults to the main layer manager but you can overwrite this.
260     * <p>
261     * The layer manager must be available when {@link #installAdapters()} is called and must not change.
262     *
263     * @return The layer manager.
264     * @since 10353
265     */
266    public MainLayerManager getLayerManager() {
267        return Main.getLayerManager();
268    }
269
270    protected static void waitFuture(final Future<?> future, final PleaseWaitProgressMonitor monitor) {
271        Main.worker.submit(() -> {
272                        try {
273                            future.get();
274                        } catch (InterruptedException | ExecutionException | CancellationException e) {
275                            Main.error(e);
276                            return;
277                        }
278                        monitor.close();
279                    });
280    }
281
282    /**
283     * Override in subclasses to init the enabled state of an action when it is
284     * created. Default behaviour is to call {@link #updateEnabledState()}
285     *
286     * @see #updateEnabledState()
287     * @see #updateEnabledState(Collection)
288     */
289    protected void initEnabledState() {
290        updateEnabledState();
291    }
292
293    /**
294     * Override in subclasses to update the enabled state of the action when
295     * something in the JOSM state changes, i.e. when a layer is removed or added.
296     *
297     * See {@link #updateEnabledState(Collection)} to respond to changes in the collection
298     * of selected primitives.
299     *
300     * Default behavior is empty.
301     *
302     * @see #updateEnabledState(Collection)
303     * @see #initEnabledState()
304     * @see #listenToLayerChange()
305     */
306    protected void updateEnabledState() {
307    }
308
309    /**
310     * Override in subclasses to update the enabled state of the action if the
311     * collection of selected primitives changes. This method is called with the
312     * new selection.
313     *
314     * @param selection the collection of selected primitives; may be empty, but not null
315     *
316     * @see #updateEnabledState()
317     * @see #initEnabledState()
318     * @see #listenToSelectionChange()
319     */
320    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
321    }
322
323    /**
324     * Updates enabled state according to primitives currently selected in edit data set, if any.
325     * Can be called in {@link #updateEnabledState()} implementations.
326     * @since 10409
327     */
328    protected final void updateEnabledStateOnCurrentSelection() {
329        DataSet ds = getLayerManager().getEditDataSet();
330        if (ds == null) {
331            setEnabled(false);
332        } else {
333            updateEnabledState(ds.getSelected());
334        }
335    }
336
337    /**
338     * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed.
339     */
340    protected class LayerChangeAdapter implements LayerChangeListener {
341        @Override
342        public void layerAdded(LayerAddEvent e) {
343            updateEnabledState();
344        }
345
346        @Override
347        public void layerRemoving(LayerRemoveEvent e) {
348            updateEnabledState();
349        }
350
351        @Override
352        public void layerOrderChanged(LayerOrderChangeEvent e) {
353            updateEnabledState();
354        }
355
356        @Override
357        public String toString() {
358            return "LayerChangeAdapter [" + JosmAction.this.toString() + ']';
359        }
360    }
361
362    /**
363     * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed.
364     */
365    protected class ActiveLayerChangeAdapter implements ActiveLayerChangeListener {
366        @Override
367        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
368            updateEnabledState();
369        }
370
371        @Override
372        public String toString() {
373            return "ActiveLayerChangeAdapter [" + JosmAction.this.toString() + ']';
374        }
375    }
376
377    /**
378     * Adapter for selection change events. Runs updateEnabledState() whenever the selection changed.
379     */
380    protected class SelectionChangeAdapter implements SelectionChangedListener {
381        @Override
382        public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
383            updateEnabledState(newSelection);
384        }
385
386        @Override
387        public String toString() {
388            return "SelectionChangeAdapter [" + JosmAction.this.toString() + ']';
389        }
390    }
391}