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.event.KeyEvent; 007import java.util.Collection; 008import java.util.concurrent.CancellationException; 009import java.util.concurrent.ExecutionException; 010import java.util.concurrent.Future; 011 012import javax.swing.AbstractAction; 013 014import org.openstreetmap.josm.Main; 015import org.openstreetmap.josm.data.SelectionChangedListener; 016import org.openstreetmap.josm.data.osm.DataSet; 017import org.openstreetmap.josm.data.osm.OsmPrimitive; 018import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 019import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 020import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 021import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 022import org.openstreetmap.josm.gui.layer.MainLayerManager; 023import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 024import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 025import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor; 026import org.openstreetmap.josm.tools.Destroyable; 027import org.openstreetmap.josm.tools.ImageProvider; 028import org.openstreetmap.josm.tools.Shortcut; 029 030/** 031 * Base class helper for all Actions in JOSM. Just to make the life easier. 032 * 033 * 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 034 * layer/selection listeners that call {@link #updateEnabledState()} whenever the global context is changed. 035 * 036 * A JosmAction can register a {@link LayerChangeListener} and a {@link SelectionChangedListener}. Upon 037 * a layer change event or a selection change event it invokes {@link #updateEnabledState()}. 038 * Subclasses can override {@link #updateEnabledState()} in order to update the {@link #isEnabled()}-state 039 * of a JosmAction depending on the {@link #getLayerManager()} state. 040 * 041 * destroy() from interface Destroyable is called e.g. for MapModes, when the last layer has 042 * been removed and so the mapframe will be destroyed. For other JosmActions, destroy() may never 043 * be called (currently). 044 * 045 * @author imi 046 */ 047public abstract class JosmAction extends AbstractAction implements Destroyable { 048 049 protected transient Shortcut sc; 050 private transient LayerChangeAdapter layerChangeAdapter; 051 private transient ActiveLayerChangeAdapter activeLayerChangeAdapter; 052 private transient SelectionChangeAdapter selectionChangeAdapter; 053 054 /** 055 * Constructs a {@code JosmAction}. 056 * 057 * @param name the action's text as displayed on the menu (if it is added to a menu) 058 * @param icon the icon to use 059 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note 060 * that html is not supported for menu actions on some platforms. 061 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always 062 * do want a shortcut, remember you can always register it with group=none, so you 063 * won't be assigned a shortcut unless the user configures one. If you pass null here, 064 * the user CANNOT configure a shortcut for your action. 065 * @param registerInToolbar register this action for the toolbar preferences? 066 * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null 067 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters 068 */ 069 public JosmAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut, boolean registerInToolbar, 070 String toolbarId, boolean installAdapters) { 071 super(name); 072 if (icon != null) 073 icon.getResource().attachImageIcon(this, true); 074 setHelpId(); 075 sc = shortcut; 076 if (sc != null) { 077 Main.registerActionShortcut(this, sc); 078 } 079 setTooltip(tooltip); 080 if (getValue("toolbar") == null) { 081 putValue("toolbar", toolbarId); 082 } 083 if (registerInToolbar && Main.toolbar != null) { 084 Main.toolbar.register(this); 085 } 086 if (installAdapters) { 087 installAdapters(); 088 } 089 } 090 091 /** 092 * The new super for all actions. 093 * 094 * Use this super constructor to setup your action. 095 * 096 * @param name the action's text as displayed on the menu (if it is added to a menu) 097 * @param iconName the filename of the icon to use 098 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note 099 * that html is not supported for menu actions on some platforms. 100 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always 101 * do want a shortcut, remember you can always register it with group=none, so you 102 * won't be assigned a shortcut unless the user configures one. If you pass null here, 103 * the user CANNOT configure a shortcut for your action. 104 * @param registerInToolbar register this action for the toolbar preferences? 105 * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null 106 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters 107 */ 108 public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar, 109 String toolbarId, boolean installAdapters) { 110 this(name, iconName == null ? null : new ImageProvider(iconName), tooltip, shortcut, registerInToolbar, 111 toolbarId == null ? iconName : toolbarId, installAdapters); 112 } 113 114 /** 115 * Constructs a new {@code JosmAction}. 116 * 117 * Use this super constructor to setup your action. 118 * 119 * @param name the action's text as displayed on the menu (if it is added to a menu) 120 * @param iconName the filename of the icon to use 121 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note 122 * that html is not supported for menu actions on some platforms. 123 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always 124 * do want a shortcut, remember you can always register it with group=none, so you 125 * won't be assigned a shortcut unless the user configures one. If you pass null here, 126 * the user CANNOT configure a shortcut for your action. 127 * @param registerInToolbar register this action for the toolbar preferences? 128 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters 129 */ 130 public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar, boolean installAdapters) { 131 this(name, iconName, tooltip, shortcut, registerInToolbar, null, 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 */ 149 public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar) { 150 this(name, iconName, tooltip, shortcut, registerInToolbar, null, true); 151 } 152 153 /** 154 * Constructs a new {@code JosmAction}. 155 */ 156 public JosmAction() { 157 this(true); 158 } 159 160 /** 161 * Constructs a new {@code JosmAction}. 162 * 163 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters 164 */ 165 public JosmAction(boolean installAdapters) { 166 setHelpId(); 167 if (installAdapters) { 168 installAdapters(); 169 } 170 } 171 172 /** 173 * Installs the listeners to this action. 174 * <p> 175 * This should either never be called or only called in the constructor of this action. 176 * <p> 177 * All registered adapters should be removed in {@link #destroy()} 178 */ 179 protected void installAdapters() { 180 // make this action listen to layer change and selection change events 181 if (listenToLayerChange()) { 182 layerChangeAdapter = new LayerChangeAdapter(); 183 activeLayerChangeAdapter = new ActiveLayerChangeAdapter(); 184 getLayerManager().addLayerChangeListener(layerChangeAdapter); 185 getLayerManager().addActiveLayerChangeListener(activeLayerChangeAdapter); 186 } 187 if (listenToSelectionChange()) { 188 selectionChangeAdapter = new SelectionChangeAdapter(); 189 DataSet.addSelectionListener(selectionChangeAdapter); 190 } 191 initEnabledState(); 192 } 193 194 /** 195 * Overwrite this if {@link #updateEnabledState()} should be called when the active / availabe layers change. Default is true. 196 * @return <code>true</code> if a {@link LayerChangeListener} and a {@link ActiveLayerChangeListener} should be registered. 197 * @since 10353 198 */ 199 protected boolean listenToLayerChange() { 200 return true; 201 } 202 203 /** 204 * Overwrite this if {@link #updateEnabledState()} should be called when the selection changed. Default is true. 205 * @return <code>true</code> if a {@link SelectionChangedListener} should be registered. 206 * @since 10353 207 */ 208 protected boolean listenToSelectionChange() { 209 return true; 210 } 211 212 @Override 213 public void destroy() { 214 if (sc != null) { 215 Main.unregisterActionShortcut(this); 216 } 217 if (layerChangeAdapter != null) { 218 getLayerManager().removeLayerChangeListener(layerChangeAdapter); 219 getLayerManager().removeActiveLayerChangeListener(activeLayerChangeAdapter); 220 } 221 if (selectionChangeAdapter != null) { 222 DataSet.removeSelectionListener(selectionChangeAdapter); 223 } 224 } 225 226 private void setHelpId() { 227 String helpId = "Action/"+getClass().getName().substring(getClass().getName().lastIndexOf('.')+1); 228 if (helpId.endsWith("Action")) { 229 helpId = helpId.substring(0, helpId.length()-6); 230 } 231 putValue("help", helpId); 232 } 233 234 /** 235 * Returns the shortcut for this action. 236 * @return the shortcut for this action, or "No shortcut" if none is defined 237 */ 238 public Shortcut getShortcut() { 239 if (sc == null) { 240 sc = Shortcut.registerShortcut("core:none", tr("No Shortcut"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE); 241 // as this shortcut is shared by all action that don't want to have a shortcut, 242 // we shouldn't allow the user to change it... 243 // this is handled by special name "core:none" 244 } 245 return sc; 246 } 247 248 /** 249 * Sets the tooltip text of this action. 250 * @param tooltip The text to display in tooltip. Can be {@code null} 251 */ 252 public final void setTooltip(String tooltip) { 253 if (tooltip != null) { 254 putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc)); 255 } 256 } 257 258 /** 259 * Gets the layer manager used for this action. Defaults to the main layer manager but you can overwrite this. 260 * <p> 261 * The layer manager must be available when {@link #installAdapters()} is called and must not change. 262 * 263 * @return The layer manager. 264 * @since 10353 265 */ 266 public MainLayerManager getLayerManager() { 267 return Main.getLayerManager(); 268 } 269 270 protected static void waitFuture(final Future<?> future, final PleaseWaitProgressMonitor monitor) { 271 Main.worker.submit(() -> { 272 try { 273 future.get(); 274 } catch (InterruptedException | ExecutionException | CancellationException e) { 275 Main.error(e); 276 return; 277 } 278 monitor.close(); 279 }); 280 } 281 282 /** 283 * Override in subclasses to init the enabled state of an action when it is 284 * created. Default behaviour is to call {@link #updateEnabledState()} 285 * 286 * @see #updateEnabledState() 287 * @see #updateEnabledState(Collection) 288 */ 289 protected void initEnabledState() { 290 updateEnabledState(); 291 } 292 293 /** 294 * Override in subclasses to update the enabled state of the action when 295 * something in the JOSM state changes, i.e. when a layer is removed or added. 296 * 297 * See {@link #updateEnabledState(Collection)} to respond to changes in the collection 298 * of selected primitives. 299 * 300 * Default behavior is empty. 301 * 302 * @see #updateEnabledState(Collection) 303 * @see #initEnabledState() 304 * @see #listenToLayerChange() 305 */ 306 protected void updateEnabledState() { 307 } 308 309 /** 310 * Override in subclasses to update the enabled state of the action if the 311 * collection of selected primitives changes. This method is called with the 312 * new selection. 313 * 314 * @param selection the collection of selected primitives; may be empty, but not null 315 * 316 * @see #updateEnabledState() 317 * @see #initEnabledState() 318 * @see #listenToSelectionChange() 319 */ 320 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 321 } 322 323 /** 324 * Updates enabled state according to primitives currently selected in edit data set, if any. 325 * Can be called in {@link #updateEnabledState()} implementations. 326 * @since 10409 327 */ 328 protected final void updateEnabledStateOnCurrentSelection() { 329 DataSet ds = getLayerManager().getEditDataSet(); 330 if (ds == null) { 331 setEnabled(false); 332 } else { 333 updateEnabledState(ds.getSelected()); 334 } 335 } 336 337 /** 338 * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed. 339 */ 340 protected class LayerChangeAdapter implements LayerChangeListener { 341 @Override 342 public void layerAdded(LayerAddEvent e) { 343 updateEnabledState(); 344 } 345 346 @Override 347 public void layerRemoving(LayerRemoveEvent e) { 348 updateEnabledState(); 349 } 350 351 @Override 352 public void layerOrderChanged(LayerOrderChangeEvent e) { 353 updateEnabledState(); 354 } 355 356 @Override 357 public String toString() { 358 return "LayerChangeAdapter [" + JosmAction.this.toString() + ']'; 359 } 360 } 361 362 /** 363 * Adapter for layer change events. Runs updateEnabledState() whenever the active layer changed. 364 */ 365 protected class ActiveLayerChangeAdapter implements ActiveLayerChangeListener { 366 @Override 367 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 368 updateEnabledState(); 369 } 370 371 @Override 372 public String toString() { 373 return "ActiveLayerChangeAdapter [" + JosmAction.this.toString() + ']'; 374 } 375 } 376 377 /** 378 * Adapter for selection change events. Runs updateEnabledState() whenever the selection changed. 379 */ 380 protected class SelectionChangeAdapter implements SelectionChangedListener { 381 @Override 382 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 383 updateEnabledState(newSelection); 384 } 385 386 @Override 387 public String toString() { 388 return "SelectionChangeAdapter [" + JosmAction.this.toString() + ']'; 389 } 390 } 391}