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.MapView;
019import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
020import org.openstreetmap.josm.gui.layer.Layer;
021import org.openstreetmap.josm.gui.layer.OsmDataLayer;
022import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
023import org.openstreetmap.josm.gui.util.GuiHelper;
024import org.openstreetmap.josm.tools.Destroyable;
025import org.openstreetmap.josm.tools.ImageProvider;
026import org.openstreetmap.josm.tools.Shortcut;
027
028/**
029 * Base class helper for all Actions in JOSM. Just to make the life easier.
030 *
031 * A JosmAction is a {@link LayerChangeListener} and a {@link SelectionChangedListener}. Upon
032 * a layer change event or a selection change event it invokes {@link #updateEnabledState()}.
033 * Subclasses can override {@link #updateEnabledState()} in order to update the {@link #isEnabled()}-state
034 * of a JosmAction depending on the {@link #getCurrentDataSet()} and the current layers
035 * (see also {@link #getEditLayer()}).
036 *
037 * destroy() from interface Destroyable is called e.g. for MapModes, when the last layer has
038 * been removed and so the mapframe will be destroyed. For other JosmActions, destroy() may never
039 * be called (currently).
040 *
041 * @author imi
042 */
043public abstract class JosmAction extends AbstractAction implements Destroyable {
044
045    protected transient Shortcut sc;
046    private transient LayerChangeAdapter layerChangeAdapter;
047    private transient SelectionChangeAdapter selectionChangeAdapter;
048
049    /**
050     * Returns the shortcut for this action.
051     * @return the shortcut for this action, or "No shortcut" if none is defined
052     */
053    public Shortcut getShortcut() {
054        if (sc == null) {
055            sc = Shortcut.registerShortcut("core:none", tr("No Shortcut"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
056            // as this shortcut is shared by all action that don't want to have a shortcut,
057            // we shouldn't allow the user to change it...
058            // this is handled by special name "core:none"
059        }
060        return sc;
061    }
062
063    /**
064     * Constructs a {@code JosmAction}.
065     *
066     * @param name the action's text as displayed on the menu (if it is added to a menu)
067     * @param icon the icon to use
068     * @param tooltip  a longer description of the action that will be displayed in the tooltip. Please note
069     *           that html is not supported for menu actions on some platforms.
070     * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
071     *            do want a shortcut, remember you can always register it with group=none, so you
072     *            won't be assigned a shortcut unless the user configures one. If you pass null here,
073     *            the user CANNOT configure a shortcut for your action.
074     * @param registerInToolbar register this action for the toolbar preferences?
075     * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null
076     * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
077     */
078    public JosmAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut, boolean registerInToolbar,
079            String toolbarId, boolean installAdapters) {
080        super(name);
081        if (icon != null)
082            icon.getResource().getImageIcon(this);
083        setHelpId();
084        sc = shortcut;
085        if (sc != null) {
086            Main.registerActionShortcut(this, sc);
087        }
088        setTooltip(tooltip);
089        if (getValue("toolbar") == null) {
090            putValue("toolbar", toolbarId);
091        }
092        if (registerInToolbar && Main.toolbar != null) {
093            Main.toolbar.register(this);
094        }
095        if (installAdapters) {
096            installAdapters();
097        }
098    }
099
100    /**
101     * The new super for all actions.
102     *
103     * Use this super constructor to setup your action.
104     *
105     * @param name the action's text as displayed on the menu (if it is added to a menu)
106     * @param iconName the filename of the icon to use
107     * @param tooltip  a longer description of the action that will be displayed in the tooltip. Please note
108     *           that html is not supported for menu actions on some platforms.
109     * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
110     *            do want a shortcut, remember you can always register it with group=none, so you
111     *            won't be assigned a shortcut unless the user configures one. If you pass null here,
112     *            the user CANNOT configure a shortcut for your action.
113     * @param registerInToolbar register this action for the toolbar preferences?
114     * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null
115     * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
116     */
117    public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar,
118            String toolbarId, boolean installAdapters) {
119        this(name, iconName == null ? null : new ImageProvider(iconName), tooltip, shortcut, registerInToolbar,
120                toolbarId == null ? iconName : toolbarId, installAdapters);
121    }
122
123    /**
124     * Constructs a new {@code JosmAction}.
125     *
126     * Use this super constructor to setup your action.
127     *
128     * @param name the action's text as displayed on the menu (if it is added to a menu)
129     * @param iconName the filename of the icon to use
130     * @param tooltip  a longer description of the action that will be displayed in the tooltip. Please note
131     *           that html is not supported for menu actions on some platforms.
132     * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
133     *            do want a shortcut, remember you can always register it with group=none, so you
134     *            won't be assigned a shortcut unless the user configures one. If you pass null here,
135     *            the user CANNOT configure a shortcut for your action.
136     * @param registerInToolbar register this action for the toolbar preferences?
137     * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
138     */
139    public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar, boolean installAdapters) {
140        this(name, iconName, tooltip, shortcut, registerInToolbar, null, installAdapters);
141    }
142
143    /**
144     * Constructs a new {@code JosmAction}.
145     *
146     * Use this super constructor to setup your action.
147     *
148     * @param name the action's text as displayed on the menu (if it is added to a menu)
149     * @param iconName the filename of the icon to use
150     * @param tooltip  a longer description of the action that will be displayed in the tooltip. Please note
151     *           that html is not supported for menu actions on some platforms.
152     * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
153     *            do want a shortcut, remember you can always register it with group=none, so you
154     *            won't be assigned a shortcut unless the user configures one. If you pass null here,
155     *            the user CANNOT configure a shortcut for your action.
156     * @param registerInToolbar register this action for the toolbar preferences?
157     */
158    public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar) {
159        this(name, iconName, tooltip, shortcut, registerInToolbar, null, true);
160    }
161
162    /**
163     * Constructs a new {@code JosmAction}.
164     */
165    public JosmAction() {
166        this(true);
167    }
168
169    /**
170     * Constructs a new {@code JosmAction}.
171     *
172     * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
173     */
174    public JosmAction(boolean installAdapters) {
175        setHelpId();
176        if (installAdapters) {
177            installAdapters();
178        }
179    }
180
181    @Override
182    public void destroy() {
183        if (sc != null) {
184            Main.unregisterActionShortcut(this);
185        }
186        MapView.removeLayerChangeListener(layerChangeAdapter);
187        DataSet.removeSelectionListener(selectionChangeAdapter);
188    }
189
190    private void setHelpId() {
191        String helpId = "Action/"+getClass().getName().substring(getClass().getName().lastIndexOf('.')+1);
192        if (helpId.endsWith("Action")) {
193            helpId = helpId.substring(0, helpId.length()-6);
194        }
195        putValue("help", helpId);
196    }
197
198    /**
199     * Sets the tooltip text of this action.
200     * @param tooltip The text to display in tooltip. Can be {@code null}
201     */
202    public final void setTooltip(String tooltip) {
203        if (tooltip != null) {
204            putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
205        }
206    }
207
208    /**
209     * Replies the current edit layer
210     *
211     * @return the current edit layer. null, if no edit layer exists
212     */
213    public static OsmDataLayer getEditLayer() {
214        return Main.main != null ? Main.main.getEditLayer() : null;
215    }
216
217    /**
218     * Replies the current dataset.
219     *
220     * @return the current dataset. null, if no current dataset exists
221     */
222    public static DataSet getCurrentDataSet() {
223        return Main.main != null ? Main.main.getCurrentDataSet() : null;
224    }
225
226    protected void installAdapters() {
227        // make this action listen to layer change and selection change events
228        //
229        layerChangeAdapter = new LayerChangeAdapter();
230        selectionChangeAdapter = new SelectionChangeAdapter();
231        MapView.addLayerChangeListener(layerChangeAdapter);
232        DataSet.addSelectionListener(selectionChangeAdapter);
233        initEnabledState();
234    }
235
236    protected static void waitFuture(final Future<?> future, final PleaseWaitProgressMonitor monitor) {
237        Main.worker.submit(
238                new Runnable() {
239                    @Override
240                    public void run() {
241                        try {
242                            future.get();
243                        } catch (InterruptedException | ExecutionException | CancellationException e) {
244                            Main.error(e);
245                            return;
246                        }
247                        monitor.close();
248                    }
249                }
250        );
251    }
252
253    /**
254     * Override in subclasses to init the enabled state of an action when it is
255     * created. Default behaviour is to call {@link #updateEnabledState()}
256     *
257     * @see #updateEnabledState()
258     * @see #updateEnabledState(Collection)
259     */
260    protected void initEnabledState() {
261        updateEnabledState();
262    }
263
264    /**
265     * Override in subclasses to update the enabled state of the action when
266     * something in the JOSM state changes, i.e. when a layer is removed or added.
267     *
268     * See {@link #updateEnabledState(Collection)} to respond to changes in the collection
269     * of selected primitives.
270     *
271     * Default behavior is empty.
272     *
273     * @see #updateEnabledState(Collection)
274     * @see #initEnabledState()
275     */
276    protected void updateEnabledState() {
277    }
278
279    /**
280     * Override in subclasses to update the enabled state of the action if the
281     * collection of selected primitives changes. This method is called with the
282     * new selection.
283     *
284     * @param selection the collection of selected primitives; may be empty, but not null
285     *
286     * @see #updateEnabledState()
287     * @see #initEnabledState()
288     */
289    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
290    }
291
292    /**
293     * Adapter for layer change events
294     *
295     */
296    protected class LayerChangeAdapter implements MapView.LayerChangeListener {
297        private void updateEnabledStateInEDT() {
298            GuiHelper.runInEDT(new Runnable() {
299                @Override public void run() {
300                    updateEnabledState();
301                }
302            });
303        }
304
305        @Override
306        public void activeLayerChange(Layer oldLayer, Layer newLayer) {
307            updateEnabledStateInEDT();
308        }
309
310        @Override
311        public void layerAdded(Layer newLayer) {
312            updateEnabledStateInEDT();
313        }
314
315        @Override
316        public void layerRemoved(Layer oldLayer) {
317            updateEnabledStateInEDT();
318        }
319    }
320
321    /**
322     * Adapter for selection change events
323     */
324    protected class SelectionChangeAdapter implements SelectionChangedListener {
325        @Override
326        public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
327            updateEnabledState(newSelection);
328        }
329    }
330}