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.MapView; 019import org.openstreetmap.josm.gui.MapView.LayerChangeListener; 020import org.openstreetmap.josm.gui.layer.Layer; 021import org.openstreetmap.josm.gui.layer.OsmDataLayer; 022import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor; 023import org.openstreetmap.josm.gui.util.GuiHelper; 024import org.openstreetmap.josm.tools.Destroyable; 025import org.openstreetmap.josm.tools.ImageProvider; 026import org.openstreetmap.josm.tools.Shortcut; 027 028/** 029 * Base class helper for all Actions in JOSM. Just to make the life easier. 030 * 031 * A JosmAction is a {@link LayerChangeListener} and a {@link SelectionChangedListener}. Upon 032 * a layer change event or a selection change event it invokes {@link #updateEnabledState()}. 033 * Subclasses can override {@link #updateEnabledState()} in order to update the {@link #isEnabled()}-state 034 * of a JosmAction depending on the {@link #getCurrentDataSet()} and the current layers 035 * (see also {@link #getEditLayer()}). 036 * 037 * destroy() from interface Destroyable is called e.g. for MapModes, when the last layer has 038 * been removed and so the mapframe will be destroyed. For other JosmActions, destroy() may never 039 * be called (currently). 040 * 041 * @author imi 042 */ 043public abstract class JosmAction extends AbstractAction implements Destroyable { 044 045 protected transient Shortcut sc; 046 private transient LayerChangeAdapter layerChangeAdapter; 047 private transient SelectionChangeAdapter selectionChangeAdapter; 048 049 /** 050 * Returns the shortcut for this action. 051 * @return the shortcut for this action, or "No shortcut" if none is defined 052 */ 053 public Shortcut getShortcut() { 054 if (sc == null) { 055 sc = Shortcut.registerShortcut("core:none", tr("No Shortcut"), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE); 056 // as this shortcut is shared by all action that don't want to have a shortcut, 057 // we shouldn't allow the user to change it... 058 // this is handled by special name "core:none" 059 } 060 return sc; 061 } 062 063 /** 064 * Constructs a {@code JosmAction}. 065 * 066 * @param name the action's text as displayed on the menu (if it is added to a menu) 067 * @param icon the icon to use 068 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note 069 * that html is not supported for menu actions on some platforms. 070 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always 071 * do want a shortcut, remember you can always register it with group=none, so you 072 * won't be assigned a shortcut unless the user configures one. If you pass null here, 073 * the user CANNOT configure a shortcut for your action. 074 * @param registerInToolbar register this action for the toolbar preferences? 075 * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null 076 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters 077 */ 078 public JosmAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut, boolean registerInToolbar, 079 String toolbarId, boolean installAdapters) { 080 super(name); 081 if (icon != null) 082 icon.getResource().getImageIcon(this); 083 setHelpId(); 084 sc = shortcut; 085 if (sc != null) { 086 Main.registerActionShortcut(this, sc); 087 } 088 setTooltip(tooltip); 089 if (getValue("toolbar") == null) { 090 putValue("toolbar", toolbarId); 091 } 092 if (registerInToolbar && Main.toolbar != null) { 093 Main.toolbar.register(this); 094 } 095 if (installAdapters) { 096 installAdapters(); 097 } 098 } 099 100 /** 101 * The new super for all actions. 102 * 103 * Use this super constructor to setup your action. 104 * 105 * @param name the action's text as displayed on the menu (if it is added to a menu) 106 * @param iconName the filename of the icon to use 107 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note 108 * that html is not supported for menu actions on some platforms. 109 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always 110 * do want a shortcut, remember you can always register it with group=none, so you 111 * won't be assigned a shortcut unless the user configures one. If you pass null here, 112 * the user CANNOT configure a shortcut for your action. 113 * @param registerInToolbar register this action for the toolbar preferences? 114 * @param toolbarId identifier for the toolbar preferences. The iconName is used, if this parameter is null 115 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters 116 */ 117 public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar, 118 String toolbarId, boolean installAdapters) { 119 this(name, iconName == null ? null : new ImageProvider(iconName), tooltip, shortcut, registerInToolbar, 120 toolbarId == null ? iconName : toolbarId, installAdapters); 121 } 122 123 /** 124 * Constructs a new {@code JosmAction}. 125 * 126 * Use this super constructor to setup your action. 127 * 128 * @param name the action's text as displayed on the menu (if it is added to a menu) 129 * @param iconName the filename of the icon to use 130 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note 131 * that html is not supported for menu actions on some platforms. 132 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always 133 * do want a shortcut, remember you can always register it with group=none, so you 134 * won't be assigned a shortcut unless the user configures one. If you pass null here, 135 * the user CANNOT configure a shortcut for your action. 136 * @param registerInToolbar register this action for the toolbar preferences? 137 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters 138 */ 139 public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar, boolean installAdapters) { 140 this(name, iconName, tooltip, shortcut, registerInToolbar, null, installAdapters); 141 } 142 143 /** 144 * Constructs a new {@code JosmAction}. 145 * 146 * Use this super constructor to setup your action. 147 * 148 * @param name the action's text as displayed on the menu (if it is added to a menu) 149 * @param iconName the filename of the icon to use 150 * @param tooltip a longer description of the action that will be displayed in the tooltip. Please note 151 * that html is not supported for menu actions on some platforms. 152 * @param shortcut a ready-created shortcut object or null if you don't want a shortcut. But you always 153 * do want a shortcut, remember you can always register it with group=none, so you 154 * won't be assigned a shortcut unless the user configures one. If you pass null here, 155 * the user CANNOT configure a shortcut for your action. 156 * @param registerInToolbar register this action for the toolbar preferences? 157 */ 158 public JosmAction(String name, String iconName, String tooltip, Shortcut shortcut, boolean registerInToolbar) { 159 this(name, iconName, tooltip, shortcut, registerInToolbar, null, true); 160 } 161 162 /** 163 * Constructs a new {@code JosmAction}. 164 */ 165 public JosmAction() { 166 this(true); 167 } 168 169 /** 170 * Constructs a new {@code JosmAction}. 171 * 172 * @param installAdapters false, if you don't want to install layer changed and selection changed adapters 173 */ 174 public JosmAction(boolean installAdapters) { 175 setHelpId(); 176 if (installAdapters) { 177 installAdapters(); 178 } 179 } 180 181 @Override 182 public void destroy() { 183 if (sc != null) { 184 Main.unregisterActionShortcut(this); 185 } 186 MapView.removeLayerChangeListener(layerChangeAdapter); 187 DataSet.removeSelectionListener(selectionChangeAdapter); 188 } 189 190 private void setHelpId() { 191 String helpId = "Action/"+getClass().getName().substring(getClass().getName().lastIndexOf('.')+1); 192 if (helpId.endsWith("Action")) { 193 helpId = helpId.substring(0, helpId.length()-6); 194 } 195 putValue("help", helpId); 196 } 197 198 /** 199 * Sets the tooltip text of this action. 200 * @param tooltip The text to display in tooltip. Can be {@code null} 201 */ 202 public final void setTooltip(String tooltip) { 203 if (tooltip != null) { 204 putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc)); 205 } 206 } 207 208 /** 209 * Replies the current edit layer 210 * 211 * @return the current edit layer. null, if no edit layer exists 212 */ 213 public static OsmDataLayer getEditLayer() { 214 return Main.main != null ? Main.main.getEditLayer() : null; 215 } 216 217 /** 218 * Replies the current dataset. 219 * 220 * @return the current dataset. null, if no current dataset exists 221 */ 222 public static DataSet getCurrentDataSet() { 223 return Main.main != null ? Main.main.getCurrentDataSet() : null; 224 } 225 226 protected void installAdapters() { 227 // make this action listen to layer change and selection change events 228 // 229 layerChangeAdapter = new LayerChangeAdapter(); 230 selectionChangeAdapter = new SelectionChangeAdapter(); 231 MapView.addLayerChangeListener(layerChangeAdapter); 232 DataSet.addSelectionListener(selectionChangeAdapter); 233 initEnabledState(); 234 } 235 236 protected static void waitFuture(final Future<?> future, final PleaseWaitProgressMonitor monitor) { 237 Main.worker.submit( 238 new Runnable() { 239 @Override 240 public void run() { 241 try { 242 future.get(); 243 } catch (InterruptedException | ExecutionException | CancellationException e) { 244 Main.error(e); 245 return; 246 } 247 monitor.close(); 248 } 249 } 250 ); 251 } 252 253 /** 254 * Override in subclasses to init the enabled state of an action when it is 255 * created. Default behaviour is to call {@link #updateEnabledState()} 256 * 257 * @see #updateEnabledState() 258 * @see #updateEnabledState(Collection) 259 */ 260 protected void initEnabledState() { 261 updateEnabledState(); 262 } 263 264 /** 265 * Override in subclasses to update the enabled state of the action when 266 * something in the JOSM state changes, i.e. when a layer is removed or added. 267 * 268 * See {@link #updateEnabledState(Collection)} to respond to changes in the collection 269 * of selected primitives. 270 * 271 * Default behavior is empty. 272 * 273 * @see #updateEnabledState(Collection) 274 * @see #initEnabledState() 275 */ 276 protected void updateEnabledState() { 277 } 278 279 /** 280 * Override in subclasses to update the enabled state of the action if the 281 * collection of selected primitives changes. This method is called with the 282 * new selection. 283 * 284 * @param selection the collection of selected primitives; may be empty, but not null 285 * 286 * @see #updateEnabledState() 287 * @see #initEnabledState() 288 */ 289 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 290 } 291 292 /** 293 * Adapter for layer change events 294 * 295 */ 296 protected class LayerChangeAdapter implements MapView.LayerChangeListener { 297 private void updateEnabledStateInEDT() { 298 GuiHelper.runInEDT(new Runnable() { 299 @Override public void run() { 300 updateEnabledState(); 301 } 302 }); 303 } 304 305 @Override 306 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 307 updateEnabledStateInEDT(); 308 } 309 310 @Override 311 public void layerAdded(Layer newLayer) { 312 updateEnabledStateInEDT(); 313 } 314 315 @Override 316 public void layerRemoved(Layer oldLayer) { 317 updateEnabledStateInEDT(); 318 } 319 } 320 321 /** 322 * Adapter for selection change events 323 */ 324 protected class SelectionChangeAdapter implements SelectionChangedListener { 325 @Override 326 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 327 updateEnabledState(newSelection); 328 } 329 } 330}