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