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