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

Last change on this file since 7217 was 7217, checked in by akks, 10 years ago

see #10104: refactor key press/release detection introducing Main.map.keyDetector

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