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