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 public static void doActionPerformed(ActionEvent e) { 132 if(!Main.map.mapView.isActiveLayerDrawable()) 133 return; 134 boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0; 135 boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0; 136 137 Command c; 138 if (ctrl) { 139 c = DeleteCommand.deleteWithReferences(getEditLayer(),getCurrentDataSet().getSelected()); 140 } else { 141 c = DeleteCommand.delete(getEditLayer(),getCurrentDataSet().getSelected(), !alt /* also delete nodes in way */); 142 } 143 // if c is null, an error occurred or the user aborted. Don't do anything in that case. 144 if (c != null) { 145 Main.main.undoRedo.add(c); 146 getCurrentDataSet().setSelected(); 147 Main.map.repaint(); 148 } 149 } 150 151 @Override public void mouseDragged(MouseEvent e) { 152 mouseMoved(e); 153 } 154 155 /** 156 * Listen to mouse move to be able to update the cursor (and highlights) 157 * @param e The mouse event that has been captured 158 */ 159 @Override public void mouseMoved(MouseEvent e) { 160 oldEvent = e; 161 giveUserFeedback(e); 162 } 163 164 /** 165 * removes any highlighting that may have been set beforehand. 166 */ 167 private void removeHighlighting() { 168 highlightHelper.clear(); 169 DataSet ds = getCurrentDataSet(); 170 if(ds != null) { 171 ds.clearHighlightedWaySegments(); 172 } 173 } 174 175 /** 176 * handles everything related to highlighting primitives and way 177 * segments for the given pointer position (via MouseEvent) and 178 * modifiers. 179 * @param e 180 * @param modifiers 181 */ 182 private void addHighlighting(MouseEvent e, int modifiers) { 183 if(!drawTargetHighlight) 184 return; 185 186 Set<OsmPrimitive> newHighlights = new HashSet<>(); 187 DeleteParameters parameters = getDeleteParameters(e, modifiers); 188 189 if(parameters.mode == DeleteMode.segment) { 190 // deleting segments is the only action not working on OsmPrimitives 191 // so we have to handle them separately. 192 repaintIfRequired(newHighlights, parameters.nearestSegment); 193 } else { 194 // don't call buildDeleteCommands for DeleteMode.segment because it doesn't support 195 // silent operation and SplitWayAction will show dialogs. A lot. 196 Command delCmd = buildDeleteCommands(e, modifiers, true); 197 if(delCmd != null) { 198 // all other cases delete OsmPrimitives directly, so we can 199 // safely do the following 200 for(OsmPrimitive osm : delCmd.getParticipatingPrimitives()) { 201 newHighlights.add(osm); 202 } 203 } 204 repaintIfRequired(newHighlights, null); 205 } 206 } 207 208 private void repaintIfRequired(Set<OsmPrimitive> newHighlights, WaySegment newHighlightedWaySegment) { 209 boolean needsRepaint = false; 210 DataSet ds = getCurrentDataSet(); 211 212 if(newHighlightedWaySegment == null && oldHighlightedWaySegment != null) { 213 if(ds != null) { 214 ds.clearHighlightedWaySegments(); 215 needsRepaint = true; 216 } 217 oldHighlightedWaySegment = null; 218 } else if(newHighlightedWaySegment != null && !newHighlightedWaySegment.equals(oldHighlightedWaySegment)) { 219 if(ds != null) { 220 ds.setHighlightedWaySegments(Collections.singleton(newHighlightedWaySegment)); 221 needsRepaint = true; 222 } 223 oldHighlightedWaySegment = newHighlightedWaySegment; 224 } 225 needsRepaint |= highlightHelper.highlightOnly(newHighlights); 226 if(needsRepaint) { 227 Main.map.mapView.repaint(); 228 } 229 } 230 231 /** 232 * This function handles all work related to updating the cursor and 233 * highlights 234 * 235 * @param e 236 * @param modifiers 237 */ 238 private void updateCursor(MouseEvent e, int modifiers) { 239 if (!Main.isDisplayingMapView()) 240 return; 241 if(!Main.map.mapView.isActiveLayerVisible() || e == null) 242 return; 243 244 DeleteParameters parameters = getDeleteParameters(e, modifiers); 245 Main.map.mapView.setNewCursor(parameters.mode.cursor(), this); 246 } 247 /** 248 * Gives the user feedback for the action he/she is about to do. Currently 249 * calls the cursor and target highlighting routines. Allows for modifiers 250 * not taken from the given mouse event. 251 * 252 * Normally the mouse event also contains the modifiers. However, when the 253 * mouse is not moved and only modifier keys are pressed, no mouse event 254 * occurs. We can use AWTEvent to catch those but still lack a proper 255 * mouseevent. Instead we copy the previous event and only update the 256 * modifiers. 257 */ 258 private void giveUserFeedback(MouseEvent e, int modifiers) { 259 updateCursor(e, modifiers); 260 addHighlighting(e, modifiers); 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. Extracts modifiers 266 * from mouse event. 267 */ 268 private void giveUserFeedback(MouseEvent e) { 269 giveUserFeedback(e, e.getModifiers()); 270 } 271 272 /** 273 * If user clicked with the left button, delete the nearest object. 274 * position. 275 */ 276 @Override public void mouseReleased(MouseEvent e) { 277 if (e.getButton() != MouseEvent.BUTTON1) 278 return; 279 if(!Main.map.mapView.isActiveLayerVisible()) 280 return; 281 282 // request focus in order to enable the expected keyboard shortcuts 283 // 284 Main.map.mapView.requestFocus(); 285 286 Command c = buildDeleteCommands(e, e.getModifiers(), false); 287 if (c != null) { 288 Main.main.undoRedo.add(c); 289 } 290 291 getCurrentDataSet().setSelected(); 292 giveUserFeedback(e); 293 } 294 295 @Override public String getModeHelpText() { 296 return tr("Click to delete. Shift: delete way segment. Alt: do not delete unused nodes when deleting a way. Ctrl: delete referring objects."); 297 } 298 299 @Override public boolean layerIsSupported(Layer l) { 300 return l instanceof OsmDataLayer; 301 } 302 303 @Override 304 protected void updateEnabledState() { 305 setEnabled(Main.isDisplayingMapView() && Main.map.mapView.isActiveLayerDrawable()); 306 } 307 308 /** 309 * Deletes the relation in the context of the given layer. 310 * 311 * @param layer the layer in whose context the relation is deleted. Must not be null. 312 * @param toDelete the relation to be deleted. Must not be null. 313 * @exception IllegalArgumentException thrown if layer is null 314 * @exception IllegalArgumentException thrown if toDelete is nul 315 */ 316 public static void deleteRelation(OsmDataLayer layer, Relation toDelete) { 317 CheckParameterUtil.ensureParameterNotNull(layer, "layer"); 318 CheckParameterUtil.ensureParameterNotNull(toDelete, "toDelete"); 319 320 Command cmd = DeleteCommand.delete(layer, Collections.singleton(toDelete)); 321 if (cmd != null) { 322 // cmd can be null if the user cancels dialogs DialogCommand displays 323 Main.main.undoRedo.add(cmd); 324 if (getCurrentDataSet().getSelectedRelations().contains(toDelete)) { 325 getCurrentDataSet().toggleSelected(toDelete); 326 } 327 RelationDialogManager.getRelationDialogManager().close(layer, toDelete); 328 } 329 } 330 331 private DeleteParameters getDeleteParameters(MouseEvent e, int modifiers) { 332 updateKeyModifiers(modifiers); 333 334 DeleteParameters result = new DeleteParameters(); 335 336 result.nearestNode = Main.map.mapView.getNearestNode(e.getPoint(), OsmPrimitive.isSelectablePredicate); 337 if (result.nearestNode == null) { 338 result.nearestSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate); 339 if (result.nearestSegment != null) { 340 if (shift) { 341 result.mode = DeleteMode.segment; 342 } else if (ctrl) { 343 result.mode = DeleteMode.way_with_references; 344 } else { 345 result.mode = alt?DeleteMode.way:DeleteMode.way_with_nodes; 346 } 347 } else { 348 result.mode = DeleteMode.none; 349 } 350 } else if (ctrl) { 351 result.mode = DeleteMode.node_with_references; 352 } else { 353 result.mode = DeleteMode.node; 354 } 355 356 return result; 357 } 358 359 /** 360 * This function takes any mouse event argument and builds the list of elements 361 * that should be deleted but does not actually delete them. 362 * @param e MouseEvent from which modifiers and position are taken 363 * @param modifiers For explanation, see {@link #updateCursor} 364 * @param silent Set to true if the user should not be bugged with additional 365 * dialogs 366 * @return delete command 367 */ 368 private Command buildDeleteCommands(MouseEvent e, int modifiers, boolean silent) { 369 DeleteParameters parameters = getDeleteParameters(e, modifiers); 370 switch (parameters.mode) { 371 case node: 372 return DeleteCommand.delete(getEditLayer(),Collections.singleton(parameters.nearestNode), false, silent); 373 case node_with_references: 374 return DeleteCommand.deleteWithReferences(getEditLayer(),Collections.singleton(parameters.nearestNode), silent); 375 case segment: 376 return DeleteCommand.deleteWaySegment(getEditLayer(), parameters.nearestSegment); 377 case way: 378 return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), false, silent); 379 case way_with_nodes: 380 return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true, silent); 381 case way_with_references: 382 return DeleteCommand.deleteWithReferences(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true); 383 default: 384 return null; 385 } 386 } 387 388 /** 389 * This is required to update the cursors when ctrl/shift/alt is pressed 390 */ 391 @Override 392 public void modifiersChanged(int modifiers) { 393 if(oldEvent == null) 394 return; 395 // We don't have a mouse event, so we pass the old mouse event but the 396 // new modifiers. 397 giveUserFeedback(oldEvent, modifiers); 398 } 399}