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