001 // License: GPL. Copyright 2007 by Immanuel Scholz and others 002 package org.openstreetmap.josm.actions.mapmode; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 006 import java.awt.AWTEvent; 007 import java.awt.Cursor; 008 import java.awt.Toolkit; 009 import java.awt.event.AWTEventListener; 010 import java.awt.event.ActionEvent; 011 import java.awt.event.InputEvent; 012 import java.awt.event.KeyEvent; 013 import java.awt.event.MouseEvent; 014 import java.util.Collections; 015 import java.util.HashSet; 016 import java.util.Set; 017 018 import org.openstreetmap.josm.Main; 019 import org.openstreetmap.josm.command.Command; 020 import org.openstreetmap.josm.command.DeleteCommand; 021 import org.openstreetmap.josm.data.osm.DataSet; 022 import org.openstreetmap.josm.data.osm.Node; 023 import org.openstreetmap.josm.data.osm.OsmPrimitive; 024 import org.openstreetmap.josm.data.osm.Relation; 025 import org.openstreetmap.josm.data.osm.WaySegment; 026 import org.openstreetmap.josm.gui.MapFrame; 027 import org.openstreetmap.josm.gui.dialogs.relation.RelationDialogManager; 028 import org.openstreetmap.josm.gui.layer.Layer; 029 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 030 import org.openstreetmap.josm.tools.CheckParameterUtil; 031 import org.openstreetmap.josm.tools.ImageProvider; 032 import org.openstreetmap.josm.tools.Shortcut; 033 034 /** 035 * A map mode that enables the user to delete nodes and other objects. 036 * 037 * The user can click on an object, which gets deleted if possible. When Ctrl is 038 * pressed when releasing the button, the objects and all its references are 039 * deleted. 040 * 041 * If the user did not press Ctrl and the object has any references, the user 042 * is informed and nothing is deleted. 043 * 044 * If the user enters the mapmode and any object is selected, all selected 045 * objects are deleted, if possible. 046 * 047 * @author imi 048 */ 049 public class DeleteAction extends MapMode implements AWTEventListener { 050 // Cache previous mouse event (needed when only the modifier keys are 051 // pressed but the mouse isn't moved) 052 private MouseEvent oldEvent = null; 053 054 /** 055 * elements that have been highlighted in the previous iteration. Used 056 * to remove the highlight from them again as otherwise the whole data 057 * set would have to be checked. 058 */ 059 private Set<OsmPrimitive> oldHighlights = new HashSet<OsmPrimitive>(); 060 private WaySegment oldHighlightedWaySegment = null; 061 062 private boolean drawTargetHighlight; 063 064 private enum DeleteMode { 065 none("delete"), 066 segment("delete_segment"), 067 node("delete_node"), 068 node_with_references("delete_node"), 069 way("delete_way_only"), 070 way_with_references("delete_way_normal"), 071 way_with_nodes("delete_way_node_only"); 072 073 private final Cursor c; 074 075 private DeleteMode(String cursorName) { 076 c = ImageProvider.getCursor("normal", cursorName); 077 } 078 079 public Cursor cursor() { 080 return c; 081 } 082 } 083 084 private static class DeleteParameters { 085 DeleteMode mode; 086 Node nearestNode; 087 WaySegment nearestSegment; 088 } 089 090 /** 091 * Construct a new DeleteAction. Mnemonic is the delete - key. 092 * @param mapFrame The frame this action belongs to. 093 */ 094 public DeleteAction(MapFrame mapFrame) { 095 super(tr("Delete Mode"), 096 "delete", 097 tr("Delete nodes or ways."), 098 Shortcut.registerShortcut("mapmode:delete", tr("Mode: {0}",tr("Delete")), 099 KeyEvent.VK_DELETE, Shortcut.CTRL), 100 mapFrame, 101 ImageProvider.getCursor("normal", "delete")); 102 } 103 104 @Override public void enterMode() { 105 super.enterMode(); 106 if (!isEnabled()) 107 return; 108 109 drawTargetHighlight = Main.pref.getBoolean("draw.target-highlight", true); 110 111 Main.map.mapView.addMouseListener(this); 112 Main.map.mapView.addMouseMotionListener(this); 113 // This is required to update the cursors when ctrl/shift/alt is pressed 114 try { 115 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK); 116 } catch (SecurityException ex) { 117 System.out.println(ex); 118 } 119 } 120 121 @Override public void exitMode() { 122 super.exitMode(); 123 Main.map.mapView.removeMouseListener(this); 124 Main.map.mapView.removeMouseMotionListener(this); 125 try { 126 Toolkit.getDefaultToolkit().removeAWTEventListener(this); 127 } catch (SecurityException ex) { 128 System.out.println(ex); 129 } 130 removeHighlighting(); 131 } 132 133 @Override public void actionPerformed(ActionEvent e) { 134 super.actionPerformed(e); 135 doActionPerformed(e); 136 } 137 138 static public void doActionPerformed(ActionEvent e) { 139 if(!Main.map.mapView.isActiveLayerDrawable()) 140 return; 141 boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0; 142 boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0; 143 144 Command c; 145 if (ctrl) { 146 c = DeleteCommand.deleteWithReferences(getEditLayer(),getCurrentDataSet().getSelected()); 147 } else { 148 c = DeleteCommand.delete(getEditLayer(),getCurrentDataSet().getSelected(), !alt /* also delete nodes in way */); 149 } 150 // if c is null, an error occurred or the user aborted. Don't do anything in that case. 151 if (c != null) { 152 Main.main.undoRedo.add(c); 153 getCurrentDataSet().setSelected(); 154 Main.map.repaint(); 155 } 156 } 157 158 @Override public void mouseDragged(MouseEvent e) { 159 mouseMoved(e); 160 } 161 162 /** 163 * Listen to mouse move to be able to update the cursor (and highlights) 164 * @param e The mouse event that has been captured 165 */ 166 @Override public void mouseMoved(MouseEvent e) { 167 oldEvent = e; 168 giveUserFeedback(e); 169 } 170 171 /** 172 * removes any highlighting that may have been set beforehand. 173 */ 174 private void removeHighlighting() { 175 for(OsmPrimitive prim : oldHighlights) { 176 prim.setHighlighted(false); 177 } 178 oldHighlights = new HashSet<OsmPrimitive>(); 179 DataSet ds = getCurrentDataSet(); 180 if(ds != null) { 181 ds.clearHighlightedWaySegments(); 182 } 183 } 184 185 /** 186 * handles everything related to highlighting primitives and way 187 * segments for the given pointer position (via MouseEvent) and 188 * modifiers. 189 * @param e 190 * @param modifiers 191 */ 192 private void addHighlighting(MouseEvent e, int modifiers) { 193 if(!drawTargetHighlight) 194 return; 195 196 Set<OsmPrimitive> newHighlights = new HashSet<OsmPrimitive>(); 197 DeleteParameters parameters = getDeleteParameters(e, modifiers); 198 199 if(parameters.mode == DeleteMode.segment) { 200 // deleting segments is the only action not working on OsmPrimitives 201 // so we have to handle them separately. 202 repaintIfRequired(newHighlights, parameters.nearestSegment); 203 } else { 204 // don't call buildDeleteCommands for DeleteMode.segment because it doesn't support 205 // silent operation and SplitWayAction will show dialogs. A lot. 206 Command delCmd = buildDeleteCommands(e, modifiers, true); 207 if(delCmd != null) { 208 // all other cases delete OsmPrimitives directly, so we can 209 // safely do the following 210 for(OsmPrimitive osm : delCmd.getParticipatingPrimitives()) { 211 newHighlights.add(osm); 212 } 213 } 214 repaintIfRequired(newHighlights, null); 215 } 216 } 217 218 private void repaintIfRequired(Set<OsmPrimitive> newHighlights, WaySegment newHighlightedWaySegment) { 219 boolean needsRepaint = false; 220 DataSet ds = getCurrentDataSet(); 221 222 if(newHighlightedWaySegment == null && oldHighlightedWaySegment != null) { 223 if(ds != null) { 224 ds.clearHighlightedWaySegments(); 225 needsRepaint = true; 226 } 227 oldHighlightedWaySegment = null; 228 } else if(newHighlightedWaySegment != null && !newHighlightedWaySegment.equals(oldHighlightedWaySegment)) { 229 if(ds != null) { 230 ds.setHighlightedWaySegments(Collections.singleton(newHighlightedWaySegment)); 231 needsRepaint = true; 232 } 233 oldHighlightedWaySegment = newHighlightedWaySegment; 234 } 235 236 for(OsmPrimitive x : newHighlights) { 237 if(oldHighlights.contains(x)) { 238 continue; 239 } 240 needsRepaint = true; 241 x.setHighlighted(true); 242 } 243 oldHighlights.removeAll(newHighlights); 244 for(OsmPrimitive x : oldHighlights) { 245 x.setHighlighted(false); 246 needsRepaint = true; 247 } 248 oldHighlights = newHighlights; 249 if(needsRepaint) { 250 Main.map.mapView.repaint(); 251 } 252 } 253 254 /** 255 * This function handles all work related to updating the cursor and 256 * highlights 257 * 258 * @param e 259 * @param modifiers 260 */ 261 private void updateCursor(MouseEvent e, int modifiers) { 262 if (!Main.isDisplayingMapView()) 263 return; 264 if(!Main.map.mapView.isActiveLayerVisible() || e == null) 265 return; 266 267 DeleteParameters parameters = getDeleteParameters(e, modifiers); 268 Main.map.mapView.setNewCursor(parameters.mode.cursor(), this); 269 } 270 /** 271 * Gives the user feedback for the action he/she is about to do. Currently 272 * calls the cursor and target highlighting routines. Allows for modifiers 273 * not taken from the given mouse event. 274 * 275 * Normally the mouse event also contains the modifiers. However, when the 276 * mouse is not moved and only modifier keys are pressed, no mouse event 277 * occurs. We can use AWTEvent to catch those but still lack a proper 278 * mouseevent. Instead we copy the previous event and only update the 279 * modifiers. 280 */ 281 private void giveUserFeedback(MouseEvent e, int modifiers) { 282 updateCursor(e, modifiers); 283 addHighlighting(e, modifiers); 284 } 285 286 /** 287 * Gives the user feedback for the action he/she is about to do. Currently 288 * calls the cursor and target highlighting routines. Extracts modifiers 289 * from mouse event. 290 */ 291 private void giveUserFeedback(MouseEvent e) { 292 giveUserFeedback(e, e.getModifiers()); 293 } 294 295 /** 296 * If user clicked with the left button, delete the nearest object. 297 * position. 298 */ 299 @Override public void mouseReleased(MouseEvent e) { 300 if (e.getButton() != MouseEvent.BUTTON1) 301 return; 302 if(!Main.map.mapView.isActiveLayerVisible()) 303 return; 304 305 // request focus in order to enable the expected keyboard shortcuts 306 // 307 Main.map.mapView.requestFocus(); 308 309 Command c = buildDeleteCommands(e, e.getModifiers(), false); 310 if (c != null) { 311 Main.main.undoRedo.add(c); 312 } 313 314 getCurrentDataSet().setSelected(); 315 giveUserFeedback(e); 316 } 317 318 @Override public String getModeHelpText() { 319 return tr("Click to delete. Shift: delete way segment. Alt: do not delete unused nodes when deleting a way. Ctrl: delete referring objects."); 320 } 321 322 @Override public boolean layerIsSupported(Layer l) { 323 return l instanceof OsmDataLayer; 324 } 325 326 @Override 327 protected void updateEnabledState() { 328 setEnabled(Main.isDisplayingMapView() && Main.map.mapView.isActiveLayerDrawable()); 329 } 330 331 /** 332 * Deletes the relation in the context of the given layer. 333 * 334 * @param layer the layer in whose context the relation is deleted. Must not be null. 335 * @param toDelete the relation to be deleted. Must not be null. 336 * @exception IllegalArgumentException thrown if layer is null 337 * @exception IllegalArgumentException thrown if toDelete is nul 338 */ 339 public static void deleteRelation(OsmDataLayer layer, Relation toDelete) { 340 CheckParameterUtil.ensureParameterNotNull(layer, "layer"); 341 CheckParameterUtil.ensureParameterNotNull(toDelete, "toDelete"); 342 343 Command cmd = DeleteCommand.delete(layer, Collections.singleton(toDelete)); 344 if (cmd != null) { 345 // cmd can be null if the user cancels dialogs DialogCommand displays 346 Main.main.undoRedo.add(cmd); 347 if (getCurrentDataSet().getSelectedRelations().contains(toDelete)) { 348 getCurrentDataSet().toggleSelected(toDelete); 349 } 350 RelationDialogManager.getRelationDialogManager().close(layer, toDelete); 351 } 352 } 353 354 private DeleteParameters getDeleteParameters(MouseEvent e, int modifiers) { 355 updateKeyModifiers(modifiers); 356 357 DeleteParameters result = new DeleteParameters(); 358 359 result.nearestNode = Main.map.mapView.getNearestNode(e.getPoint(), OsmPrimitive.isSelectablePredicate); 360 if (result.nearestNode == null) { 361 result.nearestSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate); 362 if (result.nearestSegment != null) { 363 if (shift) { 364 result.mode = DeleteMode.segment; 365 } else if (ctrl) { 366 result.mode = DeleteMode.way_with_references; 367 } else { 368 result.mode = alt?DeleteMode.way:DeleteMode.way_with_nodes; 369 } 370 } else { 371 result.mode = DeleteMode.none; 372 } 373 } else if (ctrl) { 374 result.mode = DeleteMode.node_with_references; 375 } else { 376 result.mode = DeleteMode.node; 377 } 378 379 return result; 380 } 381 382 /** 383 * This function takes any mouse event argument and builds the list of elements 384 * that should be deleted but does not actually delete them. 385 * @param e MouseEvent from which modifiers and position are taken 386 * @param modifiers For explanation: @see updateCursor 387 * @param silet Set to true if the user should not be bugged with additional 388 * dialogs 389 * @return 390 */ 391 private Command buildDeleteCommands(MouseEvent e, int modifiers, boolean silent) { 392 DeleteParameters parameters = getDeleteParameters(e, modifiers); 393 switch (parameters.mode) { 394 case node: 395 return DeleteCommand.delete(getEditLayer(),Collections.singleton(parameters.nearestNode), false, silent); 396 case node_with_references: 397 return DeleteCommand.deleteWithReferences(getEditLayer(),Collections.singleton(parameters.nearestNode), silent); 398 case segment: 399 return DeleteCommand.deleteWaySegment(getEditLayer(), parameters.nearestSegment); 400 case way: 401 return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), false, silent); 402 case way_with_nodes: 403 return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true, silent); 404 case way_with_references: 405 return DeleteCommand.deleteWithReferences(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true); 406 default: 407 return null; 408 } 409 } 410 411 /** 412 * This is required to update the cursors when ctrl/shift/alt is pressed 413 */ 414 public void eventDispatched(AWTEvent e) { 415 if(oldEvent == null) 416 return; 417 // We don't have a mouse event, so we pass the old mouse event but the 418 // new modifiers. 419 giveUserFeedback(oldEvent, ((InputEvent) e).getModifiers()); 420 } 421 }