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.KeyEvent;
009import java.awt.event.MouseEvent;
010import java.util.Collection;
011import java.util.Collections;
012import java.util.HashSet;
013import java.util.List;
014import java.util.Set;
015import java.util.stream.Collectors;
016
017import org.openstreetmap.josm.command.Command;
018import org.openstreetmap.josm.command.DeleteCommand;
019import org.openstreetmap.josm.data.UndoRedoHandler;
020import org.openstreetmap.josm.data.osm.DataSet;
021import org.openstreetmap.josm.data.osm.Node;
022import org.openstreetmap.josm.data.osm.OsmPrimitive;
023import org.openstreetmap.josm.data.osm.Relation;
024import org.openstreetmap.josm.data.osm.WaySegment;
025import org.openstreetmap.josm.gui.ExtendedDialog;
026import org.openstreetmap.josm.gui.MainApplication;
027import org.openstreetmap.josm.gui.MapFrame;
028import org.openstreetmap.josm.gui.MapView;
029import org.openstreetmap.josm.gui.dialogs.relation.RelationDialogManager;
030import org.openstreetmap.josm.gui.layer.Layer;
031import org.openstreetmap.josm.gui.layer.MainLayerManager;
032import org.openstreetmap.josm.gui.layer.OsmDataLayer;
033import org.openstreetmap.josm.gui.util.HighlightHelper;
034import org.openstreetmap.josm.gui.util.ModifierExListener;
035import org.openstreetmap.josm.spi.preferences.Config;
036import org.openstreetmap.josm.tools.CheckParameterUtil;
037import org.openstreetmap.josm.tools.ImageProvider;
038import org.openstreetmap.josm.tools.Shortcut;
039
040/**
041 * A map mode that enables the user to delete nodes and other objects.
042 *
043 * The user can click on an object, which gets deleted if possible. When Ctrl is
044 * pressed when releasing the button, the objects and all its references are deleted.
045 *
046 * If the user did not press Ctrl and the object has any references, the user
047 * is informed and nothing is deleted.
048 *
049 * If the user enters the mapmode and any object is selected, all selected
050 * objects are deleted, if possible.
051 *
052 * @author imi
053 */
054public class DeleteAction extends MapMode implements ModifierExListener {
055    // Cache previous mouse event (needed when only the modifier keys are pressed but the mouse isn't moved)
056    private MouseEvent oldEvent;
057
058    /**
059     * elements that have been highlighted in the previous iteration. Used
060     * to remove the highlight from them again as otherwise the whole data
061     * set would have to be checked.
062     */
063    private transient WaySegment oldHighlightedWaySegment;
064
065    private static final HighlightHelper HIGHLIGHT_HELPER = new HighlightHelper();
066    private boolean drawTargetHighlight;
067
068    enum DeleteMode {
069        none(/* ICON(cursor/modifier/) */ "delete"),
070        segment(/* ICON(cursor/modifier/) */ "delete_segment"),
071        node(/* ICON(cursor/modifier/) */ "delete_node"),
072        node_with_references(/* ICON(cursor/modifier/) */ "delete_node"),
073        way(/* ICON(cursor/modifier/) */ "delete_way_only"),
074        way_with_references(/* ICON(cursor/modifier/) */ "delete_way_normal"),
075        way_with_nodes(/* ICON(cursor/modifier/) */ "delete_way_node_only");
076
077        @SuppressWarnings("ImmutableEnumChecker")
078        private final Cursor c;
079
080        DeleteMode(String cursorName) {
081            c = ImageProvider.getCursor("normal", cursorName);
082        }
083
084        /**
085         * Returns the mode cursor.
086         * @return the mode cursor
087         */
088        public Cursor cursor() {
089            return c;
090        }
091    }
092
093    private static class DeleteParameters {
094        private DeleteMode mode;
095        private Node nearestNode;
096        private WaySegment nearestSegment;
097    }
098
099    /**
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.getWay()), false, silent);
421        case way_with_nodes:
422            return DeleteCommand.delete(Collections.singleton(parameters.nearestSegment.getWay()), true, silent);
423        case way_with_references:
424            return DeleteCommand.deleteWithReferences(Collections.singleton(parameters.nearestSegment.getWay()), 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 static 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}