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

Last change on this file since 5174 was 5174, checked in by akks, 12 years ago

should fix #7580 - NPE on Alt-F4 (while in select-moving mode)

  • Property svn:eol-style set to native
File size: 38.5 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.AWTEvent;
9import java.awt.Cursor;
10import java.awt.Point;
11import java.awt.Rectangle;
12import java.awt.Toolkit;
13import java.awt.event.AWTEventListener;
14import java.awt.event.InputEvent;
15import java.awt.event.KeyEvent;
16import java.awt.event.MouseEvent;
17import java.awt.geom.Point2D;
18import java.util.Collection;
19import java.util.Collections;
20import java.util.HashSet;
21import java.util.Iterator;
22import java.util.LinkedList;
23import java.util.Set;
24
25import javax.swing.JOptionPane;
26
27import org.openstreetmap.josm.Main;
28import org.openstreetmap.josm.actions.MergeNodesAction;
29import org.openstreetmap.josm.command.AddCommand;
30import org.openstreetmap.josm.command.ChangeCommand;
31import org.openstreetmap.josm.command.Command;
32import org.openstreetmap.josm.command.MoveCommand;
33import org.openstreetmap.josm.command.RotateCommand;
34import org.openstreetmap.josm.command.ScaleCommand;
35import org.openstreetmap.josm.command.SequenceCommand;
36import org.openstreetmap.josm.data.coor.EastNorth;
37import org.openstreetmap.josm.data.osm.DataSet;
38import org.openstreetmap.josm.data.osm.Node;
39import org.openstreetmap.josm.data.osm.OsmPrimitive;
40import org.openstreetmap.josm.data.osm.Way;
41import org.openstreetmap.josm.data.osm.WaySegment;
42import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor;
43import org.openstreetmap.josm.data.osm.visitor.paint.WireframeMapRenderer;
44import org.openstreetmap.josm.gui.ExtendedDialog;
45import org.openstreetmap.josm.gui.MapFrame;
46import org.openstreetmap.josm.gui.MapView;
47import org.openstreetmap.josm.gui.SelectionManager;
48import org.openstreetmap.josm.gui.SelectionManager.SelectionEnded;
49import org.openstreetmap.josm.gui.layer.Layer;
50import org.openstreetmap.josm.gui.layer.OsmDataLayer;
51import org.openstreetmap.josm.tools.ImageProvider;
52import org.openstreetmap.josm.tools.Pair;
53import org.openstreetmap.josm.tools.PlatformHookOsx;
54import org.openstreetmap.josm.tools.Shortcut;
55
56/**
57 * Move is an action that can move all kind of OsmPrimitives (except keys for now).
58 *
59 * If an selected object is under the mouse when dragging, move all selected objects.
60 * If an unselected object is under the mouse when dragging, it becomes selected
61 * and will be moved.
62 * If no object is under the mouse, move all selected objects (if any)
63 *
64 * @author imi
65 */
66public class SelectAction extends MapMode implements AWTEventListener, SelectionEnded {
67 // "select" means the selection rectangle and "move" means either dragging
68 // or select if no mouse movement occurs (i.e. just clicking)
69 enum Mode { move, rotate, scale, select }
70
71 // contains all possible cases the cursor can be in the SelectAction
72 static private enum SelectActionCursor {
73 rect("normal", "selection"),
74 rect_add("normal", "select_add"),
75 rect_rm("normal", "select_remove"),
76 way("normal", "select_way"),
77 way_add("normal", "select_way_add"),
78 way_rm("normal", "select_way_remove"),
79 node("normal", "select_node"),
80 node_add("normal", "select_node_add"),
81 node_rm("normal", "select_node_remove"),
82 virtual_node("normal", "addnode"),
83 scale("scale", null),
84 rotate("rotate", null),
85 merge("crosshair", null),
86 lasso("normal", "rope"),
87 merge_to_node("crosshair", "joinnode"),
88 move(Cursor.MOVE_CURSOR);
89
90 private final Cursor c;
91 private SelectActionCursor(String main, String sub) {
92 c = ImageProvider.getCursor(main, sub);
93 }
94 private SelectActionCursor(int systemCursor) {
95 c = Cursor.getPredefinedCursor(systemCursor);
96 }
97 public Cursor cursor() {
98 return c;
99 }
100 }
101
102 private boolean lassoMode = false;
103
104 // Cache previous mouse event (needed when only the modifier keys are
105 // pressed but the mouse isn't moved)
106 private MouseEvent oldEvent = null;
107
108 private Mode mode = null;
109 private SelectionManager selectionManager;
110 private boolean cancelDrawMode = false;
111 private boolean drawTargetHighlight;
112 private boolean didMouseDrag = false;
113 /**
114 * The component this SelectAction is associated with.
115 */
116 private final MapView mv;
117 /**
118 * The old cursor before the user pressed the mouse button.
119 */
120 private Point startingDraggingPos;
121 /**
122 * The last known position of the mouse.
123 */
124 private Point lastMousePos;
125 /**
126 * The time of the user mouse down event.
127 */
128 private long mouseDownTime = 0;
129 /**
130 * The pressed button of the user mouse down event.
131 */
132 private int mouseDownButton = 0;
133 /**
134 * The time of the user mouse down event.
135 */
136 private long mouseReleaseTime = 0;
137 /**
138 * The time which needs to pass between click and release before something
139 * counts as a move, in milliseconds
140 */
141 private int initialMoveDelay;
142 /**
143 * The screen distance which needs to be travelled before something
144 * counts as a move, in pixels
145 */
146 private int initialMoveThreshold;
147 private boolean initialMoveThresholdExceeded = false;
148
149 /**
150 * elements that have been highlighted in the previous iteration. Used
151 * to remove the highlight from them again as otherwise the whole data
152 * set would have to be checked.
153 */
154 private Set<OsmPrimitive> oldHighlights = new HashSet<OsmPrimitive>();
155
156 /**
157 * Create a new SelectAction
158 * @param mapFrame The MapFrame this action belongs to.
159 */
160 public SelectAction(MapFrame mapFrame) {
161 super(tr("Select"), "move/move", tr("Select, move, scale and rotate objects"),
162 Shortcut.registerShortcut("mapmode:select", tr("Mode: {0}", tr("Select")), KeyEvent.VK_S, Shortcut.DIRECT),
163 mapFrame,
164 ImageProvider.getCursor("normal", "selection"));
165 mv = mapFrame.mapView;
166 putValue("help", ht("/Action/Select"));
167 selectionManager = new SelectionManager(this, false, mv);
168 initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay", 200);
169 initialMoveThreshold = Main.pref.getInteger("edit.initial-move-threshold", 5);
170 }
171
172 @Override
173 public void enterMode() {
174 super.enterMode();
175 mv.addMouseListener(this);
176 mv.addMouseMotionListener(this);
177 mv.setVirtualNodesEnabled(Main.pref.getInteger("mappaint.node.virtual-size", 8) != 0);
178 drawTargetHighlight = Main.pref.getBoolean("draw.target-highlight", true);
179 // This is required to update the cursors when ctrl/shift/alt is pressed
180 try {
181 Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
182 } catch (SecurityException ex) {}
183 }
184
185 @Override
186 public void exitMode() {
187 super.exitMode();
188 selectionManager.unregister(mv);
189 mv.removeMouseListener(this);
190 mv.removeMouseMotionListener(this);
191 mv.setVirtualNodesEnabled(false);
192 try {
193 Toolkit.getDefaultToolkit().removeAWTEventListener(this);
194 } catch (SecurityException ex) {}
195 removeHighlighting();
196 }
197
198 /**
199 * works out which cursor should be displayed for most of SelectAction's
200 * features. The only exception is the "move" cursor when actually dragging
201 * primitives.
202 * @param nearbyStuff primitives near the cursor
203 * @return the cursor that should be displayed
204 */
205 private Cursor getCursor(Collection<OsmPrimitive> nearbyStuff) {
206 String c = "rect";
207 switch(mode) {
208 case move:
209 if(virtualNode != null) {
210 c = "virtual_node";
211 break;
212 }
213 final Iterator<OsmPrimitive> it = nearbyStuff.iterator();
214 final OsmPrimitive osm = it.hasNext() ? it.next() : null;
215
216 if(dragInProgress()) {
217 // only consider merge if ctrl is pressed and there are nodes in
218 // the selection that could be merged
219 if(!ctrl || getCurrentDataSet().getSelectedNodes().isEmpty()) {
220 c = "move";
221 break;
222 }
223 // only show merge to node cursor if nearby node and that node is currently
224 // not being dragged
225 final boolean hasTarget = osm != null && osm instanceof Node && !osm.isSelected();
226 c = hasTarget ? "merge_to_node" : "merge";
227 break;
228 }
229
230 c = (osm instanceof Node) ? "node" : c;
231 c = (osm instanceof Way) ? "way" : c;
232 if(shift) {
233 c += "_add";
234 } else if(ctrl) {
235 c += osm == null || osm.isSelected() ? "_rm" : "_add";
236 }
237 break;
238 case rotate:
239 c = "rotate";
240 break;
241 case scale:
242 c = "scale";
243 break;
244 case select:
245 if (lassoMode) {
246 c = "lasso";
247 } else {
248 c = "rect" + (shift ? "_add" : (ctrl ? "_rm" : ""));
249 }
250 break;
251 }
252 return SelectActionCursor.valueOf(c).cursor();
253 }
254
255 /**
256 * Removes all existing highlights.
257 * @return true if a repaint is required
258 */
259 private boolean removeHighlighting() {
260 boolean needsRepaint = false;
261 DataSet ds = getCurrentDataSet();
262 if(ds != null && !ds.getHighlightedVirtualNodes().isEmpty()) {
263 needsRepaint = true;
264 ds.clearHighlightedVirtualNodes();
265 }
266 if(oldHighlights.isEmpty())
267 return needsRepaint;
268
269 for(OsmPrimitive prim : oldHighlights) {
270 prim.setHighlighted(false);
271 }
272 oldHighlights = new HashSet<OsmPrimitive>();
273 return true;
274 }
275
276 private boolean repaintIfRequired(HashSet<OsmPrimitive> newHighlights) {
277 if(!drawTargetHighlight)
278 return false;
279
280 boolean needsRepaint = false;
281 for(OsmPrimitive x : newHighlights) {
282 if(oldHighlights.contains(x)) {
283 continue;
284 }
285 needsRepaint = true;
286 x.setHighlighted(true);
287 }
288 oldHighlights.removeAll(newHighlights);
289 for(OsmPrimitive x : oldHighlights) {
290 x.setHighlighted(false);
291 needsRepaint = true;
292 }
293 oldHighlights = newHighlights;
294 return needsRepaint;
295 }
296
297 /**
298 * handles adding highlights and updating the cursor for the given mouse event.
299 * Please note that the highlighting for merging while moving is handled via mouseDragged.
300 * @param MouseEvent which should be used as base for the feedback
301 * @return true if repaint is required
302 */
303 private boolean giveUserFeedback(MouseEvent e) {
304 return giveUserFeedback(e, e.getModifiers());
305 }
306
307 /**
308 * handles adding highlights and updating the cursor for the given mouse event.
309 * Please note that the highlighting for merging while moving is handled via mouseDragged.
310 * @param MouseEvent which should be used as base for the feedback
311 * @param define custom keyboard modifiers if the ones from MouseEvent are outdated or similar
312 * @return true if repaint is required
313 */
314 private boolean giveUserFeedback(MouseEvent e, int modifiers) {
315 Collection<OsmPrimitive> c = MapView.asColl(
316 mv.getNearestNodeOrWay(e.getPoint(), OsmPrimitive.isSelectablePredicate, true));
317
318 updateKeyModifiers(modifiers);
319 determineMapMode(!c.isEmpty());
320
321 HashSet<OsmPrimitive> newHighlights = new HashSet<OsmPrimitive>();
322
323 virtualWays.clear();
324 virtualNode = null;
325 if(mode == Mode.move && setupVirtual(e)) {
326 DataSet ds = getCurrentDataSet();
327 if (ds != null && drawTargetHighlight) {
328 ds.setHighlightedVirtualNodes(virtualWays);
329 }
330 mv.setNewCursor(SelectActionCursor.virtual_node.cursor(), this);
331 // don't highlight anything else if a virtual node will be
332 return repaintIfRequired(newHighlights);
333 }
334
335 mv.setNewCursor(getCursor(c), this);
336
337 // return early if there can't be any highlights
338 if(!drawTargetHighlight || mode != Mode.move || c.isEmpty())
339 return repaintIfRequired(newHighlights);
340
341 // CTRL toggles selection, but if while dragging CTRL means merge
342 final boolean isToggleMode = ctrl && !dragInProgress();
343 for(OsmPrimitive x : c) {
344 // only highlight primitives that will change the selection
345 // when clicked. I.e. don't highlight selected elements unless
346 // we are in toggle mode.
347 if(isToggleMode || !x.isSelected()) {
348 newHighlights.add(x);
349 }
350 }
351 return repaintIfRequired(newHighlights);
352 }
353
354 /**
355 * This is called whenever the keyboard modifier status changes
356 */
357 public void eventDispatched(AWTEvent e) {
358 if(oldEvent == null)
359 return;
360 // We don't have a mouse event, so we pass the old mouse event but the
361 // new modifiers.
362 if(giveUserFeedback(oldEvent, ((InputEvent) e).getModifiers())) {
363 mv.repaint();
364 }
365 }
366
367 /**
368 * If the left mouse button is pressed, move all currently selected
369 * objects (if one of them is under the mouse) or the current one under the
370 * mouse (which will become selected).
371 */
372 @Override
373 public void mouseDragged(MouseEvent e) {
374 if (!mv.isActiveLayerVisible())
375 return;
376
377 // Swing sends random mouseDragged events when closing dialogs by double-clicking their top-left icon on Windows
378 // Ignore such false events to prevent issues like #7078
379 if (mouseDownButton == MouseEvent.BUTTON1 && mouseReleaseTime > mouseDownTime)
380 return;
381
382 cancelDrawMode = true;
383 if (mode == Mode.select)
384 return;
385
386 // do not count anything as a move if it lasts less than 100 milliseconds.
387 if ((mode == Mode.move) && (System.currentTimeMillis() - mouseDownTime < initialMoveDelay))
388 return;
389
390 if (mode != Mode.rotate && mode != Mode.scale) // button is pressed in rotate mode
391 {
392 if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0)
393 return;
394 }
395
396 if (mode == Mode.move) {
397 // If ctrl is pressed we are in merge mode. Look for a nearby node,
398 // highlight it and adjust the cursor accordingly.
399 final boolean canMerge = ctrl && !getCurrentDataSet().getSelectedNodes().isEmpty();
400 final OsmPrimitive p = canMerge ? (OsmPrimitive)findNodeToMergeTo(e) : null;
401 boolean needsRepaint = removeHighlighting();
402 if(p != null) {
403 p.setHighlighted(true);
404 oldHighlights.add(p);
405 needsRepaint = true;
406 }
407 mv.setNewCursor(getCursor(MapView.asColl(p)), this);
408 // also update the stored mouse event, so we can display the correct cursor
409 // when dragging a node onto another one and then press CTRL to merge
410 oldEvent = e;
411 if(needsRepaint) {
412 mv.repaint();
413 }
414 }
415
416 if (startingDraggingPos == null) {
417 startingDraggingPos = new Point(e.getX(), e.getY());
418 }
419
420 if( lastMousePos == null ) {
421 lastMousePos = e.getPoint();
422 return;
423 }
424
425 if (!initialMoveThresholdExceeded) {
426 int dxp = lastMousePos.x - e.getX();
427 int dyp = lastMousePos.y - e.getY();
428 int dp = (int) Math.sqrt(dxp * dxp + dyp * dyp);
429 if (dp < initialMoveThreshold)
430 return;
431 initialMoveThresholdExceeded = true;
432 }
433
434 EastNorth currentEN = mv.getEastNorth(e.getX(), e.getY());
435 EastNorth lastEN = mv.getEastNorth(lastMousePos.x, lastMousePos.y);
436 //EastNorth startEN = mv.getEastNorth(startingDraggingPos.x, startingDraggingPos.y);
437 double dx = currentEN.east() - lastEN.east();
438 double dy = currentEN.north() - lastEN.north();
439 if (dx == 0 && dy == 0)
440 return;
441
442 if (virtualWays.size() > 0) {
443 Collection<Command> virtualCmds = new LinkedList<Command>();
444 virtualCmds.add(new AddCommand(virtualNode));
445 for (WaySegment virtualWay : virtualWays) {
446 Way w = virtualWay.way;
447 Way wnew = new Way(w);
448 wnew.addNode(virtualWay.lowerIndex + 1, virtualNode);
449 virtualCmds.add(new ChangeCommand(w, wnew));
450 }
451 virtualCmds.add(new MoveCommand(virtualNode, dx, dy));
452 String text = trn("Add and move a virtual new node to way",
453 "Add and move a virtual new node to {0} ways", virtualWays.size(),
454 virtualWays.size());
455 Main.main.undoRedo.add(new SequenceCommand(text, virtualCmds));
456 getCurrentDataSet().setSelected(Collections.singleton((OsmPrimitive) virtualNode));
457 virtualWays.clear();
458 virtualNode = null;
459 } else {
460 // Currently we support only transformations which do not affect relations.
461 // So don't add them in the first place to make handling easier
462 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelectedNodesAndWays();
463 Collection<Node> affectedNodes = AllNodesVisitor.getAllNodes(selection);
464
465 // for these transformations, having only one node makes no sense - quit silently
466 if (affectedNodes.size() < 2 && (mode == Mode.rotate || mode == Mode.scale))
467 return;
468
469 Command c = !Main.main.undoRedo.commands.isEmpty()
470 ? Main.main.undoRedo.commands.getLast() : null;
471 if (c instanceof SequenceCommand) {
472 c = ((SequenceCommand) c).getLastCommand();
473 }
474
475 if (mode == Mode.move) {
476 if (c instanceof MoveCommand && affectedNodes.equals(((MoveCommand) c).getParticipatingPrimitives())) {
477 ((MoveCommand) c).moveAgain(dx, dy);
478 } else {
479 Main.main.undoRedo.add(
480 c = new MoveCommand(selection, dx, dy));
481 }
482
483 for (Node n : affectedNodes) {
484 if (n.getCoor().isOutSideWorld()) {
485 // Revert move
486 ((MoveCommand) c).moveAgain(-dx, -dy);
487
488 JOptionPane.showMessageDialog(
489 Main.parent,
490 tr("Cannot move objects outside of the world."),
491 tr("Warning"),
492 JOptionPane.WARNING_MESSAGE);
493 mv.setNewCursor(cursor, this);
494 return;
495 }
496 }
497 } else if (mode == Mode.rotate) {
498 if (c instanceof RotateCommand && affectedNodes.equals(((RotateCommand) c).getTransformedNodes())) {
499 ((RotateCommand) c).handleEvent(currentEN);
500 } else {
501 Main.main.undoRedo.add(new RotateCommand(selection, currentEN));
502 }
503 } else if (mode == Mode.scale) {
504 if (c instanceof ScaleCommand && affectedNodes.equals(((ScaleCommand) c).getTransformedNodes())) {
505 ((ScaleCommand) c).handleEvent(currentEN);
506 } else {
507 Main.main.undoRedo.add(new ScaleCommand(selection, currentEN));
508 }
509 }
510 }
511
512 mv.repaint();
513 if (mode != Mode.scale) {
514 lastMousePos = e.getPoint();
515 }
516
517 didMouseDrag = true;
518 }
519
520 @Override
521 public void mouseMoved(MouseEvent e) {
522 // Mac OSX simulates with ctrl + mouse 1 the second mouse button hence no dragging events get fired.
523 if ((Main.platform instanceof PlatformHookOsx) && (mode == Mode.rotate || mode == Mode.scale)) {
524 mouseDragged(e);
525 return;
526 }
527 oldEvent = e;
528 if(giveUserFeedback(e)) {
529 mv.repaint();
530 }
531 }
532
533 @Override
534 public void mouseExited(MouseEvent e) {
535 if(removeHighlighting()) {
536 mv.repaint();
537 }
538 }
539
540 /** returns true whenever elements have been grabbed and moved (i.e. the initial
541 * thresholds have been exceeded) and is still in progress (i.e. mouse button
542 * still pressed)
543 */
544 final private boolean dragInProgress() {
545 return didMouseDrag && startingDraggingPos != null;
546 }
547
548 private Node virtualNode = null;
549 private Collection<WaySegment> virtualWays = new LinkedList<WaySegment>();
550
551 /**
552 * Calculate a virtual node if there is enough visual space to draw a crosshair
553 * node and the middle of a way segment is clicked. If the user drags the
554 * crosshair node, it will be added to all ways in <code>virtualWays</code>.
555 *
556 * @param e contains the point clicked
557 * @return whether <code>virtualNode</code> and <code>virtualWays</code> were setup.
558 */
559 private boolean setupVirtual(MouseEvent e) {
560 if (Main.pref.getInteger("mappaint.node.virtual-size", 8) > 0) {
561 int virtualSnapDistSq = Main.pref.getInteger("mappaint.node.virtual-snap-distance", 8);
562 int virtualSpace = Main.pref.getInteger("mappaint.node.virtual-space", 70);
563 virtualSnapDistSq *= virtualSnapDistSq;
564
565 Collection<WaySegment> selVirtualWays = new LinkedList<WaySegment>();
566 Pair<Node, Node> vnp = null, wnp = new Pair<Node, Node>(null, null);
567 Point p = e.getPoint();
568 Way w = null;
569
570 for (WaySegment ws : mv.getNearestWaySegments(p, OsmPrimitive.isSelectablePredicate)) {
571 w = ws.way;
572
573 Point2D p1 = mv.getPoint2D(wnp.a = w.getNode(ws.lowerIndex));
574 Point2D p2 = mv.getPoint2D(wnp.b = w.getNode(ws.lowerIndex + 1));
575 if (WireframeMapRenderer.isLargeSegment(p1, p2, virtualSpace)) {
576 Point2D pc = new Point2D.Double((p1.getX() + p2.getX()) / 2, (p1.getY() + p2.getY()) / 2);
577 if (p.distanceSq(pc) < virtualSnapDistSq) {
578 // Check that only segments on top of each other get added to the
579 // virtual ways list. Otherwise ways that coincidentally have their
580 // virtual node at the same spot will be joined which is likely unwanted
581 Pair.sort(wnp);
582 if (vnp == null) {
583 vnp = new Pair<Node, Node>(wnp.a, wnp.b);
584 virtualNode = new Node(mv.getLatLon(pc.getX(), pc.getY()));
585 }
586 if (vnp.equals(wnp)) {
587 (w.isSelected() ? selVirtualWays : virtualWays).add(ws);
588 }
589 }
590 }
591 }
592
593 if (!selVirtualWays.isEmpty()) {
594 virtualWays = selVirtualWays;
595 }
596 }
597
598 return !virtualWays.isEmpty();
599 }
600 private Collection<OsmPrimitive> cycleList = Collections.emptyList();
601 private boolean cyclePrims = false;
602 private OsmPrimitive cycleStart = null;
603
604 /**
605 *
606 * @param osm nearest primitive found by simple method
607 * @param e
608 * @return
609 */
610 private Collection<OsmPrimitive> cycleSetup(Collection<OsmPrimitive> single, MouseEvent e) {
611 OsmPrimitive osm = null;
612
613 if (single != null && !single.isEmpty()) {
614 osm = single.iterator().next();
615
616 Point p = e.getPoint();
617 boolean waitForMouseUp = Main.pref.getBoolean("mappaint.select.waits-for-mouse-up", false);
618 updateKeyModifiers(e);
619 alt = alt || Main.pref.getBoolean("selectaction.cycles.multiple.matches", false);
620
621 if (!alt) {
622 cycleList = MapView.asColl(osm);
623
624 if (waitForMouseUp) {
625 // prefer a selected nearest node or way, if possible
626 osm = mv.getNearestNodeOrWay(p, OsmPrimitive.isSelectablePredicate, true);
627 }
628 } else {
629 cycleList = mv.getAllNearest(p, OsmPrimitive.isSelectablePredicate);
630
631 if (cycleList.size() > 1) {
632 cyclePrims = false;
633
634 OsmPrimitive old = osm;
635 for (OsmPrimitive o : cycleList) {
636 if (o.isSelected()) {
637 cyclePrims = true;
638 osm = o;
639 break;
640 }
641 }
642
643 // special case: for cycle groups of 2, we can toggle to the
644 // true nearest primitive on mousePressed right away
645 if (cycleList.size() == 2 && !waitForMouseUp) {
646 if (!(osm.equals(old) || osm.isNew() || ctrl)) {
647 cyclePrims = false;
648 osm = old;
649 } // else defer toggling to mouseRelease time in those cases:
650 /*
651 * osm == old -- the true nearest node is the selected one
652 * osm is a new node -- do not break unglue ways in ALT mode
653 * ctrl is pressed -- ctrl generally works on mouseReleased
654 */
655 }
656 }
657 }
658 }
659
660 return MapView.asColl(osm);
661 }
662
663 /**
664 * sets the mapmode according to key modifiers and if there are any
665 * selectables nearby. Everything has to be pre-determined for this
666 * function; its main purpose is to centralize what the modifiers do.
667 * @param hasSelectionNearby
668 */
669 private void determineMapMode(boolean hasSelectionNearby) {
670 if (shift && ctrl) {
671 mode = Mode.rotate;
672 } else if (alt && ctrl) {
673 mode = Mode.scale;
674 } else if (hasSelectionNearby || dragInProgress()) {
675 mode = Mode.move;
676 } else {
677 mode = Mode.select;
678 }
679 }
680
681 /**
682 * Look, whether any object is selected. If not, select the nearest node.
683 * If there are no nodes in the dataset, do nothing.
684 *
685 * If the user did not press the left mouse button, do nothing.
686 *
687 * Also remember the starting position of the movement and change the mouse
688 * cursor to movement.
689 */
690 @Override
691 public void mousePressed(MouseEvent e) {
692 // return early
693 if (!mv.isActiveLayerVisible() || !(Boolean) this.getValue("active") || (mouseDownButton = e.getButton()) != MouseEvent.BUTTON1)
694 return;
695
696 // request focus in order to enable the expected keyboard shortcuts
697 mv.requestFocus();
698
699 // update which modifiers are pressed (shift, alt, ctrl)
700 updateKeyModifiers(e);
701
702 // We don't want to change to draw tool if the user tries to (de)select
703 // stuff but accidentally clicks in an empty area when selection is empty
704 cancelDrawMode = (shift || ctrl);
705 didMouseDrag = false;
706 initialMoveThresholdExceeded = false;
707 mouseDownTime = System.currentTimeMillis();
708 lastMousePos = e.getPoint();
709
710 Collection<OsmPrimitive> c = MapView.asColl(
711 mv.getNearestNodeOrWay(e.getPoint(), OsmPrimitive.isSelectablePredicate, true));
712
713 determineMapMode(!c.isEmpty());
714 switch(mode) {
715 case rotate:
716 case scale:
717 if (getCurrentDataSet().getSelected().isEmpty()) {
718 getCurrentDataSet().setSelected(c);
719 }
720
721 // Mode.select redraws when selectPrims is called
722 // Mode.move redraws when mouseDragged is called
723 // Mode.rotate redraws here
724 // Mode.scale redraws here
725 break;
726 case move:
727 if (!cancelDrawMode && c.iterator().next() instanceof Way) {
728 setupVirtual(e);
729 }
730
731 selectPrims(cycleSetup(c, e), e, false, false);
732 break;
733 case select:
734 default:
735 selectionManager.register(mv, lassoMode);
736 selectionManager.mousePressed(e);
737 break;
738 }
739 giveUserFeedback(e);
740 mv.repaint();
741 updateStatusLine();
742 }
743
744 @Override
745 public void mouseReleased(MouseEvent e) {
746 if (!mv.isActiveLayerVisible())
747 return;
748
749 startingDraggingPos = null;
750 mouseReleaseTime = System.currentTimeMillis();
751
752 if (mode == Mode.select) {
753 selectionManager.unregister(mv);
754
755 // Select Draw Tool if no selection has been made
756 if (getCurrentDataSet().getSelected().size() == 0 && !cancelDrawMode) {
757 Main.map.selectDrawTool(true);
758 return;
759 }
760 }
761
762 if (mode == Mode.move && e.getButton() == MouseEvent.BUTTON1) {
763 if (!didMouseDrag) {
764 // only built in move mode
765 virtualWays.clear();
766 virtualNode = null;
767
768 // do nothing if the click was to short too be recognized as a drag,
769 // but the release position is farther than 10px away from the press position
770 if (lastMousePos == null || lastMousePos.distanceSq(e.getPoint()) < 100) {
771 selectPrims(cyclePrims(cycleList, e), e, true, false);
772
773 // If the user double-clicked a node, change to draw mode
774 Collection<OsmPrimitive> c = getCurrentDataSet().getSelected();
775 if (e.getClickCount() >= 2 && c.size() == 1 && c.iterator().next() instanceof Node) {
776 // We need to do it like this as otherwise drawAction will see a double
777 // click and switch back to SelectMode
778 Main.worker.execute(new Runnable() {
779 public void run() {
780 Main.map.selectDrawTool(true);
781 }
782 });
783 return;
784 }
785 }
786 } else {
787 int max = Main.pref.getInteger("warn.move.maxelements", 20), limit = max;
788 for (OsmPrimitive osm : getCurrentDataSet().getSelected()) {
789 if (osm instanceof Way) {
790 limit -= ((Way) osm).getNodes().size();
791 }
792 if ((limit -= 1) < 0) {
793 break;
794 }
795 }
796 if (limit < 0) {
797 ExtendedDialog ed = new ExtendedDialog(
798 Main.parent,
799 tr("Move elements"),
800 new String[]{tr("Move them"), tr("Undo move")});
801 ed.setButtonIcons(new String[]{"reorder.png", "cancel.png"});
802 ed.setContent(tr("You moved more than {0} elements. " + "Moving a large number of elements is often an error.\n" + "Really move them?", max));
803 ed.setCancelButton(2);
804 ed.toggleEnable("movedManyElements");
805 ed.showDialog();
806
807 if (ed.getValue() != 1) {
808 Main.main.undoRedo.undo();
809 }
810 } else {
811 mergePrims(e);
812 }
813 getCurrentDataSet().fireSelectionChanged();
814 }
815 }
816
817 mode = null;
818
819 // simply remove any highlights if the middle click popup is active because
820 // the highlights don't depend on the cursor position there. If something was
821 // selected beforehand this would put us into move mode as well, which breaks
822 // the cycling through primitives on top of each other (see #6739).
823 if(e.getButton() == MouseEvent.BUTTON2) {
824 removeHighlighting();
825 } else {
826 giveUserFeedback(e);
827 }
828 updateStatusLine();
829 }
830
831 public void selectionEnded(Rectangle r, MouseEvent e) {
832 updateKeyModifiers(e);
833 mv.repaint();
834 selectPrims(selectionManager.getSelectedObjects(alt), e, true, true);
835 }
836
837 /**
838 * Modifies current selection state and returns the next element in a
839 * selection cycle given by <code>prims</code>.
840 * @param prims the primitives that form the selection cycle
841 * @param mouse event
842 * @return the next element of cycle list <code>prims</code>.
843 */
844 private Collection<OsmPrimitive> cyclePrims(Collection<OsmPrimitive> prims, MouseEvent e) {
845 OsmPrimitive nxt = null;
846
847 if (prims.size() > 1) {
848 updateKeyModifiers(e);
849
850 DataSet ds = getCurrentDataSet();
851 OsmPrimitive first = prims.iterator().next(), foundInDS = null;
852 nxt = first;
853
854 for (Iterator<OsmPrimitive> i = prims.iterator(); i.hasNext();) {
855 if (cyclePrims && shift) {
856 if (!(nxt = i.next()).isSelected()) {
857 break; // take first primitive in prims list not in sel
858 }
859 } else {
860 if ((nxt = i.next()).isSelected()) {
861 foundInDS = nxt;
862 if (cyclePrims || ctrl) {
863 ds.clearSelection(foundInDS);
864 nxt = i.hasNext() ? i.next() : first;
865 }
866 break; // take next primitive in prims list
867 }
868 }
869 }
870
871 if (ctrl) {
872 // a member of prims was found in the current dataset selection
873 if (foundInDS != null) {
874 // mouse was moved to a different selection group w/ a previous sel
875 if (!prims.contains(cycleStart)) {
876 ds.clearSelection(prims);
877 cycleStart = foundInDS;
878 } else if (cycleStart.equals(nxt)) {
879 // loop detected, insert deselect step
880 ds.addSelected(nxt);
881 }
882 } else {
883 // setup for iterating a sel group again or a new, different one..
884 nxt = (prims.contains(cycleStart)) ? cycleStart : first;
885 cycleStart = nxt;
886 }
887 } else {
888 cycleStart = null;
889 }
890 }
891
892 // pass on prims, if it had less than 2 elements
893 return (nxt != null) ? MapView.asColl(nxt) : prims;
894 }
895
896 /** Merges the selected nodes to the one closest to the given mouse position iff the control
897 * key is pressed. If there is no such node, no action will be done and no error will be
898 * reported. If there is, it will execute the merge and add it to the undo buffer. */
899 final private void mergePrims(MouseEvent e) {
900 updateKeyModifiers(e);
901 Collection<Node> selNodes = getCurrentDataSet().getSelectedNodes();
902 if (!ctrl || selNodes.isEmpty())
903 return;
904
905 Node target = findNodeToMergeTo(e);
906 if (target == null)
907 return;
908
909 Collection<Node> nodesToMerge = new LinkedList<Node>(selNodes);
910 nodesToMerge.add(target);
911 Command cmd = MergeNodesAction.mergeNodes(Main.main.getEditLayer(), nodesToMerge, target);
912 if (cmd != null) {
913 Main.main.undoRedo.add(cmd);
914 getCurrentDataSet().setSelected(target);
915 }
916 }
917
918 /** tries to find a node to merge to when in move-merge mode for the current mouse
919 * position. Either returns the node or null, if no suitable one is nearby. */
920 final private Node findNodeToMergeTo(MouseEvent e) {
921 Collection<Node> target = mv.getNearestNodes(e.getPoint(),
922 getCurrentDataSet().getSelectedNodes(),
923 OsmPrimitive.isSelectablePredicate);
924 return target.isEmpty() ? null : target.iterator().next();
925 }
926
927 private void selectPrims(Collection<OsmPrimitive> prims, MouseEvent e, boolean released, boolean area) {
928 updateKeyModifiers(e);
929 DataSet ds = getCurrentDataSet();
930
931 // not allowed together: do not change dataset selection, return early
932 // Virtual Ways: if non-empty the cursor is above a virtual node. So don't highlight
933 // anything if about to drag the virtual node (i.e. !released) but continue if the
934 // cursor is only released above a virtual node by accident (i.e. released). See #7018
935 if ((shift && ctrl) || (ctrl && !released) || (!virtualWays.isEmpty() && !released))
936 return;
937
938 if (!released) {
939 // Don't replace the selection if the user clicked on a
940 // selected object (it breaks moving of selected groups).
941 // Do it later, on mouse release.
942 shift |= getCurrentDataSet().getSelected().containsAll(prims);
943 }
944
945 if (ctrl) {
946 // Ctrl on an item toggles its selection status,
947 // but Ctrl on an *area* just clears those items
948 // out of the selection.
949 if (area) {
950 ds.clearSelection(prims);
951 } else {
952 ds.toggleSelected(prims);
953 }
954 } else if (shift) {
955 // add prims to an existing selection
956 ds.addSelected(prims);
957 } else {
958 // clear selection, then select the prims clicked
959 ds.setSelected(prims);
960 }
961 }
962
963 @Override
964 public String getModeHelpText() {
965 if (mode == Mode.select)
966 return tr("Release the mouse button to select the objects in the rectangle.");
967 else if (mode == Mode.move) {
968 final boolean canMerge = getCurrentDataSet()!=null && !getCurrentDataSet().getSelectedNodes().isEmpty();
969 final String mergeHelp = canMerge ? (" " + tr("Ctrl to merge with nearest node.")) : "";
970 return tr("Release the mouse button to stop moving.") + mergeHelp;
971 } else if (mode == Mode.rotate)
972 return tr("Release the mouse button to stop rotating.");
973 else if (mode == Mode.scale)
974 return tr("Release the mouse button to stop scaling.");
975 else
976 return tr("Move objects by dragging; Shift to add to selection (Ctrl to toggle); Shift-Ctrl to rotate selected; Alt-Ctrl to scale selected; or change selection");
977 }
978
979 @Override
980 public boolean layerIsSupported(Layer l) {
981 return l instanceof OsmDataLayer;
982 }
983
984 public void setLassoMode(boolean lassoMode) {
985 this.selectionManager.setLassoMode(lassoMode);
986 this.lassoMode = lassoMode;
987 }
988}
Note: See TracBrowser for help on using the repository browser.