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

Last change on this file since 16626 was 16626, checked in by simon04, 4 years ago

see #19334 - https://errorprone.info/bugpattern/ImmutableEnumChecker

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