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

Last change on this file since 4327 was 4327, checked in by xeen, 13 years ago

updates visual appearance of highlights and adds them to select and delete action

in more detail:

  • add target highlighting to select action
  • add target cursor to select action
  • add target highlighting to delete action
  • unify ctrl/alt/shift modifier detection in MapMode actions
  • highlights are now a halo around the way/node instead of a color change
  • default highlight color is now the same as the select color (red)
  • ability to highlight WaySegments and VirtualNodes
  • various style/whitespace nits
  • fixes #2411

This patch touches a lot of areas, so please report any regressions in the map mode
tools. Also please add a comment to #2411 if you find to highlighting in select
mode distracting, so it can be fine tuned (or turned off by default).

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