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