source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java@ 1634

Last change on this file since 1634 was 1634, checked in by stoecker, 15 years ago

fix #2483 - patch by dmuecke - rotate does not work under Mac OS X

  • Property svn:eol-style set to native
File size: 19.2 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.Cursor;
8import java.awt.Point;
9import java.awt.Rectangle;
10import java.awt.event.ActionEvent;
11import java.awt.event.InputEvent;
12import java.awt.event.KeyEvent;
13import java.awt.event.MouseEvent;
14import java.util.ArrayList;
15import java.util.Collection;
16import java.util.Collections;
17import java.util.LinkedList;
18import java.util.List;
19
20import javax.swing.JOptionPane;
21
22import org.openstreetmap.josm.Main;
23import org.openstreetmap.josm.actions.MergeNodesAction;
24import org.openstreetmap.josm.command.AddCommand;
25import org.openstreetmap.josm.command.ChangeCommand;
26import org.openstreetmap.josm.command.Command;
27import org.openstreetmap.josm.command.MoveCommand;
28import org.openstreetmap.josm.command.RotateCommand;
29import org.openstreetmap.josm.command.SequenceCommand;
30import org.openstreetmap.josm.data.coor.EastNorth;
31import org.openstreetmap.josm.data.osm.DataSet;
32import org.openstreetmap.josm.data.osm.Node;
33import org.openstreetmap.josm.data.osm.OsmPrimitive;
34import org.openstreetmap.josm.data.osm.Way;
35import org.openstreetmap.josm.data.osm.WaySegment;
36import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor;
37import org.openstreetmap.josm.data.osm.visitor.SimplePaintVisitor;
38import org.openstreetmap.josm.gui.MapFrame;
39import org.openstreetmap.josm.gui.MapView;
40import org.openstreetmap.josm.gui.SelectionManager;
41import org.openstreetmap.josm.gui.SelectionManager.SelectionEnded;
42import org.openstreetmap.josm.gui.layer.Layer;
43import org.openstreetmap.josm.gui.layer.OsmDataLayer;
44import org.openstreetmap.josm.tools.ImageProvider;
45import org.openstreetmap.josm.tools.Shortcut;
46
47/**
48 * Move is an action that can move all kind of OsmPrimitives (except keys for now).
49 *
50 * If an selected object is under the mouse when dragging, move all selected objects.
51 * If an unselected object is under the mouse when dragging, it becomes selected
52 * and will be moved.
53 * If no object is under the mouse, move all selected objects (if any)
54 *
55 * @author imi
56 */
57public class SelectAction extends MapMode implements SelectionEnded {
58 public static boolean needMouseMove = false;
59 enum Mode { move, rotate, select }
60 private Mode mode = null;
61 private long mouseDownTime = 0;
62 private boolean didMove = false;
63 private boolean cancelDrawMode = false;
64 Node virtualNode = null;
65 Collection<WaySegment> virtualWays = new ArrayList<WaySegment>();
66 SequenceCommand virtualCmds = null;
67
68 /**
69 * The old cursor before the user pressed the mouse button.
70 */
71 private Cursor oldCursor;
72 /**
73 * The position of the mouse before the user moves a node.
74 */
75 private Point mousePos;
76 private SelectionManager selectionManager;
77
78 /**
79 * The time which needs to pass between click and release before something
80 * counts as a move, in milliseconds
81 */
82 private int initialMoveDelay;
83
84 /**
85 * The screen distance which needs to be travelled before something
86 * counts as a move, in pixels
87 */
88 private int initialMoveThreshold;
89 private boolean initialMoveThresholdExceeded = false;
90 /**
91 * Create a new SelectAction
92 * @param mapFrame The MapFrame this action belongs to.
93 */
94 public SelectAction(MapFrame mapFrame) {
95 super(tr("Select"), "move/move", tr("Select, move and rotate objects"),
96 Shortcut.registerShortcut("mapmode:select", tr("Mode: {0}", tr("Select")), KeyEvent.VK_S, Shortcut.GROUP_EDIT),
97 mapFrame,
98 getCursor("normal", "selection", Cursor.DEFAULT_CURSOR));
99 putValue("help", "Action/Move/Move");
100 selectionManager = new SelectionManager(this, false, mapFrame.mapView);
101 initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay",200);
102 initialMoveThreshold = Main.pref.getInteger("edit.initial-move-threshold",5);
103 }
104
105 private static Cursor getCursor(String name, String mod, int def) {
106 try {
107 return ImageProvider.getCursor(name, mod);
108 } catch (Exception e) {
109 }
110 return Cursor.getPredefinedCursor(def);
111 }
112
113 private void setCursor(Cursor c) {
114 if (oldCursor == null) {
115 oldCursor = Main.map.mapView.getCursor();
116 Main.map.mapView.setCursor(c);
117 }
118 }
119
120 private void restoreCursor() {
121 if (oldCursor != null) {
122 Main.map.mapView.setCursor(oldCursor);
123 oldCursor = null;
124 }
125 }
126
127 @Override public void enterMode() {
128 super.enterMode();
129 Main.map.mapView.addMouseListener(this);
130 Main.map.mapView.addMouseMotionListener(this);
131 Main.map.mapView.enableVirtualNodes(
132 Main.pref.getInteger("mappaint.node.virtual-size", 8) != 0);
133 }
134
135 @Override public void exitMode() {
136 super.exitMode();
137 selectionManager.unregister(Main.map.mapView);
138 Main.map.mapView.removeMouseListener(this);
139 Main.map.mapView.removeMouseMotionListener(this);
140 Main.map.mapView.enableVirtualNodes(false);
141 }
142
143 /**
144 * If the left mouse button is pressed, move all currently selected
145 * objects (if one of them is under the mouse) or the current one under the
146 * mouse (which will become selected).
147 */
148 @Override public void mouseDragged(MouseEvent e) {
149 if(!Main.map.mapView.isVisibleDrawableLayer())
150 return;
151
152 cancelDrawMode = true;
153 if (mode == Mode.select) return;
154
155 // do not count anything as a move if it lasts less than 100 milliseconds.
156 if ((mode == Mode.move) && (System.currentTimeMillis() - mouseDownTime < initialMoveDelay)) return;
157
158 if(mode != Mode.rotate) // button is pressed in rotate mode
159 if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0)
160 return;
161
162 if (mode == Mode.move)
163 setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
164
165 if (mousePos == null) {
166 mousePos = e.getPoint();
167 return;
168 }
169
170 if (!initialMoveThresholdExceeded) {
171 int dxp = mousePos.x - e.getX();
172 int dyp = mousePos.y - e.getY();
173 int dp = (int) Math.sqrt(dxp*dxp+dyp*dyp);
174 if (dp < initialMoveThreshold) return;
175 initialMoveThresholdExceeded = true;
176 }
177
178 EastNorth mouseEN = Main.map.mapView.getEastNorth(e.getX(), e.getY());
179 EastNorth mouseStartEN = Main.map.mapView.getEastNorth(mousePos.x, mousePos.y);
180 double dx = mouseEN.east() - mouseStartEN.east();
181 double dy = mouseEN.north() - mouseStartEN.north();
182 if (dx == 0 && dy == 0)
183 return;
184
185 if (virtualWays.size() > 0) {
186 Collection<Command> virtualCmds = new LinkedList<Command>();
187 virtualCmds.add(new AddCommand(virtualNode));
188 for(WaySegment virtualWay : virtualWays) {
189 Way w = virtualWay.way;
190 Way wnew = new Way(w);
191 wnew.addNode(virtualWay.lowerIndex+1, virtualNode);
192 virtualCmds.add(new ChangeCommand(w, wnew));
193 }
194 virtualCmds.add(new MoveCommand(virtualNode, dx, dy));
195 String text = trn("Add and move a virtual new node to way",
196 "Add and move a virtual new node to {0} ways", virtualWays.size(),
197 virtualWays.size());
198
199 Main.main.undoRedo.add(new SequenceCommand(text, virtualCmds));
200 selectPrims(Collections.singleton((OsmPrimitive)virtualNode), false, false, false, false);
201 virtualWays.clear();
202 virtualNode = null;
203 } else {
204 // Currently we support moving and rotating, which do not affect relations.
205 // So don't add them in the first place to make handling easier
206 Collection<OsmPrimitive> selection = Main.ds.getSelectedNodesAndWays();
207 Collection<Node> affectedNodes = AllNodesVisitor.getAllNodes(selection);
208
209 // when rotating, having only one node makes no sense - quit silently
210 if (mode == Mode.rotate && affectedNodes.size() < 2)
211 return;
212
213 Command c = !Main.main.undoRedo.commands.isEmpty()
214 ? Main.main.undoRedo.commands.getLast() : null;
215 if (c instanceof SequenceCommand)
216 c = ((SequenceCommand)c).getLastCommand();
217
218 if (mode == Mode.move) {
219 if (c instanceof MoveCommand && affectedNodes.equals(((MoveCommand)c).objects))
220 ((MoveCommand)c).moveAgain(dx,dy);
221 else
222 Main.main.undoRedo.add(
223 c = new MoveCommand(selection, dx, dy));
224
225 for (Node n : affectedNodes) {
226 if (n.coor.isOutSideWorld()) {
227 // Revert move
228 ((MoveCommand) c).moveAgain(-dx, -dy);
229
230 JOptionPane.showMessageDialog(Main.parent,
231 tr("Cannot move objects outside of the world."));
232 restoreCursor();
233 return;
234 }
235 }
236 } else if (mode == Mode.rotate) {
237 if (c instanceof RotateCommand && affectedNodes.equals(((RotateCommand)c).objects))
238 ((RotateCommand)c).rotateAgain(mouseStartEN, mouseEN);
239 else
240 Main.main.undoRedo.add(new RotateCommand(selection, mouseStartEN, mouseEN));
241 }
242 }
243
244 Main.map.mapView.repaint();
245 mousePos = e.getPoint();
246
247 didMove = true;
248 }
249
250 /**
251 * Mac OSX simulates with ctrl + mouse 1 the second mouse button hence no dragging events get fired.
252 *
253 */
254 @Override public void mouseMoved(MouseEvent e) {
255 if (needMouseMove && mode == Mode.rotate)
256 mouseDragged(e);
257 }
258
259 private Collection<OsmPrimitive> getNearestCollectionVirtual(Point p, boolean allSegements) {
260 MapView c = Main.map.mapView;
261 int snapDistance = Main.pref.getInteger("mappaint.node.virtual-snap-distance", 8);
262 snapDistance *= snapDistance;
263 OsmPrimitive osm = c.getNearestNode(p);
264 virtualWays.clear();
265 virtualNode = null;
266 Node virtualWayNode = null;
267
268 if (osm == null)
269 {
270 Collection<WaySegment> nearestWaySegs = allSegements
271 ? c.getNearestWaySegments(p)
272 : Collections.singleton(c.getNearestWaySegment(p));
273
274 for(WaySegment nearestWS : nearestWaySegs) {
275 if (nearestWS == null)
276 continue;
277
278 osm = nearestWS.way;
279 if(Main.pref.getInteger("mappaint.node.virtual-size", 8) > 0)
280 {
281 Way w = (Way)osm;
282 Point p1 = c.getPoint(w.nodes.get(nearestWS.lowerIndex).eastNorth);
283 Point p2 = c.getPoint(w.nodes.get(nearestWS.lowerIndex+1).eastNorth);
284 if(SimplePaintVisitor.isLargeSegment(p1, p2, Main.pref.getInteger("mappaint.node.virtual-space", 70)))
285 {
286 Point pc = new Point((p1.x+p2.x)/2, (p1.y+p2.y)/2);
287 if (p.distanceSq(pc) < snapDistance)
288 {
289 // Check that only segments on top of each other get added to the
290 // virtual ways list. Otherwise ways that coincidentally have their
291 // virtual node at the same spot will be joined which is likely unwanted
292 if(virtualWayNode != null) {
293 if( !w.nodes.get(nearestWS.lowerIndex+1).equals(virtualWayNode)
294 && !w.nodes.get(nearestWS.lowerIndex ).equals(virtualWayNode))
295 continue;
296 } else
297 virtualWayNode = w.nodes.get(nearestWS.lowerIndex+1);
298
299 virtualWays.add(nearestWS);
300 if(virtualNode == null)
301 virtualNode = new Node(Main.map.mapView.getLatLon(pc.x, pc.y));
302 }
303 }
304 }
305 }
306 }
307 if (osm == null)
308 return Collections.emptySet();
309 return Collections.singleton(osm);
310 }
311
312 /**
313 * Look, whether any object is selected. If not, select the nearest node.
314 * If there are no nodes in the dataset, do nothing.
315 *
316 * If the user did not press the left mouse button, do nothing.
317 *
318 * Also remember the starting position of the movement and change the mouse
319 * cursor to movement.
320 */
321 @Override public void mousePressed(MouseEvent e) {
322 if(!Main.map.mapView.isVisibleDrawableLayer())
323 return;
324
325 cancelDrawMode = false;
326 if (! (Boolean)this.getValue("active")) return;
327 if (e.getButton() != MouseEvent.BUTTON1)
328 return;
329 boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
330 boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
331 boolean shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
332
333 // We don't want to change to draw tool if the user tries to (de)select
334 // stuff but accidentally clicks in an empty area when selection is empty
335 if(shift || ctrl)
336 cancelDrawMode = true;
337
338 mouseDownTime = System.currentTimeMillis();
339 didMove = false;
340 initialMoveThresholdExceeded = false;
341
342 Collection<OsmPrimitive> osmColl = getNearestCollectionVirtual(e.getPoint(), alt);
343
344 if (ctrl && shift) {
345 if (Main.ds.getSelected().isEmpty()) selectPrims(osmColl, true, false, false, false);
346 mode = Mode.rotate;
347 setCursor(ImageProvider.getCursor("rotate", null));
348 } else if (!osmColl.isEmpty()) {
349 // Don't replace the selection now if the user clicked on a
350 // selected object (this would break moving of selected groups).
351 // We'll do that later in mouseReleased if the user didn't try to
352 // move.
353 selectPrims(osmColl,
354 shift || Main.ds.getSelected().containsAll(osmColl),
355 ctrl, false, false);
356 mode = Mode.move;
357 } else {
358 mode = Mode.select;
359 oldCursor = Main.map.mapView.getCursor();
360 selectionManager.register(Main.map.mapView);
361 selectionManager.mousePressed(e);
362 }
363 if(mode != Mode.move || shift || ctrl)
364 {
365 virtualNode = null;
366 virtualWays.clear();
367 }
368
369 updateStatusLine();
370 // Mode.select redraws when selectPrims is called
371 // Mode.move redraws when mouseDragged is called
372 // Mode.rotate redraws here
373 if(mode == Mode.rotate)
374 Main.map.mapView.repaint();
375
376 mousePos = e.getPoint();
377 }
378
379 /**
380 * Restore the old mouse cursor.
381 */
382 @Override public void mouseReleased(MouseEvent e) {
383 if(!Main.map.mapView.isVisibleDrawableLayer())
384 return;
385
386 if (mode == Mode.select) {
387 selectionManager.unregister(Main.map.mapView);
388
389 // Select Draw Tool if no selection has been made
390 if(Main.ds.getSelected().size() == 0 && !cancelDrawMode) {
391 Main.map.selectDrawTool(true);
392 return;
393 }
394 }
395 restoreCursor();
396
397 if (mode == Mode.move) {
398 boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
399 boolean shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
400 if (!didMove) {
401 selectPrims(
402 Main.map.mapView.getNearestCollection(e.getPoint()),
403 shift, ctrl, true, false);
404
405 // If the user double-clicked a node, change to draw mode
406 List<OsmPrimitive> sel = new ArrayList<OsmPrimitive>(Main.ds.getSelected());
407 if(e.getClickCount() >=2 && sel.size() == 1 && sel.get(0) instanceof Node) {
408 // We need to do it like this as otherwise drawAction will see a double
409 // click and switch back to SelectMode
410 Main.worker.execute(new Runnable(){
411 public void run() {
412 Main.map.selectDrawTool(true);
413 }
414 });
415 return;
416 }
417 } else {
418 Collection<OsmPrimitive> selection = Main.ds.getSelected();
419 if (ctrl) {
420 Collection<Node> affectedNodes = AllNodesVisitor.getAllNodes(selection);
421 Collection<Node> nn = Main.map.mapView.getNearestNodes(e.getPoint(), affectedNodes);
422 if (nn != null) {
423 Node n = nn.iterator().next();
424 LinkedList<Node> selNodes = new LinkedList<Node>();
425 for (OsmPrimitive osm : selection)
426 if (osm instanceof Node)
427 selNodes.add((Node)osm);
428 if (selNodes.size() > 0) {
429 selNodes.add(n);
430 MergeNodesAction.mergeNodes(selNodes, n);
431 }
432 }
433 }
434 DataSet.fireSelectionChanged(selection);
435 }
436 }
437
438 // I don't see why we need this.
439 //updateStatusLine();
440 mode = null;
441 updateStatusLine();
442 }
443
444 public void selectionEnded(Rectangle r, boolean alt, boolean shift, boolean ctrl) {
445 selectPrims(selectionManager.getObjectsInRectangle(r, alt), shift, ctrl, true, true);
446 }
447
448 public void selectPrims(Collection<OsmPrimitive> selectionList, boolean shift,
449 boolean ctrl, boolean released, boolean area) {
450 if ((shift && ctrl) || (ctrl && !released))
451 return; // not allowed together
452
453 Collection<OsmPrimitive> curSel;
454 if (!ctrl && !shift)
455 curSel = new LinkedList<OsmPrimitive>(); // new selection will replace the old.
456 else
457 curSel = Main.ds.getSelected();
458
459 for (OsmPrimitive osm : selectionList)
460 {
461 if (ctrl)
462 {
463 if(curSel.contains(osm))
464 curSel.remove(osm);
465 else if(!area)
466 curSel.add(osm);
467 }
468 else
469 curSel.add(osm);
470 }
471 Main.ds.setSelected(curSel);
472 Main.map.mapView.repaint();
473 }
474
475 @Override public String getModeHelpText() {
476 if (mode == Mode.select) {
477 return tr("Release the mouse button to select the objects in the rectangle.");
478 } else if (mode == Mode.move) {
479 return tr("Release the mouse button to stop moving. Ctrl to merge with nearest node.");
480 } else if (mode == Mode.rotate) {
481 return tr("Release the mouse button to stop rotating.");
482 } else {
483 return tr("Move objects by dragging; Shift to add to selection (Ctrl to toggle); Shift-Ctrl to rotate selected; or change selection");
484 }
485 }
486
487 @Override public boolean layerIsSupported(Layer l) {
488 return l instanceof OsmDataLayer;
489 }
490}
Note: See TracBrowser for help on using the repository browser.