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