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.GridBagLayout;
007import java.awt.event.KeyEvent;
008import java.util.Collection;
009import java.util.List;
010import java.util.concurrent.CancellationException;
011import java.util.concurrent.ExecutionException;
012import java.util.concurrent.Future;
013
014import javax.swing.AbstractAction;
015import javax.swing.JOptionPane;
016import javax.swing.JPanel;
017
018import org.openstreetmap.josm.command.Command;
019import org.openstreetmap.josm.data.osm.DataSelectionListener;
020import org.openstreetmap.josm.data.osm.DataSet;
021import org.openstreetmap.josm.data.osm.OsmPrimitive;
022import org.openstreetmap.josm.data.osm.OsmUtils;
023import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
024import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
025import org.openstreetmap.josm.gui.MainApplication;
026import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
027import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
028import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
029import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
030import org.openstreetmap.josm.gui.layer.MainLayerManager;
031import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
032import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
033import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
034import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
035import org.openstreetmap.josm.tools.Destroyable;
036import org.openstreetmap.josm.tools.ImageProvider;
037import org.openstreetmap.josm.tools.ImageResource;
038import org.openstreetmap.josm.tools.Logging;
039import org.openstreetmap.josm.tools.Shortcut;
040
041/**
042 * Base class helper for all Actions in JOSM. Just to make the life easier.
043 *
044 * 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
045 * layer/selection listeners that call {@link #updateEnabledState()} whenever the global context is changed.
046 *
047 * A JosmAction can register a {@link LayerChangeListener} and a {@link DataSelectionListener}. Upon
048 * a layer change event or a selection change event it invokes {@link #updateEnabledState()}.
049 * Subclasses can override {@link #updateEnabledState()} in order to update the {@link #isEnabled()}-state
050 * of a JosmAction depending on the {@link #getLayerManager()} state.
051 *
052 * destroy() from interface Destroyable is called e.g. for MapModes, when the last layer has
053 * been removed and so the mapframe will be destroyed. For other JosmActions, destroy() may never
054 * be called (currently).
055 *
056 * @author imi
057 */
058public abstract class JosmAction extends AbstractAction implements Destroyable {
059
060    protected transient Shortcut sc;
061    private transient LayerChangeAdapter layerChangeAdapter;
062    private transient ActiveLayerChangeAdapter activeLayerChangeAdapter;
063    private transient SelectionChangeAdapter selectionChangeAdapter;
064
065    /**
066     * Constructs a {@code JosmAction}.
067     *
068     * @param name the action's text as displayed on the menu (if it is added to a menu)
069     * @param icon the icon to use
070     * @param tooltip  a longer description of the action that will be displayed in the tooltip. Please note
071     *           that html is not supported for menu actions on some platforms.
072     * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
073     *            do want a shortcut, remember you can always register it with group=none, so you
074     *            won't be assigned a shortcut unless the user configures one. If you pass null here,
075     *            the user CANNOT configure a shortcut for your action.
076     * @param registerInToolbar register this action for the toolbar preferences?
077     * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null
078     * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
079     */
080    public JosmAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut, boolean registerInToolbar,
081            String toolbarId, boolean installAdapters) {
082        super(name);
083        if (icon != null) {
084            ImageResource resource = icon.getResource();
085            if (resource != null) {
086                try {
087                    resource.attachImageIcon(this, true);
088                } catch (RuntimeException e) {
089                    Logging.warn("Unable to attach image icon {0} for action {1}", icon, name);
090                    Logging.error(e);
091                }
092            }
093        }
094        setHelpId();
095        sc = shortcut;
096        if (sc != null && !sc.isAutomatic()) {
097            MainApplication.registerActionShortcut(this, sc);
098        }
099        setTooltip(tooltip);
100        if (getValue("toolbar") == null) {
101            putValue("toolbar", toolbarId);
102        }
103        if (registerInToolbar && MainApplication.getToolbar() != null) {
104            MainApplication.getToolbar().register(this);
105        }
106        if (installAdapters) {
107            installAdapters();
108        }
109    }
110
111    /**
112     * The new super for all actions.
113     *
114     * Use this super constructor to setup your action.
115     *
116     * @param name the action's text as displayed on the menu (if it is added to a menu)
117     * @param iconName the filename of the icon to use
118     * @param tooltip  a longer description of the action that will be displayed in the tooltip. Please note
119     *           that html is not supported for menu actions on some platforms.
120     * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
121     *            do want a shortcut, remember you can always register it with group=none, so you
122     *            won't be assigned a shortcut unless the user configures one. If you pass null here,
123     *            the user CANNOT configure a shortcut for your action.
124     * @param registerInToolbar register this action for the toolbar preferences?
125     * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null
126     * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
127     */
128    public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar,
129            String toolbarId, boolean installAdapters) {
130        this(name, iconName == null ? null : new ImageProvider(iconName).setOptional(true), tooltip, shortcut, registerInToolbar,
131                toolbarId == null ? iconName : toolbarId, 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     * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
149     */
150    public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar, boolean installAdapters) {
151        this(name, iconName, tooltip, shortcut, registerInToolbar, null, installAdapters);
152    }
153
154    /**
155     * Constructs a new {@code JosmAction}.
156     *
157     * Use this super constructor to setup your action.
158     *
159     * @param name the action's text as displayed on the menu (if it is added to a menu)
160     * @param iconName the filename of the icon to use
161     * @param tooltip  a longer description of the action that will be displayed in the tooltip. Please note
162     *           that html is not supported for menu actions on some platforms.
163     * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always
164     *            do want a shortcut, remember you can always register it with group=none, so you
165     *            won't be assigned a shortcut unless the user configures one. If you pass null here,
166     *            the user CANNOT configure a shortcut for your action.
167     * @param registerInToolbar register this action for the toolbar preferences?
168     */
169    public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar) {
170        this(name, iconName, tooltip, shortcut, registerInToolbar, null, true);
171    }
172
173    /**
174     * Constructs a new {@code JosmAction}.
175     */
176    public JosmAction() {
177        this(true);
178    }
179
180    /**
181     * Constructs a new {@code JosmAction}.
182     *
183     * @param installAdapters false, if you don't want to install layer changed and selection changed adapters
184     */
185    public JosmAction(boolean installAdapters) {
186        setHelpId();
187        if (installAdapters) {
188            installAdapters();
189        }
190    }
191
192    /**
193     * Constructs a new {@code JosmAction}.
194     *
195     * Use this super constructor to setup your action.
196     *
197     * @param name the action's text as displayed on the menu (if it is added to a menu)
198     * @param iconName the filename of the icon to use
199     * @param tooltip  a longer description of the action that will be displayed in the tooltip. Please note
200     *           that html is not supported for menu actions on some platforms.
201     * @param shortcuts ready-created shortcut objects
202     * @since 14012
203     */
204    public JosmAction(String name, String iconName, String tooltip, List<Shortcut> shortcuts) {
205        this(name, iconName, tooltip, shortcuts.get(0), true, null, true);
206        for (int i = 1; i < shortcuts.size(); i++) {
207            MainApplication.registerActionShortcut(this, shortcuts.get(i));
208        }
209    }
210
211    /**
212     * Installs the listeners to this action.
213     * <p>
214     * This should either never be called or only called in the constructor of this action.
215     * <p>
216     * All registered adapters should be removed in {@link #destroy()}
217     */
218    protected void installAdapters() {
219        // make this action listen to layer change and selection change events
220        if (listenToLayerChange()) {
221            layerChangeAdapter = buildLayerChangeAdapter();
222            activeLayerChangeAdapter = buildActiveLayerChangeAdapter();
223            getLayerManager().addLayerChangeListener(layerChangeAdapter);
224            getLayerManager().addActiveLayerChangeListener(activeLayerChangeAdapter);
225        }
226        if (listenToSelectionChange()) {
227            selectionChangeAdapter = new SelectionChangeAdapter();
228            SelectionEventManager.getInstance().addSelectionListenerForEdt(selectionChangeAdapter);
229        }
230        initEnabledState();
231    }
232
233    /**
234     * Override this if calling {@link #updateEnabledState()} on layer change events is not enough.
235     * @return the {@link LayerChangeAdapter} that will be called on layer change events
236     * @since 15404
237     */
238    protected LayerChangeAdapter buildLayerChangeAdapter() {
239        return new LayerChangeAdapter();
240    }
241
242    /**
243     * Override this if calling {@link #updateEnabledState()} on active layer change event is not enough.
244     * @return the {@link LayerChangeAdapter} that will be called on active layer change event
245     * @since 15404
246     */
247    protected ActiveLayerChangeAdapter buildActiveLayerChangeAdapter() {
248        return new ActiveLayerChangeAdapter();
249    }
250
251    /**
252     * Overwrite this if {@link #updateEnabledState()} should be called when the active / available layers change. Default is true.
253     * @return <code>true</code> if a {@link LayerChangeListener} and a {@link ActiveLayerChangeListener} should be registered.
254     * @since 10353
255     */
256    protected boolean listenToLayerChange() {
257        return true;
258    }
259
260    /**
261     * Overwrite this if {@link #updateEnabledState()} should be called when the selection changed. Default is true.
262     * @return <code>true</code> if a {@link DataSelectionListener} should be registered.
263     * @since 10353
264     */
265    protected boolean listenToSelectionChange() {
266        return true;
267    }
268
269    @Override
270    public void destroy() {
271        if (sc != null && !sc.isAutomatic()) {
272            MainApplication.unregisterActionShortcut(this);
273        }
274        if (layerChangeAdapter != null) {
275            getLayerManager().removeLayerChangeListener(layerChangeAdapter);
276            getLayerManager().removeActiveLayerChangeListener(activeLayerChangeAdapter);
277        }
278        if (selectionChangeAdapter != null) {
279            SelectionEventManager.getInstance().removeSelectionListener(selectionChangeAdapter);
280        }
281        MainApplication.getToolbar().unregister(this);
282    }
283
284    private void setHelpId() {
285        String helpId = "Action/"+getClass().getName().substring(getClass().getName().lastIndexOf('.')+1);
286        if (helpId.endsWith("Action")) {
287            helpId = helpId.substring(0, helpId.length()-6);
288        }
289        setHelpId(helpId);
290    }
291
292    protected void setHelpId(String helpId) {
293        putValue("help", helpId);
294    }
295
296    /**
297     * Returns the shortcut for this action.
298     * @return the shortcut for this action, or "No shortcut" if none is defined
299     */
300    public Shortcut getShortcut() {
301        if (sc == null) {
302            sc = Shortcut.registerShortcut("core:none", tr("No Shortcut"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
303            // as this shortcut is shared by all action that don't want to have a shortcut,
304            // we shouldn't allow the user to change it...
305            // this is handled by special name "core:none"
306        }
307        return sc;
308    }
309
310    /**
311     * Sets the tooltip text of this action.
312     * @param tooltip The text to display in tooltip. Can be {@code null}
313     */
314    public final void setTooltip(String tooltip) {
315        if (tooltip != null && sc != null) {
316            sc.setTooltip(this, tooltip);
317        } else if (tooltip != null) {
318            putValue(SHORT_DESCRIPTION, tooltip);
319        }
320    }
321
322    /**
323     * Gets the layer manager used for this action. Defaults to the main layer manager but you can overwrite this.
324     * <p>
325     * The layer manager must be available when {@link #installAdapters()} is called and must not change.
326     *
327     * @return The layer manager.
328     * @since 10353
329     */
330    public MainLayerManager getLayerManager() {
331        return MainApplication.getLayerManager();
332    }
333
334    protected static void waitFuture(final Future<?> future, final PleaseWaitProgressMonitor monitor) {
335        MainApplication.worker.submit(() -> {
336                        try {
337                            future.get();
338                        } catch (InterruptedException | ExecutionException | CancellationException e) {
339                            Logging.error(e);
340                            return;
341                        }
342                        monitor.close();
343                    });
344    }
345
346    /**
347     * Override in subclasses to init the enabled state of an action when it is
348     * created. Default behaviour is to call {@link #updateEnabledState()}
349     *
350     * @see #updateEnabledState()
351     * @see #updateEnabledState(Collection)
352     */
353    protected void initEnabledState() {
354        updateEnabledState();
355    }
356
357    /**
358     * Override in subclasses to update the enabled state of the action when
359     * something in the JOSM state changes, i.e. when a layer is removed or added.
360     *
361     * See {@link #updateEnabledState(Collection)} to respond to changes in the collection
362     * of selected primitives.
363     *
364     * Default behavior is empty.
365     *
366     * @see #updateEnabledState(Collection)
367     * @see #initEnabledState()
368     * @see #listenToLayerChange()
369     */
370    protected void updateEnabledState() {
371    }
372
373    /**
374     * Override in subclasses to update the enabled state of the action if the
375     * collection of selected primitives changes. This method is called with the
376     * new selection.
377     *
378     * @param selection the collection of selected primitives; may be empty, but not null
379     *
380     * @see #updateEnabledState()
381     * @see #initEnabledState()
382     * @see #listenToSelectionChange()
383     */
384    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
385    }
386
387    /**
388     * Updates enabled state according to primitives currently selected in edit data set, if any.
389     * Can be called in {@link #updateEnabledState()} implementations.
390     * @see #updateEnabledStateOnCurrentSelection(boolean)
391     * @since 10409
392     */
393    protected final void updateEnabledStateOnCurrentSelection() {
394        updateEnabledStateOnCurrentSelection(false);
395    }
396
397    /**
398     * Updates enabled state according to primitives currently selected in active data set, if any.
399     * Can be called in {@link #updateEnabledState()} implementations.
400     * @param allowReadOnly if {@code true}, read-only data sets are considered
401     * @since 13434
402     */
403    protected final void updateEnabledStateOnCurrentSelection(boolean allowReadOnly) {
404        DataSet ds = getLayerManager().getActiveDataSet();
405        if (ds != null && (allowReadOnly || !ds.isLocked())) {
406            updateEnabledState(ds.getSelected());
407        } else {
408            setEnabled(false);
409        }
410    }
411
412    /**
413     * Updates enabled state according to selected primitives, if any.
414     * Enables action if the collection is not empty and references primitives in a modifiable data layer.
415     * Can be called in {@link #updateEnabledState(Collection)} implementations.
416     * @param selection the collection of selected primitives
417     * @since 13434
418     */
419    protected final void updateEnabledStateOnModifiableSelection(Collection<? extends OsmPrimitive> selection) {
420        setEnabled(OsmUtils.isOsmCollectionEditable(selection));
421    }
422
423    /**
424     * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed.
425     */
426    protected class LayerChangeAdapter implements LayerChangeListener {
427        @Override
428        public void layerAdded(LayerAddEvent e) {
429            updateEnabledState();
430        }
431
432        @Override
433        public void layerRemoving(LayerRemoveEvent e) {
434            updateEnabledState();
435        }
436
437        @Override
438        public void layerOrderChanged(LayerOrderChangeEvent e) {
439            updateEnabledState();
440        }
441
442        @Override
443        public String toString() {
444            return "LayerChangeAdapter [" + JosmAction.this + ']';
445        }
446    }
447
448    /**
449     * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed.
450     */
451    protected class ActiveLayerChangeAdapter implements ActiveLayerChangeListener {
452        @Override
453        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
454            updateEnabledState();
455        }
456
457        @Override
458        public String toString() {
459            return "ActiveLayerChangeAdapter [" + JosmAction.this + ']';
460        }
461    }
462
463    /**
464     * Adapter for selection change events. Runs updateEnabledState() whenever the selection changed.
465     */
466    protected class SelectionChangeAdapter implements DataSelectionListener {
467        @Override
468        public void selectionChanged(SelectionChangeEvent event) {
469            updateEnabledState(event.getSelection());
470        }
471
472        @Override
473        public String toString() {
474            return "SelectionChangeAdapter [" + JosmAction.this + ']';
475        }
476    }
477
478    /**
479     * Check whether user is about to operate on data outside of the download area.
480     * Request confirmation if he is.
481     *
482     * @param operation the operation name which is used for setting some preferences
483     * @param dialogTitle the title of the dialog being displayed
484     * @param outsideDialogMessage the message text to be displayed when data is outside of the download area
485     * @param incompleteDialogMessage the message text to be displayed when data is incomplete
486     * @param primitives the primitives to operate on
487     * @param ignore {@code null} or a primitive to be ignored
488     * @return true, if operating on outlying primitives is OK; false, otherwise
489     * @since 12749 (moved from Command)
490     */
491    public static boolean checkAndConfirmOutlyingOperation(String operation,
492            String dialogTitle, String outsideDialogMessage, String incompleteDialogMessage,
493            Collection<? extends OsmPrimitive> primitives,
494            Collection<? extends OsmPrimitive> ignore) {
495        int checkRes = Command.checkOutlyingOrIncompleteOperation(primitives, ignore);
496        if ((checkRes & Command.IS_OUTSIDE) != 0) {
497            JPanel msg = new JPanel(new GridBagLayout());
498            msg.add(new JMultilineLabel("<html>" + outsideDialogMessage + "</html>"));
499            boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
500                    operation + "_outside_nodes",
501                    MainApplication.getMainFrame(),
502                    msg,
503                    dialogTitle,
504                    JOptionPane.YES_NO_OPTION,
505                    JOptionPane.QUESTION_MESSAGE,
506                    JOptionPane.YES_OPTION);
507            if (!answer)
508                return false;
509        }
510        if ((checkRes & Command.IS_INCOMPLETE) != 0) {
511            JPanel msg = new JPanel(new GridBagLayout());
512            msg.add(new JMultilineLabel("<html>" + incompleteDialogMessage + "</html>"));
513            boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
514                    operation + "_incomplete",
515                    MainApplication.getMainFrame(),
516                    msg,
517                    dialogTitle,
518                    JOptionPane.YES_NO_OPTION,
519                    JOptionPane.QUESTION_MESSAGE,
520                    JOptionPane.YES_OPTION);
521            if (!answer)
522                return false;
523        }
524        return true;
525    }
526}