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 * @param e mouse event 259 * @param modifiers mouse modifiers 260 */ 261 private void giveUserFeedback(MouseEvent e, int modifiers) { 262 updateCursor(e, modifiers); 263 addHighlighting(e, modifiers); 264 } 265 266 /** 267 * Gives the user feedback for the action he/she is about to do. Currently 268 * calls the cursor and target highlighting routines. Extracts modifiers 269 * from mouse event. 270 * @param e mouse event 271 */ 272 private void giveUserFeedback(MouseEvent e) { 273 giveUserFeedback(e, e.getModifiers()); 274 } 275 276 /** 277 * If user clicked with the left button, delete the nearest object. 278 */ 279 @Override 280 public void mouseReleased(MouseEvent e) { 281 if (e.getButton() != MouseEvent.BUTTON1) 282 return; 283 if (!Main.map.mapView.isActiveLayerVisible()) 284 return; 285 286 // request focus in order to enable the expected keyboard shortcuts 287 // 288 Main.map.mapView.requestFocus(); 289 290 Command c = buildDeleteCommands(e, e.getModifiers(), false); 291 if (c != null) { 292 Main.main.undoRedo.add(c); 293 } 294 295 getCurrentDataSet().setSelected(); 296 giveUserFeedback(e); 297 } 298 299 @Override 300 public String getModeHelpText() { 301 // CHECKSTYLE.OFF: LineLength 302 return tr("Click to delete. Shift: delete way segment. Alt: do not delete unused nodes when deleting a way. Ctrl: delete referring objects."); 303 // CHECKSTYLE.ON: LineLength 304 } 305 306 @Override 307 public boolean layerIsSupported(Layer l) { 308 return l instanceof OsmDataLayer; 309 } 310 311 @Override 312 protected void updateEnabledState() { 313 setEnabled(Main.isDisplayingMapView() && Main.map.mapView.isActiveLayerDrawable()); 314 } 315 316 /** 317 * Deletes the relation in the context of the given layer. 318 * 319 * @param layer the layer in whose context the relation is deleted. Must not be null. 320 * @param toDelete the relation to be deleted. Must not be null. 321 * @throws IllegalArgumentException if layer is null 322 * @throws IllegalArgumentException if toDelete is nul 323 */ 324 public static void deleteRelation(OsmDataLayer layer, Relation toDelete) { 325 CheckParameterUtil.ensureParameterNotNull(layer, "layer"); 326 CheckParameterUtil.ensureParameterNotNull(toDelete, "toDelete"); 327 328 Command cmd = DeleteCommand.delete(layer, Collections.singleton(toDelete)); 329 if (cmd != null) { 330 // cmd can be null if the user cancels dialogs DialogCommand displays 331 Main.main.undoRedo.add(cmd); 332 if (getCurrentDataSet().getSelectedRelations().contains(toDelete)) { 333 getCurrentDataSet().toggleSelected(toDelete); 334 } 335 RelationDialogManager.getRelationDialogManager().close(layer, toDelete); 336 } 337 } 338 339 private DeleteParameters getDeleteParameters(MouseEvent e, int modifiers) { 340 updateKeyModifiers(modifiers); 341 342 DeleteParameters result = new DeleteParameters(); 343 344 result.nearestNode = Main.map.mapView.getNearestNode(e.getPoint(), OsmPrimitive.isSelectablePredicate); 345 if (result.nearestNode == null) { 346 result.nearestSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate); 347 if (result.nearestSegment != null) { 348 if (shift) { 349 result.mode = DeleteMode.segment; 350 } else if (ctrl) { 351 result.mode = DeleteMode.way_with_references; 352 } else { 353 result.mode = alt ? DeleteMode.way : DeleteMode.way_with_nodes; 354 } 355 } else { 356 result.mode = DeleteMode.none; 357 } 358 } else if (ctrl) { 359 result.mode = DeleteMode.node_with_references; 360 } else { 361 result.mode = DeleteMode.node; 362 } 363 364 return result; 365 } 366 367 /** 368 * This function takes any mouse event argument and builds the list of elements 369 * that should be deleted but does not actually delete them. 370 * @param e MouseEvent from which modifiers and position are taken 371 * @param modifiers For explanation, see {@link #updateCursor} 372 * @param silent Set to true if the user should not be bugged with additional dialogs 373 * @return delete command 374 */ 375 private Command buildDeleteCommands(MouseEvent e, int modifiers, boolean silent) { 376 DeleteParameters parameters = getDeleteParameters(e, modifiers); 377 switch (parameters.mode) { 378 case node: 379 return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestNode), false, silent); 380 case node_with_references: 381 return DeleteCommand.deleteWithReferences(getEditLayer(), Collections.singleton(parameters.nearestNode), silent); 382 case segment: 383 return DeleteCommand.deleteWaySegment(getEditLayer(), parameters.nearestSegment); 384 case way: 385 return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), false, silent); 386 case way_with_nodes: 387 return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true, silent); 388 case way_with_references: 389 return DeleteCommand.deleteWithReferences(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true); 390 default: 391 return null; 392 } 393 } 394 395 /** 396 * This is required to update the cursors when ctrl/shift/alt is pressed 397 */ 398 @Override 399 public void modifiersChanged(int modifiers) { 400 if (oldEvent == null) 401 return; 402 // We don't have a mouse event, so we pass the old mouse event but the new modifiers. 403 giveUserFeedback(oldEvent, modifiers); 404 } 405}