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

Last change on this file since 12641 was 12641, checked in by Don-vip, 4 weeks ago

see #15182 - deprecate Main.main.undoRedo. Replacement: gui.MainApplication.undoRedo

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