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.AWTEvent; 007import java.awt.Cursor; 008import java.awt.Toolkit; 009import java.awt.event.AWTEventListener; 010import java.awt.event.ActionEvent; 011import java.awt.event.InputEvent; 012import java.awt.event.KeyEvent; 013import java.awt.event.MouseEvent; 014import java.util.Collections; 015import java.util.HashSet; 016import java.util.Set; 017 018import org.openstreetmap.josm.Main; 019import org.openstreetmap.josm.command.Command; 020import org.openstreetmap.josm.command.DeleteCommand; 021import org.openstreetmap.josm.data.osm.DataSet; 022import org.openstreetmap.josm.data.osm.Node; 023import org.openstreetmap.josm.data.osm.OsmPrimitive; 024import org.openstreetmap.josm.data.osm.Relation; 025import org.openstreetmap.josm.data.osm.WaySegment; 026import org.openstreetmap.josm.gui.MapFrame; 027import org.openstreetmap.josm.gui.dialogs.relation.RelationDialogManager; 028import org.openstreetmap.josm.gui.layer.Layer; 029import org.openstreetmap.josm.gui.layer.OsmDataLayer; 030import org.openstreetmap.josm.gui.util.HighlightHelper; 031import org.openstreetmap.josm.tools.CheckParameterUtil; 032import org.openstreetmap.josm.tools.ImageProvider; 033import org.openstreetmap.josm.tools.Shortcut; 034 035/** 036 * A map mode that enables the user to delete nodes and other objects. 037 * 038 * The user can click on an object, which gets deleted if possible. When Ctrl is 039 * pressed when releasing the button, the objects and all its references are 040 * 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 AWTEventListener { 051 // Cache previous mouse event (needed when only the modifier keys are 052 // pressed but the mouse isn't moved) 053 private MouseEvent oldEvent = null; 054 055 /** 056 * elements that have been highlighted in the previous iteration. Used 057 * to remove the highlight from them again as otherwise the whole data 058 * set would have to be checked. 059 */ 060 private WaySegment oldHighlightedWaySegment = null; 061 062 private static final HighlightHelper highlightHelper = new HighlightHelper(); 063 private boolean drawTargetHighlight; 064 065 private enum DeleteMode { 066 none("delete"), 067 segment("delete_segment"), 068 node("delete_node"), 069 node_with_references("delete_node"), 070 way("delete_way_only"), 071 way_with_references("delete_way_normal"), 072 way_with_nodes("delete_way_node_only"); 073 074 private final Cursor c; 075 076 private DeleteMode(String cursorName) { 077 c = ImageProvider.getCursor("normal", cursorName); 078 } 079 080 public Cursor cursor() { 081 return c; 082 } 083 } 084 085 private static class DeleteParameters { 086 DeleteMode mode; 087 Node nearestNode; 088 WaySegment nearestSegment; 089 } 090 091 /** 092 * Construct a new DeleteAction. Mnemonic is the delete - key. 093 * @param mapFrame The frame this action belongs to. 094 */ 095 public DeleteAction(MapFrame mapFrame) { 096 super(tr("Delete Mode"), 097 "delete", 098 tr("Delete nodes or ways."), 099 Shortcut.registerShortcut("mapmode:delete", tr("Mode: {0}",tr("Delete")), 100 KeyEvent.VK_DELETE, Shortcut.CTRL), 101 mapFrame, 102 ImageProvider.getCursor("normal", "delete")); 103 } 104 105 @Override public void enterMode() { 106 super.enterMode(); 107 if (!isEnabled()) 108 return; 109 110 drawTargetHighlight = Main.pref.getBoolean("draw.target-highlight", true); 111 112 Main.map.mapView.addMouseListener(this); 113 Main.map.mapView.addMouseMotionListener(this); 114 // This is required to update the cursors when ctrl/shift/alt is pressed 115 try { 116 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK); 117 } catch (SecurityException ex) { 118 Main.warn(ex); 119 } 120 } 121 122 @Override public void exitMode() { 123 super.exitMode(); 124 Main.map.mapView.removeMouseListener(this); 125 Main.map.mapView.removeMouseMotionListener(this); 126 try { 127 Toolkit.getDefaultToolkit().removeAWTEventListener(this); 128 } catch (SecurityException ex) { 129 Main.warn(ex); 130 } 131 removeHighlighting(); 132 } 133 134 @Override public void actionPerformed(ActionEvent e) { 135 super.actionPerformed(e); 136 doActionPerformed(e); 137 } 138 139 static public void doActionPerformed(ActionEvent e) { 140 if(!Main.map.mapView.isActiveLayerDrawable()) 141 return; 142 boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0; 143 boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0; 144 145 Command c; 146 if (ctrl) { 147 c = DeleteCommand.deleteWithReferences(getEditLayer(),getCurrentDataSet().getSelected()); 148 } else { 149 c = DeleteCommand.delete(getEditLayer(),getCurrentDataSet().getSelected(), !alt /* also delete nodes in way */); 150 } 151 // if c is null, an error occurred or the user aborted. Don't do anything in that case. 152 if (c != null) { 153 Main.main.undoRedo.add(c); 154 getCurrentDataSet().setSelected(); 155 Main.map.repaint(); 156 } 157 } 158 159 @Override public void mouseDragged(MouseEvent e) { 160 mouseMoved(e); 161 } 162 163 /** 164 * Listen to mouse move to be able to update the cursor (and highlights) 165 * @param e The mouse event that has been captured 166 */ 167 @Override public void mouseMoved(MouseEvent e) { 168 oldEvent = e; 169 giveUserFeedback(e); 170 } 171 172 /** 173 * removes any highlighting that may have been set beforehand. 174 */ 175 private void removeHighlighting() { 176 highlightHelper.clear(); 177 DataSet ds = getCurrentDataSet(); 178 if(ds != null) { 179 ds.clearHighlightedWaySegments(); 180 } 181 } 182 183 /** 184 * handles everything related to highlighting primitives and way 185 * segments for the given pointer position (via MouseEvent) and 186 * modifiers. 187 * @param e 188 * @param modifiers 189 */ 190 private void addHighlighting(MouseEvent e, int modifiers) { 191 if(!drawTargetHighlight) 192 return; 193 194 Set<OsmPrimitive> newHighlights = new HashSet<OsmPrimitive>(); 195 DeleteParameters parameters = getDeleteParameters(e, modifiers); 196 197 if(parameters.mode == DeleteMode.segment) { 198 // deleting segments is the only action not working on OsmPrimitives 199 // so we have to handle them separately. 200 repaintIfRequired(newHighlights, parameters.nearestSegment); 201 } else { 202 // don't call buildDeleteCommands for DeleteMode.segment because it doesn't support 203 // silent operation and SplitWayAction will show dialogs. A lot. 204 Command delCmd = buildDeleteCommands(e, modifiers, true); 205 if(delCmd != null) { 206 // all other cases delete OsmPrimitives directly, so we can 207 // safely do the following 208 for(OsmPrimitive osm : delCmd.getParticipatingPrimitives()) { 209 newHighlights.add(osm); 210 } 211 } 212 repaintIfRequired(newHighlights, null); 213 } 214 } 215 216 private void repaintIfRequired(Set<OsmPrimitive> newHighlights, WaySegment newHighlightedWaySegment) { 217 boolean needsRepaint = false; 218 DataSet ds = getCurrentDataSet(); 219 220 if(newHighlightedWaySegment == null && oldHighlightedWaySegment != null) { 221 if(ds != null) { 222 ds.clearHighlightedWaySegments(); 223 needsRepaint = true; 224 } 225 oldHighlightedWaySegment = null; 226 } else if(newHighlightedWaySegment != null && !newHighlightedWaySegment.equals(oldHighlightedWaySegment)) { 227 if(ds != null) { 228 ds.setHighlightedWaySegments(Collections.singleton(newHighlightedWaySegment)); 229 needsRepaint = true; 230 } 231 oldHighlightedWaySegment = newHighlightedWaySegment; 232 } 233 needsRepaint |= highlightHelper.highlightOnly(newHighlights); 234 if(needsRepaint) { 235 Main.map.mapView.repaint(); 236 } 237 } 238 239 /** 240 * This function handles all work related to updating the cursor and 241 * highlights 242 * 243 * @param e 244 * @param modifiers 245 */ 246 private void updateCursor(MouseEvent e, int modifiers) { 247 if (!Main.isDisplayingMapView()) 248 return; 249 if(!Main.map.mapView.isActiveLayerVisible() || e == null) 250 return; 251 252 DeleteParameters parameters = getDeleteParameters(e, modifiers); 253 Main.map.mapView.setNewCursor(parameters.mode.cursor(), this); 254 } 255 /** 256 * Gives the user feedback for the action he/she is about to do. Currently 257 * calls the cursor and target highlighting routines. Allows for modifiers 258 * not taken from the given mouse event. 259 * 260 * Normally the mouse event also contains the modifiers. However, when the 261 * mouse is not moved and only modifier keys are pressed, no mouse event 262 * occurs. We can use AWTEvent to catch those but still lack a proper 263 * mouseevent. Instead we copy the previous event and only update the 264 * modifiers. 265 */ 266 private void giveUserFeedback(MouseEvent e, int modifiers) { 267 updateCursor(e, modifiers); 268 addHighlighting(e, modifiers); 269 } 270 271 /** 272 * Gives the user feedback for the action he/she is about to do. Currently 273 * calls the cursor and target highlighting routines. Extracts modifiers 274 * from mouse event. 275 */ 276 private void giveUserFeedback(MouseEvent e) { 277 giveUserFeedback(e, e.getModifiers()); 278 } 279 280 /** 281 * If user clicked with the left button, delete the nearest object. 282 * position. 283 */ 284 @Override 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 public String getModeHelpText() { 304 return tr("Click to delete. Shift: delete way segment. Alt: do not delete unused nodes when deleting a way. Ctrl: delete referring objects."); 305 } 306 307 @Override 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 * @exception IllegalArgumentException thrown if layer is null 322 * @exception IllegalArgumentException thrown 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 373 * dialogs 374 * @return delete command 375 */ 376 private Command buildDeleteCommands(MouseEvent e, int modifiers, boolean silent) { 377 DeleteParameters parameters = getDeleteParameters(e, modifiers); 378 switch (parameters.mode) { 379 case node: 380 return DeleteCommand.delete(getEditLayer(),Collections.singleton(parameters.nearestNode), false, silent); 381 case node_with_references: 382 return DeleteCommand.deleteWithReferences(getEditLayer(),Collections.singleton(parameters.nearestNode), silent); 383 case segment: 384 return DeleteCommand.deleteWaySegment(getEditLayer(), parameters.nearestSegment); 385 case way: 386 return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), false, silent); 387 case way_with_nodes: 388 return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true, silent); 389 case way_with_references: 390 return DeleteCommand.deleteWithReferences(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true); 391 default: 392 return null; 393 } 394 } 395 396 /** 397 * This is required to update the cursors when ctrl/shift/alt is pressed 398 */ 399 @Override 400 public void eventDispatched(AWTEvent e) { 401 if(oldEvent == null) 402 return; 403 // We don't have a mouse event, so we pass the old mouse event but the 404 // new modifiers. 405 giveUserFeedback(oldEvent, ((InputEvent) e).getModifiers()); 406 } 407}