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