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