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