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