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        if (MainApplication.getToolbar() != null) {
282            MainApplication.getToolbar().unregister(this);
283        }
284    }
285
286    private void setHelpId() {
287        String helpId = "Action/"+getClass().getName().substring(getClass().getName().lastIndexOf('.')+1);
288        if (helpId.endsWith("Action")) {
289            helpId = helpId.substring(0, helpId.length()-6);
290        }
291        setHelpId(helpId);
292    }
293
294    protected void setHelpId(String helpId) {
295        putValue("help", helpId);
296    }
297
298    /**
299     * Returns the shortcut for this action.
300     * @return the shortcut for this action, or "No shortcut" if none is defined
301     */
302    public Shortcut getShortcut() {
303        if (sc == null) {
304            sc = Shortcut.registerShortcut("core:none", tr("No Shortcut"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
305            // as this shortcut is shared by all action that don't want to have a shortcut,
306            // we shouldn't allow the user to change it...
307            // this is handled by special name "core:none"
308        }
309        return sc;
310    }
311
312    /**
313     * Sets the tooltip text of this action.
314     * @param tooltip The text to display in tooltip. Can be {@code null}
315     */
316    public final void setTooltip(String tooltip) {
317        if (tooltip != null && sc != null) {
318            sc.setTooltip(this, tooltip);
319        } else if (tooltip != null) {
320            putValue(SHORT_DESCRIPTION, tooltip);
321        }
322    }
323
324    /**
325     * Gets the layer manager used for this action. Defaults to the main layer manager but you can overwrite this.
326     * <p>
327     * The layer manager must be available when {@link #installAdapters()} is called and must not change.
328     *
329     * @return The layer manager.
330     * @since 10353
331     */
332    public MainLayerManager getLayerManager() {
333        return MainApplication.getLayerManager();
334    }
335
336    protected static void waitFuture(final Future<?> future, final PleaseWaitProgressMonitor monitor) {
337        MainApplication.worker.submit(() -> {
338                        try {
339                            future.get();
340                        } catch (InterruptedException | ExecutionException | CancellationException e) {
341                            Logging.error(e);
342                            return;
343                        }
344                        monitor.close();
345                    });
346    }
347
348    /**
349     * Override in subclasses to init the enabled state of an action when it is
350     * created. Default behaviour is to call {@link #updateEnabledState()}
351     *
352     * @see #updateEnabledState()
353     * @see #updateEnabledState(Collection)
354     */
355    protected void initEnabledState() {
356        updateEnabledState();
357    }
358
359    /**
360     * Override in subclasses to update the enabled state of the action when
361     * something in the JOSM state changes, i.e. when a layer is removed or added.
362     *
363     * See {@link #updateEnabledState(Collection)} to respond to changes in the collection
364     * of selected primitives.
365     *
366     * Default behavior is empty.
367     *
368     * @see #updateEnabledState(Collection)
369     * @see #initEnabledState()
370     * @see #listenToLayerChange()
371     */
372    protected void updateEnabledState() {
373    }
374
375    /**
376     * Override in subclasses to update the enabled state of the action if the
377     * collection of selected primitives changes. This method is called with the
378     * new selection.
379     *
380     * @param selection the collection of selected primitives; may be empty, but not null
381     *
382     * @see #updateEnabledState()
383     * @see #initEnabledState()
384     * @see #listenToSelectionChange()
385     */
386    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
387    }
388
389    /**
390     * Updates enabled state according to primitives currently selected in edit data set, if any.
391     * Can be called in {@link #updateEnabledState()} implementations.
392     * @see #updateEnabledStateOnCurrentSelection(boolean)
393     * @since 10409
394     */
395    protected final void updateEnabledStateOnCurrentSelection() {
396        updateEnabledStateOnCurrentSelection(false);
397    }
398
399    /**
400     * Updates enabled state according to primitives currently selected in active data set, if any.
401     * Can be called in {@link #updateEnabledState()} implementations.
402     * @param allowReadOnly if {@code true}, read-only data sets are considered
403     * @since 13434
404     */
405    protected final void updateEnabledStateOnCurrentSelection(boolean allowReadOnly) {
406        DataSet ds = getLayerManager().getActiveDataSet();
407        if (ds != null && (allowReadOnly || !ds.isLocked())) {
408            updateEnabledState(ds.getSelected());
409        } else {
410            setEnabled(false);
411        }
412    }
413
414    /**
415     * Updates enabled state according to selected primitives, if any.
416     * Enables action if the collection is not empty and references primitives in a modifiable data layer.
417     * Can be called in {@link #updateEnabledState(Collection)} implementations.
418     * @param selection the collection of selected primitives
419     * @since 13434
420     */
421    protected final void updateEnabledStateOnModifiableSelection(Collection<? extends OsmPrimitive> selection) {
422        setEnabled(OsmUtils.isOsmCollectionEditable(selection));
423    }
424
425    /**
426     * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed.
427     */
428    protected class LayerChangeAdapter implements LayerChangeListener {
429        @Override
430        public void layerAdded(LayerAddEvent e) {
431            updateEnabledState();
432        }
433
434        @Override
435        public void layerRemoving(LayerRemoveEvent e) {
436            updateEnabledState();
437        }
438
439        @Override
440        public void layerOrderChanged(LayerOrderChangeEvent e) {
441            updateEnabledState();
442        }
443
444        @Override
445        public String toString() {
446            return "LayerChangeAdapter [" + JosmAction.this + ']';
447        }
448    }
449
450    /**
451     * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed.
452     */
453    protected class ActiveLayerChangeAdapter implements ActiveLayerChangeListener {
454        @Override
455        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
456            updateEnabledState();
457        }
458
459        @Override
460        public String toString() {
461            return "ActiveLayerChangeAdapter [" + JosmAction.this + ']';
462        }
463    }
464
465    /**
466     * Adapter for selection change events. Runs updateEnabledState() whenever the selection changed.
467     */
468    protected class SelectionChangeAdapter implements DataSelectionListener {
469        @Override
470        public void selectionChanged(SelectionChangeEvent event) {
471            updateEnabledState(event.getSelection());
472        }
473
474        @Override
475        public String toString() {
476            return "SelectionChangeAdapter [" + JosmAction.this + ']';
477        }
478    }
479
480    /**
481     * Check whether user is about to operate on data outside of the download area.
482     * Request confirmation if he is.
483     *
484     * @param operation the operation name which is used for setting some preferences
485     * @param dialogTitle the title of the dialog being displayed
486     * @param outsideDialogMessage the message text to be displayed when data is outside of the download area
487     * @param incompleteDialogMessage the message text to be displayed when data is incomplete
488     * @param primitives the primitives to operate on
489     * @param ignore {@code null} or a primitive to be ignored
490     * @return true, if operating on outlying primitives is OK; false, otherwise
491     * @since 12749 (moved from Command)
492     */
493    public static boolean checkAndConfirmOutlyingOperation(String operation,
494            String dialogTitle, String outsideDialogMessage, String incompleteDialogMessage,
495            Collection<? extends OsmPrimitive> primitives,
496            Collection<? extends OsmPrimitive> ignore) {
497        int checkRes = Command.checkOutlyingOrIncompleteOperation(primitives, ignore);
498        if ((checkRes & Command.IS_OUTSIDE) != 0) {
499            JPanel msg = new JPanel(new GridBagLayout());
500            msg.add(new JMultilineLabel("<html>" + outsideDialogMessage + "</html>"));
501            boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
502                    operation + "_outside_nodes",
503                    MainApplication.getMainFrame(),
504                    msg,
505                    dialogTitle,
506                    JOptionPane.YES_NO_OPTION,
507                    JOptionPane.QUESTION_MESSAGE,
508                    JOptionPane.YES_OPTION);
509            if (!answer)
510                return false;
511        }
512        if ((checkRes & Command.IS_INCOMPLETE) != 0) {
513            JPanel msg = new JPanel(new GridBagLayout());
514            msg.add(new JMultilineLabel("<html>" + incompleteDialogMessage + "</html>"));
515            boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
516                    operation + "_incomplete",
517                    MainApplication.getMainFrame(),
518                    msg,
519                    dialogTitle,
520                    JOptionPane.YES_NO_OPTION,
521                    JOptionPane.QUESTION_MESSAGE,
522                    JOptionPane.YES_OPTION);
523            if (!answer)
524                return false;
525        }
526        return true;
527    }
528}