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