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