source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/DeleteAction.java@ 15126

Last change on this file since 15126 was 15126, checked in by Don-vip, 5 years ago

fix #15030 - make sure deleting a relation effectively removes it from selection

  • Property svn:eol-style set to native
File size: 16.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Cursor;
7import java.awt.event.ActionEvent;
8import java.awt.event.KeyEvent;
9import java.awt.event.MouseEvent;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.HashSet;
13import java.util.List;
14import java.util.Set;
15import java.util.stream.Collectors;
16
17import org.openstreetmap.josm.command.Command;
18import org.openstreetmap.josm.command.DeleteCommand;
19import org.openstreetmap.josm.data.UndoRedoHandler;
20import org.openstreetmap.josm.data.osm.DataSet;
21import org.openstreetmap.josm.data.osm.Node;
22import org.openstreetmap.josm.data.osm.OsmPrimitive;
23import org.openstreetmap.josm.data.osm.Relation;
24import org.openstreetmap.josm.data.osm.WaySegment;
25import org.openstreetmap.josm.gui.MainApplication;
26import org.openstreetmap.josm.gui.MapFrame;
27import org.openstreetmap.josm.gui.MapView;
28import org.openstreetmap.josm.gui.dialogs.relation.RelationDialogManager;
29import org.openstreetmap.josm.gui.layer.Layer;
30import org.openstreetmap.josm.gui.layer.MainLayerManager;
31import org.openstreetmap.josm.gui.layer.OsmDataLayer;
32import org.openstreetmap.josm.gui.util.HighlightHelper;
33import org.openstreetmap.josm.gui.util.ModifierExListener;
34import org.openstreetmap.josm.spi.preferences.Config;
35import org.openstreetmap.josm.tools.CheckParameterUtil;
36import org.openstreetmap.josm.tools.ImageProvider;
37import org.openstreetmap.josm.tools.Shortcut;
38
39/**
40 * A map mode that enables the user to delete nodes and other objects.
41 *
42 * The user can click on an object, which gets deleted if possible. When Ctrl is
43 * pressed when releasing the button, the objects and all its references are deleted.
44 *
45 * If the user did not press Ctrl and the object has any references, the user
46 * is informed and nothing is deleted.
47 *
48 * If the user enters the mapmode and any object is selected, all selected
49 * objects are deleted, if possible.
50 *
51 * @author imi
52 */
53public class DeleteAction extends MapMode implements ModifierExListener {
54 // Cache previous mouse event (needed when only the modifier keys are pressed but the mouse isn't moved)
55 private MouseEvent oldEvent;
56
57 /**
58 * elements that have been highlighted in the previous iteration. Used
59 * to remove the highlight from them again as otherwise the whole data
60 * set would have to be checked.
61 */
62 private transient WaySegment oldHighlightedWaySegment;
63
64 private static final HighlightHelper HIGHLIGHT_HELPER = new HighlightHelper();
65 private boolean drawTargetHighlight;
66
67 enum DeleteMode {
68 none(/* ICON(cursor/modifier/) */ "delete"),
69 segment(/* ICON(cursor/modifier/) */ "delete_segment"),
70 node(/* ICON(cursor/modifier/) */ "delete_node"),
71 node_with_references(/* ICON(cursor/modifier/) */ "delete_node"),
72 way(/* ICON(cursor/modifier/) */ "delete_way_only"),
73 way_with_references(/* ICON(cursor/modifier/) */ "delete_way_normal"),
74 way_with_nodes(/* ICON(cursor/modifier/) */ "delete_way_node_only");
75
76 private final Cursor c;
77
78 DeleteMode(String cursorName) {
79 c = ImageProvider.getCursor("normal", cursorName);
80 }
81
82 /**
83 * Returns the mode cursor.
84 * @return the mode cursor
85 */
86 public Cursor cursor() {
87 return c;
88 }
89 }
90
91 private static class DeleteParameters {
92 private DeleteMode mode;
93 private Node nearestNode;
94 private WaySegment nearestSegment;
95 }
96
97 /**
98 * Construct a new DeleteAction. Mnemonic is the delete - key.
99 * @since 11713
100 */
101 public DeleteAction() {
102 super(tr("Delete Mode"),
103 "delete",
104 tr("Delete nodes or ways."),
105 Shortcut.registerShortcut("mapmode:delete", tr("Mode: {0}", tr("Delete")),
106 KeyEvent.VK_DELETE, Shortcut.CTRL),
107 ImageProvider.getCursor("normal", "delete"));
108 }
109
110 @Override
111 public void enterMode() {
112 super.enterMode();
113 if (!isEnabled())
114 return;
115
116 drawTargetHighlight = Config.getPref().getBoolean("draw.target-highlight", true);
117
118 MapFrame map = MainApplication.getMap();
119 map.mapView.addMouseListener(this);
120 map.mapView.addMouseMotionListener(this);
121 // This is required to update the cursors when ctrl/shift/alt is pressed
122 map.keyDetector.addModifierExListener(this);
123 }
124
125 @Override
126 public void exitMode() {
127 super.exitMode();
128 MapFrame map = MainApplication.getMap();
129 map.mapView.removeMouseListener(this);
130 map.mapView.removeMouseMotionListener(this);
131 map.keyDetector.removeModifierExListener(this);
132 removeHighlighting();
133 }
134
135 @Override
136 public void actionPerformed(ActionEvent e) {
137 super.actionPerformed(e);
138 doActionPerformed(e);
139 }
140
141 /**
142 * Invoked when the action occurs.
143 * @param e Action event
144 */
145 public void doActionPerformed(ActionEvent e) {
146 MainLayerManager lm = MainApplication.getLayerManager();
147 OsmDataLayer editLayer = lm.getEditLayer();
148 if (editLayer == null) {
149 return;
150 }
151
152 updateKeyModifiers(e);
153
154 Command c;
155 if (ctrl) {
156 c = DeleteCommand.deleteWithReferences(lm.getEditDataSet().getSelected());
157 } else {
158 c = DeleteCommand.delete(lm.getEditDataSet().getSelected(), !alt /* also delete nodes in way */);
159 }
160 // if c is null, an error occurred or the user aborted. Don't do anything in that case.
161 if (c != null) {
162 UndoRedoHandler.getInstance().add(c);
163 //FIXME: This should not be required, DeleteCommand should update the selection, otherwise undo/redo won't work.
164 lm.getEditDataSet().setSelected();
165 }
166 }
167
168 @Override
169 public void mouseDragged(MouseEvent e) {
170 mouseMoved(e);
171 }
172
173 /**
174 * Listen to mouse move to be able to update the cursor (and highlights)
175 * @param e The mouse event that has been captured
176 */
177 @Override
178 public void mouseMoved(MouseEvent e) {
179 oldEvent = e;
180 giveUserFeedback(e);
181 }
182
183 /**
184 * removes any highlighting that may have been set beforehand.
185 */
186 private void removeHighlighting() {
187 HIGHLIGHT_HELPER.clear();
188 DataSet ds = getLayerManager().getEditDataSet();
189 if (ds != null) {
190 ds.clearHighlightedWaySegments();
191 }
192 }
193
194 /**
195 * handles everything related to highlighting primitives and way
196 * segments for the given pointer position (via MouseEvent) and modifiers.
197 * @param e current mouse event
198 * @param modifiers extended mouse modifiers, not necessarly taken from the given mouse event
199 */
200 private void addHighlighting(MouseEvent e, int modifiers) {
201 if (!drawTargetHighlight)
202 return;
203
204 Set<OsmPrimitive> newHighlights = new HashSet<>();
205 DeleteParameters parameters = getDeleteParameters(e, modifiers);
206
207 if (parameters.mode == DeleteMode.segment) {
208 // deleting segments is the only action not working on OsmPrimitives
209 // so we have to handle them separately.
210 repaintIfRequired(newHighlights, parameters.nearestSegment);
211 } else {
212 // don't call buildDeleteCommands for DeleteMode.segment because it doesn't support
213 // silent operation and SplitWayAction will show dialogs. A lot.
214 Command delCmd = buildDeleteCommands(e, modifiers, true);
215 if (delCmd != null) {
216 // all other cases delete OsmPrimitives directly, so we can safely do the following
217 for (OsmPrimitive osm : delCmd.getParticipatingPrimitives()) {
218 newHighlights.add(osm);
219 }
220 }
221 repaintIfRequired(newHighlights, null);
222 }
223 }
224
225 private void repaintIfRequired(Set<OsmPrimitive> newHighlights, WaySegment newHighlightedWaySegment) {
226 boolean needsRepaint = false;
227 OsmDataLayer editLayer = getLayerManager().getEditLayer();
228
229 if (newHighlightedWaySegment == null && oldHighlightedWaySegment != null) {
230 if (editLayer != null) {
231 editLayer.data.clearHighlightedWaySegments();
232 needsRepaint = true;
233 }
234 oldHighlightedWaySegment = null;
235 } else if (newHighlightedWaySegment != null && !newHighlightedWaySegment.equals(oldHighlightedWaySegment)) {
236 if (editLayer != null) {
237 editLayer.data.setHighlightedWaySegments(Collections.singleton(newHighlightedWaySegment));
238 needsRepaint = true;
239 }
240 oldHighlightedWaySegment = newHighlightedWaySegment;
241 }
242 needsRepaint |= HIGHLIGHT_HELPER.highlightOnly(newHighlights);
243 if (needsRepaint && editLayer != null) {
244 editLayer.invalidate();
245 }
246 }
247
248 /**
249 * This function handles all work related to updating the cursor and highlights
250 *
251 * @param e current mouse event
252 * @param modifiers extended mouse modifiers, not necessarly taken from the given mouse event
253 */
254 private void updateCursor(MouseEvent e, int modifiers) {
255 if (!MainApplication.isDisplayingMapView())
256 return;
257 MapFrame map = MainApplication.getMap();
258 if (!map.mapView.isActiveLayerVisible() || e == null)
259 return;
260
261 DeleteParameters parameters = getDeleteParameters(e, modifiers);
262 map.mapView.setNewCursor(parameters.mode.cursor(), this);
263 }
264
265 /**
266 * Gives the user feedback for the action he/she is about to do. Currently
267 * calls the cursor and target highlighting routines. Allows for modifiers
268 * not taken from the given mouse event.
269 *
270 * Normally the mouse event also contains the modifiers. However, when the
271 * mouse is not moved and only modifier keys are pressed, no mouse event
272 * occurs. We can use AWTEvent to catch those but still lack a proper
273 * mouseevent. Instead we copy the previous event and only update the modifiers.
274 * @param e mouse event
275 * @param modifiers mouse modifiers
276 */
277 private void giveUserFeedback(MouseEvent e, int modifiers) {
278 updateCursor(e, modifiers);
279 addHighlighting(e, modifiers);
280 }
281
282 /**
283 * Gives the user feedback for the action he/she is about to do. Currently
284 * calls the cursor and target highlighting routines. Extracts modifiers
285 * from mouse event.
286 * @param e mouse event
287 */
288 private void giveUserFeedback(MouseEvent e) {
289 giveUserFeedback(e, e.getModifiersEx());
290 }
291
292 /**
293 * If user clicked with the left button, delete the nearest object.
294 */
295 @Override
296 public void mouseReleased(MouseEvent e) {
297 if (e.getButton() != MouseEvent.BUTTON1)
298 return;
299 MapFrame map = MainApplication.getMap();
300 if (!map.mapView.isActiveLayerVisible())
301 return;
302
303 // request focus in order to enable the expected keyboard shortcuts
304 //
305 map.mapView.requestFocus();
306
307 Command c = buildDeleteCommands(e, e.getModifiersEx(), false);
308 if (c != null) {
309 UndoRedoHandler.getInstance().add(c);
310 }
311
312 getLayerManager().getEditDataSet().setSelected();
313 giveUserFeedback(e);
314 }
315
316 @Override
317 public String getModeHelpText() {
318 // CHECKSTYLE.OFF: LineLength
319 return tr("Click to delete. Shift: delete way segment. Alt: do not delete unused nodes when deleting a way. Ctrl: delete referring objects.");
320 // CHECKSTYLE.ON: LineLength
321 }
322
323 @Override
324 public boolean layerIsSupported(Layer l) {
325 return isEditableDataLayer(l);
326 }
327
328 @Override
329 protected void updateEnabledState() {
330 setEnabled(MainApplication.isDisplayingMapView() && MainApplication.getMap().mapView.isActiveLayerDrawable());
331 }
332
333 /**
334 * Deletes the relation in the context of the given layer.
335 *
336 * @param layer the layer in whose context the relation is deleted. Must not be null.
337 * @param toDelete the relation to be deleted. Must not be null.
338 * @throws IllegalArgumentException if layer is null
339 * @throws IllegalArgumentException if toDelete is null
340 */
341 public static void deleteRelation(OsmDataLayer layer, Relation toDelete) {
342 deleteRelations(layer, Collections.singleton(toDelete));
343 }
344
345 /**
346 * Deletes the relations in the context of the given layer.
347 *
348 * @param layer the layer in whose context the relations are deleted. Must not be null.
349 * @param toDelete the relations to be deleted. Must not be null.
350 * @throws IllegalArgumentException if layer is null
351 * @throws IllegalArgumentException if toDelete is null
352 */
353 public static void deleteRelations(OsmDataLayer layer, Collection<Relation> toDelete) {
354 CheckParameterUtil.ensureParameterNotNull(layer, "layer");
355 CheckParameterUtil.ensureParameterNotNull(toDelete, "toDelete");
356
357 final Command cmd = DeleteCommand.delete(toDelete);
358 if (cmd != null) {
359 // cmd can be null if the user cancels dialogs DialogCommand displays
360 List<Relation> toUnselect = toDelete.stream().filter(Relation::isSelected).collect(Collectors.toList());
361 UndoRedoHandler.getInstance().add(cmd);
362 toDelete.forEach(relation -> RelationDialogManager.getRelationDialogManager().close(layer, relation));
363 toUnselect.forEach(layer.data::toggleSelected);
364 }
365 }
366
367 private DeleteParameters getDeleteParameters(MouseEvent e, int modifiers) {
368 updateKeyModifiersEx(modifiers);
369
370 DeleteParameters result = new DeleteParameters();
371
372 MapView mapView = MainApplication.getMap().mapView;
373 result.nearestNode = mapView.getNearestNode(e.getPoint(), OsmPrimitive::isSelectable);
374 if (result.nearestNode == null) {
375 result.nearestSegment = mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive::isSelectable);
376 if (result.nearestSegment != null) {
377 if (shift) {
378 result.mode = DeleteMode.segment;
379 } else if (ctrl) {
380 result.mode = DeleteMode.way_with_references;
381 } else {
382 result.mode = alt ? DeleteMode.way : DeleteMode.way_with_nodes;
383 }
384 } else {
385 result.mode = DeleteMode.none;
386 }
387 } else if (ctrl) {
388 result.mode = DeleteMode.node_with_references;
389 } else {
390 result.mode = DeleteMode.node;
391 }
392
393 return result;
394 }
395
396 /**
397 * This function takes any mouse event argument and builds the list of elements
398 * that should be deleted but does not actually delete them.
399 * @param e MouseEvent from which modifiers and position are taken
400 * @param modifiers For explanation, see {@link #updateCursor}
401 * @param silent Set to true if the user should not be bugged with additional dialogs
402 * @return delete command
403 */
404 private Command buildDeleteCommands(MouseEvent e, int modifiers, boolean silent) {
405 DeleteParameters parameters = getDeleteParameters(e, modifiers);
406 switch (parameters.mode) {
407 case node:
408 return DeleteCommand.delete(Collections.singleton(parameters.nearestNode), false, silent);
409 case node_with_references:
410 return DeleteCommand.deleteWithReferences(Collections.singleton(parameters.nearestNode), silent);
411 case segment:
412 return DeleteCommand.deleteWaySegment(parameters.nearestSegment);
413 case way:
414 return DeleteCommand.delete(Collections.singleton(parameters.nearestSegment.way), false, silent);
415 case way_with_nodes:
416 return DeleteCommand.delete(Collections.singleton(parameters.nearestSegment.way), true, silent);
417 case way_with_references:
418 return DeleteCommand.deleteWithReferences(Collections.singleton(parameters.nearestSegment.way), true);
419 default:
420 return null;
421 }
422 }
423
424 /**
425 * This is required to update the cursors when ctrl/shift/alt is pressed
426 */
427 @Override
428 public void modifiersExChanged(int modifiers) {
429 if (oldEvent == null)
430 return;
431 // We don't have a mouse event, so we pass the old mouse event but the new modifiers.
432 giveUserFeedback(oldEvent, modifiers);
433 }
434}
Note: See TracBrowser for help on using the repository browser.