// License: GPL. Copyright 2007 by Immanuel Scholz and others package org.openstreetmap.josm.actions.mapmode; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.AWTEvent; import java.awt.Cursor; import java.awt.EventQueue; import java.awt.Toolkit; import java.awt.event.AWTEventListener; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.Collections; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.DeleteCommand; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.WaySegment; import org.openstreetmap.josm.gui.MapFrame; import org.openstreetmap.josm.gui.dialogs.relation.RelationDialogManager; import org.openstreetmap.josm.gui.layer.Layer; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.tools.CheckParameterUtil; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.Shortcut; /** * An action that enables the user to delete nodes and other objects. * * The user can click on an object, which gets deleted if possible. When Ctrl is * pressed when releasing the button, the objects and all its references are * deleted. * * If the user did not press Ctrl and the object has any references, the user * is informed and nothing is deleted. * * If the user enters the mapmode and any object is selected, all selected * objects that can be deleted will. * * @author imi */ /** * This class contains stubs for highlighting affected primitives when affected. * However, way segments can be deleted as well, but cannot be highlighted * alone. If the highlight feature for this delete action is to be implemented * properly, highlighting way segments must be possible first. --xeen, 2009-09-02 */ public class DeleteAction extends MapMode implements AWTEventListener { //private boolean drawTargetHighlight; private boolean drawTargetCursor; //private Collection<? extends OsmPrimitive> oldPrims = null; // Cache previous mouse event (needed when only the modifier keys are // pressed but the mouse isn't moved) private MouseEvent oldEvent = null; private enum DeleteMode { none("delete"), segment("delete_segment"), node("delete_node"), node_with_references("delete_node"), way("delete_way_only"), way_with_references("delete_way_normal"), way_with_nodes("delete_way_node_only"); private final Cursor c; private DeleteMode(String cursorName) { c = ImageProvider.getCursor("normal", cursorName); } public Cursor cursor() { return c; } } private DeleteMode currentMode = DeleteMode.none; private static class DeleteParameters { DeleteMode mode; Node nearestNode; WaySegment nearestSegment; } /** * Construct a new DeleteAction. Mnemonic is the delete - key. * @param mapFrame The frame this action belongs to. */ public DeleteAction(MapFrame mapFrame) { super(tr("Delete Mode"), "delete", tr("Delete nodes or ways."), Shortcut.registerShortcut("mapmode:delete", tr("Mode: {0}",tr("Delete")), KeyEvent.VK_D, Shortcut.GROUP_EDIT), mapFrame, ImageProvider.getCursor("normal", "delete")); } @Override public void enterMode() { super.enterMode(); if (!isEnabled()) return; //drawTargetHighlight = Main.pref.getBoolean("draw.target-highlight", true); drawTargetCursor = Main.pref.getBoolean("draw.target-cursor", true); Main.map.mapView.addMouseListener(this); Main.map.mapView.addMouseMotionListener(this); // This is required to update the cursors when ctrl/shift/alt is pressed try { Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK); } catch (SecurityException ex) { System.out.println(ex); } currentMode = DeleteMode.none; } @Override public void exitMode() { super.exitMode(); Main.map.mapView.removeMouseListener(this); Main.map.mapView.removeMouseMotionListener(this); try { Toolkit.getDefaultToolkit().removeAWTEventListener(this); } catch (SecurityException ex) { System.out.println(ex); } } @Override public void actionPerformed(ActionEvent e) { super.actionPerformed(e); if(!Main.map.mapView.isActiveLayerDrawable()) return; doActionPerformed(e); } public void doActionPerformed(ActionEvent e) { if(!Main.map.mapView.isActiveLayerDrawable()) return; boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0; boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0; Command c; if (ctrl) { c = DeleteCommand.deleteWithReferences(getEditLayer(),getCurrentDataSet().getSelected()); } else { c = DeleteCommand.delete(getEditLayer(),getCurrentDataSet().getSelected(), !alt /* also delete nodes in way */); } if (c != null) { Main.main.undoRedo.add(c); } getCurrentDataSet().setSelected(); Main.map.repaint(); } @Override public void mouseDragged(MouseEvent e) { mouseMoved(e); } /** * Listen to mouse move to be able to update the cursor (and highlights) * @param MouseEvent The mouse event that has been captured */ @Override public void mouseMoved(MouseEvent e) { oldEvent = e; updateCursor(e, e.getModifiers()); } /** * This function handles all work related to updating the cursor and * highlights. For now, only the cursor is enabled because highlighting * requires WaySegment to be highlightable. * * Normally the mouse event also contains the modifiers. However, when the * mouse is not moved and only modifier keys are pressed, no mouse event * occurs. We can use AWTEvent to catch those but still lack a proper * mouseevent. Instead we copy the previous event and only update the * modifiers. * * @param MouseEvent * @param int modifiers */ private void updateCursor(MouseEvent e, int modifiers) { if (!Main.isDisplayingMapView()) return; if(!Main.map.mapView.isActiveLayerVisible() || e == null) return; // Clean old highlights //cleanOldHighlights(); DeleteParameters parameters = getDeleteParameters(e, modifiers); setCursor(parameters.mode); // Needs to implement WaySegment highlight first /*if(drawTargetHighlight) { // Add new highlights for(OsmPrimitive p : prims) { p.highlighted = true; } oldPrims = prims; }*/ // We only need to repaint if the highlights changed //Main.map.mapView.repaint(); } /** * Small helper function that cleans old highlights */ /*private void cleanOldHighlights() { if(oldPrims == null) return; for(OsmPrimitive p: oldPrims) { p.highlighted = false; } }*/ /** * If user clicked with the left button, delete the nearest object. * position. */ @Override public void mouseReleased(MouseEvent e) { if (e.getButton() != MouseEvent.BUTTON1) return; if(!Main.map.mapView.isActiveLayerVisible()) return; // request focus in order to enable the expected keyboard shortcuts // Main.map.mapView.requestFocus(); Command c = buildDeleteCommands(e, e.getModifiers(), false); if (c != null) { Main.main.undoRedo.add(c); } getCurrentDataSet().setSelected(); Main.map.mapView.repaint(); } @Override public String getModeHelpText() { return tr("Click to delete. Shift: delete way segment. Alt: do not delete unused nodes when deleting a way. Ctrl: delete referring objects."); } @Override public boolean layerIsSupported(Layer l) { return l instanceof OsmDataLayer; } @Override protected void updateEnabledState() { setEnabled(Main.map != null && Main.map.mapView != null && Main.map.mapView.isActiveLayerDrawable()); } /** * Deletes the relation in the context of the given layer. Also notifies * {@see RelationDialogManager} and {@see OsmDataLayer#fireDataChange()} events. * * @param layer the layer in whose context the relation is deleted. Must not be null. * @param toDelete the relation to be deleted. Must not be null. * @exception IllegalArgumentException thrown if layer is null * @exception IllegalArgumentException thrown if toDelete is nul */ public static void deleteRelation(OsmDataLayer layer, Relation toDelete) { CheckParameterUtil.ensureParameterNotNull(layer, "layer"); CheckParameterUtil.ensureParameterNotNull(toDelete, "toDelete"); Command cmd = DeleteCommand.delete(layer, Collections.singleton(toDelete)); if (cmd != null) { // cmd can be null if the user cancels dialogs DialogCommand displays Main.main.undoRedo.add(cmd); RelationDialogManager.getRelationDialogManager().close(layer, toDelete); } } private DeleteParameters getDeleteParameters(MouseEvent e, int modifiers) { // Note: CTRL is the only modifier that is checked in MouseMove, don't // forget updating it there boolean ctrl = (modifiers & ActionEvent.CTRL_MASK) != 0; boolean shift = (modifiers & ActionEvent.SHIFT_MASK) != 0; boolean alt = (modifiers & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0; DeleteParameters result = new DeleteParameters(); result.nearestNode = Main.map.mapView.getNearestNode(e.getPoint(), OsmPrimitive.isSelectablePredicate); if (result.nearestNode == null) { result.nearestSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate); if (result.nearestSegment != null) { if (shift) { result.mode = DeleteMode.segment; } else if (ctrl) { result.mode = DeleteMode.way_with_references; } else { result.mode = alt?DeleteMode.way:DeleteMode.way_with_nodes; } } else { result.mode = DeleteMode.none; } } else if (ctrl) { result.mode = DeleteMode.node_with_references; } else { result.mode = DeleteMode.node; } return result; } /** * This function takes any mouse event argument and builds the list of elements * that should be deleted but does not actually delete them. * @param e MouseEvent from which modifiers and position are taken * @param int modifiers For explanation: @see updateCursor * @param silet Set to true if the user should not be bugged with additional * dialogs * @return */ private Command buildDeleteCommands(MouseEvent e, int modifiers, boolean silent) { DeleteParameters parameters = getDeleteParameters(e, modifiers); switch (parameters.mode) { case node: return DeleteCommand.delete(getEditLayer(),Collections.singleton(parameters.nearestNode), false, silent); case node_with_references: return DeleteCommand.deleteWithReferences(getEditLayer(),Collections.singleton(parameters.nearestNode)); case segment: return DeleteCommand.deleteWaySegment(getEditLayer(), parameters.nearestSegment); case way: return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), false, silent); case way_with_nodes: return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true, silent); case way_with_references: return DeleteCommand.deleteWithReferences(getEditLayer(),Collections.singleton(parameters.nearestSegment.way),true); default: return null; } } /** * This function sets the given cursor in a safe way. This implementation * differs from the on in DrawAction (it is favorable, too). * FIXME: Update DrawAction to use this "setCursor-style" and move function * to MapMode. * @param c */ private void setCursor(final DeleteMode c) { if(currentMode.equals(c) || (!drawTargetCursor && currentMode.equals(DeleteMode.none))) return; // We invoke this to prevent strange things from happening EventQueue.invokeLater(new Runnable() { public void run() { // Don't change cursor when mode has changed already if(!(Main.map.mapMode instanceof DeleteAction)) return; Main.map.mapView.setCursor(c.cursor()); //System.out.println("Set cursor to: " + c.name()); } }); currentMode = c; } /** * This is required to update the cursors when ctrl/shift/alt is pressed */ public void eventDispatched(AWTEvent e) { // We don't have a mouse event, so we pass the old mouse event but the // new modifiers. updateCursor(oldEvent, ((InputEvent)e).getModifiers()); } }