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