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