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