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.Set;
014
015import org.openstreetmap.josm.command.Command;
016import org.openstreetmap.josm.command.DeleteCommand;
017import org.openstreetmap.josm.data.UndoRedoHandler;
018import org.openstreetmap.josm.data.osm.DataSet;
019import org.openstreetmap.josm.data.osm.Node;
020import org.openstreetmap.josm.data.osm.OsmPrimitive;
021import org.openstreetmap.josm.data.osm.Relation;
022import org.openstreetmap.josm.data.osm.WaySegment;
023import org.openstreetmap.josm.gui.MainApplication;
024import org.openstreetmap.josm.gui.MapFrame;
025import org.openstreetmap.josm.gui.MapView;
026import org.openstreetmap.josm.gui.dialogs.relation.RelationDialogManager;
027import org.openstreetmap.josm.gui.layer.Layer;
028import org.openstreetmap.josm.gui.layer.MainLayerManager;
029import org.openstreetmap.josm.gui.layer.OsmDataLayer;
030import org.openstreetmap.josm.gui.util.HighlightHelper;
031import org.openstreetmap.josm.gui.util.ModifierExListener;
032import org.openstreetmap.josm.spi.preferences.Config;
033import org.openstreetmap.josm.tools.CheckParameterUtil;
034import org.openstreetmap.josm.tools.ImageProvider;
035import org.openstreetmap.josm.tools.Shortcut;
036
037/**
038 * A map mode that enables the user to delete nodes and other objects.
039 *
040 * The user can click on an object, which gets deleted if possible. When Ctrl is
041 * pressed when releasing the button, the objects and all its references are deleted.
042 *
043 * If the user did not press Ctrl and the object has any references, the user
044 * is informed and nothing is deleted.
045 *
046 * If the user enters the mapmode and any object is selected, all selected
047 * objects are deleted, if possible.
048 *
049 * @author imi
050 */
051public class DeleteAction extends MapMode implements ModifierExListener {
052    // Cache previous mouse event (needed when only the modifier keys are pressed but the mouse isn't moved)
053    private MouseEvent oldEvent;
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 transient WaySegment oldHighlightedWaySegment;
061
062    private static final HighlightHelper HIGHLIGHT_HELPER = new HighlightHelper();
063    private boolean drawTargetHighlight;
064
065    enum DeleteMode {
066        none(/* ICON(cursor/modifier/) */ "delete"),
067        segment(/* ICON(cursor/modifier/) */ "delete_segment"),
068        node(/* ICON(cursor/modifier/) */ "delete_node"),
069        node_with_references(/* ICON(cursor/modifier/) */ "delete_node"),
070        way(/* ICON(cursor/modifier/) */ "delete_way_only"),
071        way_with_references(/* ICON(cursor/modifier/) */ "delete_way_normal"),
072        way_with_nodes(/* ICON(cursor/modifier/) */ "delete_way_node_only");
073
074        private final Cursor c;
075
076        DeleteMode(String cursorName) {
077            c = ImageProvider.getCursor("normal", cursorName);
078        }
079
080        /**
081         * Returns the mode cursor.
082         * @return the mode cursor
083         */
084        public Cursor cursor() {
085            return c;
086        }
087    }
088
089    private static class DeleteParameters {
090        private DeleteMode mode;
091        private Node nearestNode;
092        private WaySegment nearestSegment;
093    }
094
095    /**
096     * Construct a new DeleteAction. Mnemonic is the delete - key.
097     * @since 11713
098     */
099    public DeleteAction() {
100        super(tr("Delete Mode"),
101                "delete",
102                tr("Delete nodes or ways."),
103                Shortcut.registerShortcut("mapmode:delete", tr("Mode: {0}", tr("Delete")),
104                KeyEvent.VK_DELETE, Shortcut.CTRL),
105                ImageProvider.getCursor("normal", "delete"));
106    }
107
108    @Override
109    public void enterMode() {
110        super.enterMode();
111        if (!isEnabled())
112            return;
113
114        drawTargetHighlight = Config.getPref().getBoolean("draw.target-highlight", true);
115
116        MapFrame map = MainApplication.getMap();
117        map.mapView.addMouseListener(this);
118        map.mapView.addMouseMotionListener(this);
119        // This is required to update the cursors when ctrl/shift/alt is pressed
120        map.keyDetector.addModifierExListener(this);
121    }
122
123    @Override
124    public void exitMode() {
125        super.exitMode();
126        MapFrame map = MainApplication.getMap();
127        map.mapView.removeMouseListener(this);
128        map.mapView.removeMouseMotionListener(this);
129        map.keyDetector.removeModifierExListener(this);
130        removeHighlighting();
131    }
132
133    @Override
134    public void actionPerformed(ActionEvent e) {
135        super.actionPerformed(e);
136        doActionPerformed(e);
137    }
138
139    /**
140     * Invoked when the action occurs.
141     * @param e Action event
142     */
143    public void doActionPerformed(ActionEvent e) {
144        MainLayerManager lm = MainApplication.getLayerManager();
145        OsmDataLayer editLayer = lm.getEditLayer();
146        if (editLayer == null) {
147            return;
148        }
149
150        updateKeyModifiers(e);
151
152        Command c;
153        if (ctrl) {
154            c = DeleteCommand.deleteWithReferences(lm.getEditDataSet().getSelected());
155        } else {
156            c = DeleteCommand.delete(lm.getEditDataSet().getSelected(), !alt /* also delete nodes in way */);
157        }
158        // if c is null, an error occurred or the user aborted. Don't do anything in that case.
159        if (c != null) {
160            UndoRedoHandler.getInstance().add(c);
161            //FIXME: This should not be required, DeleteCommand should update the selection, otherwise undo/redo won't work.
162            lm.getEditDataSet().setSelected();
163        }
164    }
165
166    @Override
167    public void mouseDragged(MouseEvent e) {
168        mouseMoved(e);
169    }
170
171    /**
172     * Listen to mouse move to be able to update the cursor (and highlights)
173     * @param e The mouse event that has been captured
174     */
175    @Override
176    public void mouseMoved(MouseEvent e) {
177        oldEvent = e;
178        giveUserFeedback(e);
179    }
180
181    /**
182     * removes any highlighting that may have been set beforehand.
183     */
184    private void removeHighlighting() {
185        HIGHLIGHT_HELPER.clear();
186        DataSet ds = getLayerManager().getEditDataSet();
187        if (ds != null) {
188            ds.clearHighlightedWaySegments();
189        }
190    }
191
192    /**
193     * handles everything related to highlighting primitives and way
194     * segments for the given pointer position (via MouseEvent) and modifiers.
195     * @param e current mouse event
196     * @param modifiers extended mouse modifiers, not necessarly taken from the given mouse event
197     */
198    private void addHighlighting(MouseEvent e, int modifiers) {
199        if (!drawTargetHighlight)
200            return;
201
202        Set<OsmPrimitive> newHighlights = new HashSet<>();
203        DeleteParameters parameters = getDeleteParameters(e, modifiers);
204
205        if (parameters.mode == DeleteMode.segment) {
206            // deleting segments is the only action not working on OsmPrimitives
207            // so we have to handle them separately.
208            repaintIfRequired(newHighlights, parameters.nearestSegment);
209        } else {
210            // don't call buildDeleteCommands for DeleteMode.segment because it doesn't support
211            // silent operation and SplitWayAction will show dialogs. A lot.
212            Command delCmd = buildDeleteCommands(e, modifiers, true);
213            if (delCmd != null) {
214                // all other cases delete OsmPrimitives directly, so we can safely do the following
215                for (OsmPrimitive osm : delCmd.getParticipatingPrimitives()) {
216                    newHighlights.add(osm);
217                }
218            }
219            repaintIfRequired(newHighlights, null);
220        }
221    }
222
223    private void repaintIfRequired(Set<OsmPrimitive> newHighlights, WaySegment newHighlightedWaySegment) {
224        boolean needsRepaint = false;
225        OsmDataLayer editLayer = getLayerManager().getEditLayer();
226
227        if (newHighlightedWaySegment == null && oldHighlightedWaySegment != null) {
228            if (editLayer != null) {
229                editLayer.data.clearHighlightedWaySegments();
230                needsRepaint = true;
231            }
232            oldHighlightedWaySegment = null;
233        } else if (newHighlightedWaySegment != null && !newHighlightedWaySegment.equals(oldHighlightedWaySegment)) {
234            if (editLayer != null) {
235                editLayer.data.setHighlightedWaySegments(Collections.singleton(newHighlightedWaySegment));
236                needsRepaint = true;
237            }
238            oldHighlightedWaySegment = newHighlightedWaySegment;
239        }
240        needsRepaint |= HIGHLIGHT_HELPER.highlightOnly(newHighlights);
241        if (needsRepaint && editLayer != null) {
242            editLayer.invalidate();
243        }
244    }
245
246    /**
247     * This function handles all work related to updating the cursor and highlights
248     *
249     * @param e current mouse event
250     * @param modifiers extended mouse modifiers, not necessarly taken from the given mouse event
251     */
252    private void updateCursor(MouseEvent e, int modifiers) {
253        if (!MainApplication.isDisplayingMapView())
254            return;
255        MapFrame map = MainApplication.getMap();
256        if (!map.mapView.isActiveLayerVisible() || e == null)
257            return;
258
259        DeleteParameters parameters = getDeleteParameters(e, modifiers);
260        map.mapView.setNewCursor(parameters.mode.cursor(), this);
261    }
262
263    /**
264     * Gives the user feedback for the action he/she is about to do. Currently
265     * calls the cursor and target highlighting routines. Allows for modifiers
266     * not taken from the given mouse event.
267     *
268     * Normally the mouse event also contains the modifiers. However, when the
269     * mouse is not moved and only modifier keys are pressed, no mouse event
270     * occurs. We can use AWTEvent to catch those but still lack a proper
271     * mouseevent. Instead we copy the previous event and only update the modifiers.
272     * @param e mouse event
273     * @param modifiers mouse modifiers
274     */
275    private void giveUserFeedback(MouseEvent e, int modifiers) {
276        updateCursor(e, modifiers);
277        addHighlighting(e, modifiers);
278    }
279
280    /**
281     * Gives the user feedback for the action he/she is about to do. Currently
282     * calls the cursor and target highlighting routines. Extracts modifiers
283     * from mouse event.
284     * @param e mouse event
285     */
286    private void giveUserFeedback(MouseEvent e) {
287        giveUserFeedback(e, e.getModifiersEx());
288    }
289
290    /**
291     * If user clicked with the left button, delete the nearest object.
292     */
293    @Override
294    public void mouseReleased(MouseEvent e) {
295        if (e.getButton() != MouseEvent.BUTTON1)
296            return;
297        MapFrame map = MainApplication.getMap();
298        if (!map.mapView.isActiveLayerVisible())
299            return;
300
301        // request focus in order to enable the expected keyboard shortcuts
302        //
303        map.mapView.requestFocus();
304
305        Command c = buildDeleteCommands(e, e.getModifiersEx(), false);
306        if (c != null) {
307            UndoRedoHandler.getInstance().add(c);
308        }
309
310        getLayerManager().getEditDataSet().setSelected();
311        giveUserFeedback(e);
312    }
313
314    @Override
315    public String getModeHelpText() {
316        // CHECKSTYLE.OFF: LineLength
317        return tr("Click to delete. Shift: delete way segment. Alt: do not delete unused nodes when deleting a way. Ctrl: delete referring objects.");
318        // CHECKSTYLE.ON: LineLength
319    }
320
321    @Override
322    public boolean layerIsSupported(Layer l) {
323        return isEditableDataLayer(l);
324    }
325
326    @Override
327    protected void updateEnabledState() {
328        setEnabled(MainApplication.isDisplayingMapView() && MainApplication.getMap().mapView.isActiveLayerDrawable());
329    }
330
331    /**
332     * Deletes the relation in the context of the given layer.
333     *
334     * @param layer the layer in whose context the relation is deleted. Must not be null.
335     * @param toDelete  the relation to be deleted. Must not be null.
336     * @throws IllegalArgumentException if layer is null
337     * @throws IllegalArgumentException if toDelete is null
338     */
339    public static void deleteRelation(OsmDataLayer layer, Relation toDelete) {
340        deleteRelations(layer, Collections.singleton(toDelete));
341    }
342
343    /**
344     * Deletes the relations in the context of the given layer.
345     *
346     * @param layer the layer in whose context the relations are deleted. Must not be null.
347     * @param toDelete the relations to be deleted. Must not be null.
348     * @throws IllegalArgumentException if layer is null
349     * @throws IllegalArgumentException if toDelete is null
350     */
351    public static void deleteRelations(OsmDataLayer layer, Collection<Relation> toDelete) {
352        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
353        CheckParameterUtil.ensureParameterNotNull(toDelete, "toDelete");
354
355        final Command cmd = DeleteCommand.delete(toDelete);
356        if (cmd != null) {
357            // cmd can be null if the user cancels dialogs DialogCommand displays
358            UndoRedoHandler.getInstance().add(cmd);
359            for (Relation relation : toDelete) {
360                if (layer.data.getSelectedRelations().contains(relation)) {
361                    layer.data.toggleSelected(relation);
362                }
363                RelationDialogManager.getRelationDialogManager().close(layer, relation);
364            }
365        }
366    }
367
368    private DeleteParameters getDeleteParameters(MouseEvent e, int modifiers) {
369        updateKeyModifiersEx(modifiers);
370
371        DeleteParameters result = new DeleteParameters();
372
373        MapView mapView = MainApplication.getMap().mapView;
374        result.nearestNode = mapView.getNearestNode(e.getPoint(), OsmPrimitive::isSelectable);
375        if (result.nearestNode == null) {
376            result.nearestSegment = mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive::isSelectable);
377            if (result.nearestSegment != null) {
378                if (shift) {
379                    result.mode = DeleteMode.segment;
380                } else if (ctrl) {
381                    result.mode = DeleteMode.way_with_references;
382                } else {
383                    result.mode = alt ? DeleteMode.way : DeleteMode.way_with_nodes;
384                }
385            } else {
386                result.mode = DeleteMode.none;
387            }
388        } else if (ctrl) {
389            result.mode = DeleteMode.node_with_references;
390        } else {
391            result.mode = DeleteMode.node;
392        }
393
394        return result;
395    }
396
397    /**
398     * This function takes any mouse event argument and builds the list of elements
399     * that should be deleted but does not actually delete them.
400     * @param e MouseEvent from which modifiers and position are taken
401     * @param modifiers For explanation, see {@link #updateCursor}
402     * @param silent Set to true if the user should not be bugged with additional dialogs
403     * @return delete command
404     */
405    private Command buildDeleteCommands(MouseEvent e, int modifiers, boolean silent) {
406        DeleteParameters parameters = getDeleteParameters(e, modifiers);
407        switch (parameters.mode) {
408        case node:
409            return DeleteCommand.delete(Collections.singleton(parameters.nearestNode), false, silent);
410        case node_with_references:
411            return DeleteCommand.deleteWithReferences(Collections.singleton(parameters.nearestNode), silent);
412        case segment:
413            return DeleteCommand.deleteWaySegment(parameters.nearestSegment);
414        case way:
415            return DeleteCommand.delete(Collections.singleton(parameters.nearestSegment.way), false, silent);
416        case way_with_nodes:
417            return DeleteCommand.delete(Collections.singleton(parameters.nearestSegment.way), true, silent);
418        case way_with_references:
419            return DeleteCommand.deleteWithReferences(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}