// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.actions; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.GridBagLayout; import java.awt.event.KeyEvent; import java.util.Collection; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import javax.swing.AbstractAction; import javax.swing.JOptionPane; import javax.swing.JPanel; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.data.SelectionChangedListener; import org.openstreetmap.josm.data.osm.DataSet; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.OsmUtils; import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode; import org.openstreetmap.josm.data.osm.event.SelectionEventManager; import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; import org.openstreetmap.josm.gui.MainApplication; import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; import org.openstreetmap.josm.gui.layer.MainLayerManager; import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor; import org.openstreetmap.josm.gui.widgets.JMultilineLabel; import org.openstreetmap.josm.tools.Destroyable; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.ImageResource; import org.openstreetmap.josm.tools.Logging; import org.openstreetmap.josm.tools.Shortcut; /** * Base class helper for all Actions in JOSM. Just to make the life easier. * * 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 * layer/selection listeners that call {@link #updateEnabledState()} whenever the global context is changed. * * A JosmAction can register a {@link LayerChangeListener} and a {@link SelectionChangedListener}. Upon * a layer change event or a selection change event it invokes {@link #updateEnabledState()}. * Subclasses can override {@link #updateEnabledState()} in order to update the {@link #isEnabled()}-state * of a JosmAction depending on the {@link #getLayerManager()} state. * * destroy() from interface Destroyable is called e.g. for MapModes, when the last layer has * been removed and so the mapframe will be destroyed. For other JosmActions, destroy() may never * be called (currently). * * @author imi */ public abstract class JosmAction extends AbstractAction implements Destroyable { protected transient Shortcut sc; private transient LayerChangeAdapter layerChangeAdapter; private transient ActiveLayerChangeAdapter activeLayerChangeAdapter; private transient SelectionChangeAdapter selectionChangeAdapter; /** * Constructs a {@code JosmAction}. * * @param name the action's text as displayed on the menu (if it is added to a menu) * @param icon the icon to use * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note * that html is not supported for menu actions on some platforms. * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always * do want a shortcut, remember you can always register it with group=none, so you * won't be assigned a shortcut unless the user configures one. If you pass null here, * the user CANNOT configure a shortcut for your action. * @param registerInToolbar register this action for the toolbar preferences? * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null * @param installAdapters false, if you don't want to install layer changed and selection changed adapters */ public JosmAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut, boolean registerInToolbar, String toolbarId, boolean installAdapters) { super(name); if (icon != null) { ImageResource resource = icon.getResource(); if (resource != null) { resource.attachImageIcon(this, true); } } setHelpId(); sc = shortcut; if (sc != null && !sc.isAutomatic()) { MainApplication.registerActionShortcut(this, sc); } setTooltip(tooltip); if (getValue("toolbar") == null) { putValue("toolbar", toolbarId); } if (registerInToolbar && MainApplication.getToolbar() != null) { MainApplication.getToolbar().register(this); } if (installAdapters) { installAdapters(); } } /** * The new super for all actions. * * Use this super constructor to setup your action. * * @param name the action's text as displayed on the menu (if it is added to a menu) * @param iconName the filename of the icon to use * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note * that html is not supported for menu actions on some platforms. * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always * do want a shortcut, remember you can always register it with group=none, so you * won't be assigned a shortcut unless the user configures one. If you pass null here, * the user CANNOT configure a shortcut for your action. * @param registerInToolbar register this action for the toolbar preferences? * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null * @param installAdapters false, if you don't want to install layer changed and selection changed adapters */ public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar, String toolbarId, boolean installAdapters) { this(name, iconName == null ? null : new ImageProvider(iconName).setOptional(true), tooltip, shortcut, registerInToolbar, toolbarId == null ? iconName : toolbarId, installAdapters); } /** * Constructs a new {@code JosmAction}. * * Use this super constructor to setup your action. * * @param name the action's text as displayed on the menu (if it is added to a menu) * @param iconName the filename of the icon to use * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note * that html is not supported for menu actions on some platforms. * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always * do want a shortcut, remember you can always register it with group=none, so you * won't be assigned a shortcut unless the user configures one. If you pass null here, * the user CANNOT configure a shortcut for your action. * @param registerInToolbar register this action for the toolbar preferences? * @param installAdapters false, if you don't want to install layer changed and selection changed adapters */ public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar, boolean installAdapters) { this(name, iconName, tooltip, shortcut, registerInToolbar, null, installAdapters); } /** * Constructs a new {@code JosmAction}. * * Use this super constructor to setup your action. * * @param name the action's text as displayed on the menu (if it is added to a menu) * @param iconName the filename of the icon to use * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note * that html is not supported for menu actions on some platforms. * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always * do want a shortcut, remember you can always register it with group=none, so you * won't be assigned a shortcut unless the user configures one. If you pass null here, * the user CANNOT configure a shortcut for your action. * @param registerInToolbar register this action for the toolbar preferences? */ public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar) { this(name, iconName, tooltip, shortcut, registerInToolbar, null, true); } /** * Constructs a new {@code JosmAction}. */ public JosmAction() { this(true); } /** * Constructs a new {@code JosmAction}. * * @param installAdapters false, if you don't want to install layer changed and selection changed adapters */ public JosmAction(boolean installAdapters) { setHelpId(); if (installAdapters) { installAdapters(); } } /** * Installs the listeners to this action. *

* This should either never be called or only called in the constructor of this action. *

* All registered adapters should be removed in {@link #destroy()} */ protected void installAdapters() { // make this action listen to layer change and selection change events if (listenToLayerChange()) { layerChangeAdapter = new LayerChangeAdapter(); activeLayerChangeAdapter = new ActiveLayerChangeAdapter(); getLayerManager().addLayerChangeListener(layerChangeAdapter); getLayerManager().addActiveLayerChangeListener(activeLayerChangeAdapter); } if (listenToSelectionChange()) { selectionChangeAdapter = new SelectionChangeAdapter(); SelectionEventManager.getInstance() .addSelectionListener(selectionChangeAdapter, FireMode.IN_EDT_CONSOLIDATED); } initEnabledState(); } /** * Overwrite this if {@link #updateEnabledState()} should be called when the active / availabe layers change. Default is true. * @return true if a {@link LayerChangeListener} and a {@link ActiveLayerChangeListener} should be registered. * @since 10353 */ protected boolean listenToLayerChange() { return true; } /** * Overwrite this if {@link #updateEnabledState()} should be called when the selection changed. Default is true. * @return true if a {@link SelectionChangedListener} should be registered. * @since 10353 */ protected boolean listenToSelectionChange() { return true; } @Override public void destroy() { if (sc != null && !sc.isAutomatic()) { MainApplication.unregisterActionShortcut(this); } if (layerChangeAdapter != null) { getLayerManager().removeLayerChangeListener(layerChangeAdapter); getLayerManager().removeActiveLayerChangeListener(activeLayerChangeAdapter); } if (selectionChangeAdapter != null) { DataSet.removeSelectionListener(selectionChangeAdapter); } } private void setHelpId() { String helpId = "Action/"+getClass().getName().substring(getClass().getName().lastIndexOf('.')+1); if (helpId.endsWith("Action")) { helpId = helpId.substring(0, helpId.length()-6); } putValue("help", helpId); } /** * Returns the shortcut for this action. * @return the shortcut for this action, or "No shortcut" if none is defined */ public Shortcut getShortcut() { if (sc == null) { sc = Shortcut.registerShortcut("core:none", tr("No Shortcut"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE); // as this shortcut is shared by all action that don't want to have a shortcut, // we shouldn't allow the user to change it... // this is handled by special name "core:none" } return sc; } /** * Sets the tooltip text of this action. * @param tooltip The text to display in tooltip. Can be {@code null} */ public final void setTooltip(String tooltip) { if (tooltip != null) { putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc)); } } /** * Gets the layer manager used for this action. Defaults to the main layer manager but you can overwrite this. *

* The layer manager must be available when {@link #installAdapters()} is called and must not change. * * @return The layer manager. * @since 10353 */ public MainLayerManager getLayerManager() { return MainApplication.getLayerManager(); } protected static void waitFuture(final Future future, final PleaseWaitProgressMonitor monitor) { MainApplication.worker.submit(() -> { try { future.get(); } catch (InterruptedException | ExecutionException | CancellationException e) { Logging.error(e); return; } monitor.close(); }); } /** * Override in subclasses to init the enabled state of an action when it is * created. Default behaviour is to call {@link #updateEnabledState()} * * @see #updateEnabledState() * @see #updateEnabledState(Collection) */ protected void initEnabledState() { updateEnabledState(); } /** * Override in subclasses to update the enabled state of the action when * something in the JOSM state changes, i.e. when a layer is removed or added. * * See {@link #updateEnabledState(Collection)} to respond to changes in the collection * of selected primitives. * * Default behavior is empty. * * @see #updateEnabledState(Collection) * @see #initEnabledState() * @see #listenToLayerChange() */ protected void updateEnabledState() { } /** * Override in subclasses to update the enabled state of the action if the * collection of selected primitives changes. This method is called with the * new selection. * * @param selection the collection of selected primitives; may be empty, but not null * * @see #updateEnabledState() * @see #initEnabledState() * @see #listenToSelectionChange() */ protected void updateEnabledState(Collection selection) { } /** * Updates enabled state according to primitives currently selected in edit data set, if any. * Can be called in {@link #updateEnabledState()} implementations. * @see #updateEnabledStateOnCurrentSelection(boolean) * @since 10409 */ protected final void updateEnabledStateOnCurrentSelection() { updateEnabledStateOnCurrentSelection(false); } /** * Updates enabled state according to primitives currently selected in active data set, if any. * Can be called in {@link #updateEnabledState()} implementations. * @param allowReadOnly if {@code true}, read-only data sets are considered * @since 13434 */ protected final void updateEnabledStateOnCurrentSelection(boolean allowReadOnly) { DataSet ds = getLayerManager().getActiveDataSet(); if (ds != null && (allowReadOnly || !ds.isLocked())) { updateEnabledState(ds.getSelected()); } else { setEnabled(false); } } /** * Updates enabled state according to selected primitives, if any. * Enables action if the collection is not empty and references primitives in a modifiable data layer. * Can be called in {@link #updateEnabledState(Collection)} implementations. * @param selection the collection of selected primitives * @since 13434 */ protected final void updateEnabledStateOnModifiableSelection(Collection selection) { setEnabled(OsmUtils.isOsmCollectionEditable(selection)); } /** * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed. */ protected class LayerChangeAdapter implements LayerChangeListener { @Override public void layerAdded(LayerAddEvent e) { updateEnabledState(); } @Override public void layerRemoving(LayerRemoveEvent e) { updateEnabledState(); } @Override public void layerOrderChanged(LayerOrderChangeEvent e) { updateEnabledState(); } @Override public String toString() { return "LayerChangeAdapter [" + JosmAction.this + ']'; } } /** * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed. */ protected class ActiveLayerChangeAdapter implements ActiveLayerChangeListener { @Override public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { updateEnabledState(); } @Override public String toString() { return "ActiveLayerChangeAdapter [" + JosmAction.this + ']'; } } /** * Adapter for selection change events. Runs updateEnabledState() whenever the selection changed. */ protected class SelectionChangeAdapter implements SelectionChangedListener { @Override public void selectionChanged(Collection newSelection) { updateEnabledState(newSelection); } @Override public String toString() { return "SelectionChangeAdapter [" + JosmAction.this + ']'; } } /** * Check whether user is about to operate on data outside of the download area. * Request confirmation if he is. * * @param operation the operation name which is used for setting some preferences * @param dialogTitle the title of the dialog being displayed * @param outsideDialogMessage the message text to be displayed when data is outside of the download area * @param incompleteDialogMessage the message text to be displayed when data is incomplete * @param primitives the primitives to operate on * @param ignore {@code null} or a primitive to be ignored * @return true, if operating on outlying primitives is OK; false, otherwise * @since 12749 (moved from Command) */ public static boolean checkAndConfirmOutlyingOperation(String operation, String dialogTitle, String outsideDialogMessage, String incompleteDialogMessage, Collection primitives, Collection ignore) { int checkRes = Command.checkOutlyingOrIncompleteOperation(primitives, ignore); if ((checkRes & Command.IS_OUTSIDE) != 0) { JPanel msg = new JPanel(new GridBagLayout()); msg.add(new JMultilineLabel("" + outsideDialogMessage + "")); boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( operation + "_outside_nodes", Main.parent, msg, dialogTitle, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_OPTION); if (!answer) return false; } if ((checkRes & Command.IS_INCOMPLETE) != 0) { JPanel msg = new JPanel(new GridBagLayout()); msg.add(new JMultilineLabel("" + incompleteDialogMessage + "")); boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog( operation + "_incomplete", Main.parent, msg, dialogTitle, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_OPTION); if (!answer) return false; } return true; } }