source: josm/trunk/src/org/openstreetmap/josm/gui/SelectionManager.java

Last change on this file was 18456, checked in by taylor.smock, 2 years ago

Fix #22065: Mac users cannot deselect with ctrl

From the Mac OS HIG guidelines, "[as] much as possible,
avoid using the Control key as a modifier."

In this case, we were using ctrl to deselect elements (and
we disabled this on Mac), merge nodes, scale, and rotate.

All of those now use (meta) instead of ctrl on Mac.

  • Property svn:eol-style set to native
File size: 15.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui;
3
4import java.awt.Color;
5import java.awt.Graphics2D;
6import java.awt.Point;
7import java.awt.Polygon;
8import java.awt.Rectangle;
9import java.awt.event.InputEvent;
10import java.awt.event.MouseEvent;
11import java.awt.event.MouseListener;
12import java.awt.event.MouseMotionListener;
13import java.beans.PropertyChangeEvent;
14import java.beans.PropertyChangeListener;
15import java.util.Collection;
16import java.util.LinkedList;
17
18import javax.swing.Action;
19
20import org.openstreetmap.josm.actions.SelectByInternalPointAction;
21import org.openstreetmap.josm.data.Bounds;
22import org.openstreetmap.josm.data.osm.DataSet;
23import org.openstreetmap.josm.data.osm.Node;
24import org.openstreetmap.josm.data.osm.OsmPrimitive;
25import org.openstreetmap.josm.data.osm.Way;
26import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
27import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable;
28import org.openstreetmap.josm.tools.ColorHelper;
29import org.openstreetmap.josm.tools.PlatformManager;
30
31/**
32 * Manages the selection of a rectangle or a lasso loop. Listening to left and right mouse button
33 * presses and to mouse motions and draw the rectangle accordingly.
34 *
35 * Left mouse button selects a rectangle from the press until release. Pressing
36 * right mouse button while left is still pressed enable the selection area to move
37 * around. Releasing the left button fires an action event to the listener given
38 * at constructor, except if the right is still pressed, which just remove the
39 * selection rectangle and does nothing.
40 *
41 * It is possible to switch between lasso selection and rectangle selection by using {@link #setLassoMode(boolean)}.
42 *
43 * The point where the left mouse button was pressed and the current mouse
44 * position are two opposite corners of the selection rectangle.
45 *
46 * For rectangle mode, it is possible to specify an aspect ratio (width per height) which the
47 * selection rectangle always must have. In this case, the selection rectangle
48 * will be the largest window with this aspect ratio, where the position the left
49 * mouse button was pressed and the corner of the current mouse position are at
50 * opposite sites (the mouse position corner is the corner nearest to the mouse
51 * cursor).
52 *
53 * When the left mouse button was released, an ActionEvent is send to the
54 * ActionListener given at constructor. The source of this event is this manager.
55 *
56 * @author imi
57 */
58public class SelectionManager implements MouseListener, MouseMotionListener, PropertyChangeListener {
59
60 /**
61 * This is the interface that an user of SelectionManager has to implement
62 * to get informed when a selection closes.
63 * @author imi
64 */
65 public interface SelectionEnded extends Action {
66 /**
67 * Called, when the left mouse button was released.
68 * @param r The rectangle that encloses the current selection.
69 * @param e The mouse event.
70 * @see InputEvent#getModifiersEx()
71 * @see SelectionManager#getSelectedObjects(boolean)
72 */
73 void selectionEnded(Rectangle r, MouseEvent e);
74 }
75
76 /**
77 * This draws the selection hint (rectangle or lasso polygon) on the screen.
78 *
79 * @author Michael Zangl
80 */
81 private class SelectionHintLayer extends AbstractMapViewPaintable {
82 @Override
83 public void paint(Graphics2D g, MapView mv, Bounds bbox) {
84 if (mousePos == null || mousePosStart == null || mousePos == mousePosStart)
85 return;
86 Color color = ColorHelper.complement(PaintColors.getBackgroundColor());
87 g.setColor(color);
88 if (lassoMode) {
89 g.drawPolygon(lasso);
90
91 g.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha() / 8));
92 g.fillPolygon(lasso);
93 } else {
94 Rectangle paintRect = getSelectionRectangle();
95 g.drawRect(paintRect.x, paintRect.y, paintRect.width, paintRect.height);
96 }
97 }
98 }
99
100 /**
101 * The listener that receives the events after left mouse button is released.
102 */
103 private final SelectionEnded selectionEndedListener;
104 /**
105 * Position of the map when the mouse button was pressed.
106 * If this is not <code>null</code>, a rectangle/lasso line is drawn on screen.
107 * If this is <code>null</code>, no selection is active.
108 */
109 private Point mousePosStart;
110 /**
111 * The last position of the mouse while the mouse button was pressed.
112 */
113 private Point mousePos;
114 /**
115 * The Component that provides us with OSM data and the aspect is taken from.
116 */
117 private final NavigatableComponent nc;
118 /**
119 * Whether the selection rectangle must obtain the aspect ratio of the drawComponent.
120 */
121 private final boolean aspectRatio;
122
123 /**
124 * <code>true</code> if we should paint a lasso instead of a rectangle.
125 */
126 private boolean lassoMode;
127 /**
128 * The polygon to store the selection outline if {@link #lassoMode} is used.
129 */
130 private final Polygon lasso = new Polygon();
131
132 /**
133 * The result of the last selection.
134 */
135 private Polygon selectionResult = new Polygon();
136
137 private final SelectionHintLayer selectionHintLayer = new SelectionHintLayer();
138
139 /**
140 * Create a new SelectionManager.
141 *
142 * @param selectionEndedListener The action listener that receives the event when
143 * the left button is released.
144 * @param aspectRatio If true, the selection window must obtain the aspect
145 * ratio of the drawComponent.
146 * @param navComp The component that provides us with OSM data and the aspect is taken from.
147 */
148 public SelectionManager(SelectionEnded selectionEndedListener, boolean aspectRatio, NavigatableComponent navComp) {
149 this.selectionEndedListener = selectionEndedListener;
150 this.aspectRatio = aspectRatio;
151 this.nc = navComp;
152 }
153
154 /**
155 * Register itself at the given event source and add a hint layer.
156 * @param eventSource The emitter of the mouse events.
157 * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it.
158 */
159 public void register(MapView eventSource, boolean lassoMode) {
160 this.lassoMode = lassoMode;
161 eventSource.addMouseListener(this);
162 eventSource.addMouseMotionListener(this);
163 selectionEndedListener.addPropertyChangeListener(this);
164 eventSource.addPropertyChangeListener("scale", evt -> abortSelecting());
165 eventSource.addTemporaryLayer(selectionHintLayer);
166 }
167
168 /**
169 * Unregister itself from the given event source and hide the selection hint layer.
170 *
171 * @param eventSource The emitter of the mouse events.
172 */
173 public void unregister(MapView eventSource) {
174 abortSelecting();
175 eventSource.removeTemporaryLayer(selectionHintLayer);
176 eventSource.removeMouseListener(this);
177 eventSource.removeMouseMotionListener(this);
178 selectionEndedListener.removePropertyChangeListener(this);
179 }
180
181 /**
182 * If the correct button, from the "drawing rectangle" mode
183 */
184 @Override
185 public void mousePressed(MouseEvent e) {
186 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() > 1 && MainApplication.getLayerManager().getActiveDataSet() != null) {
187 SelectByInternalPointAction.performSelection(MainApplication.getMap().mapView.getEastNorth(e.getX(), e.getY()),
188 (e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) != 0,
189 (e.getModifiersEx() & PlatformManager.getPlatform().getMenuShortcutKeyMaskEx()) != 0);
190 } else if (e.getButton() == MouseEvent.BUTTON1) {
191 mousePosStart = mousePos = e.getPoint();
192
193 lasso.reset();
194 lasso.addPoint(mousePosStart.x, mousePosStart.y);
195 }
196 }
197
198 /**
199 * If the correct button is hold, draw the rectangle.
200 */
201 @Override
202 public void mouseDragged(MouseEvent e) {
203 int buttonPressed = e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK);
204
205 if (buttonPressed != 0) {
206 if (mousePosStart == null) {
207 mousePosStart = mousePos = e.getPoint();
208 }
209 selectionAreaChanged();
210 }
211
212 if (buttonPressed == MouseEvent.BUTTON1_DOWN_MASK) {
213 mousePos = e.getPoint();
214 addLassoPoint(e.getPoint());
215 selectionAreaChanged();
216 } else if (buttonPressed == (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) {
217 moveSelection(e.getX()-mousePos.x, e.getY()-mousePos.y);
218 mousePos = e.getPoint();
219 selectionAreaChanged();
220 }
221 }
222
223 /**
224 * Moves the current selection by some pixels.
225 * @param dx How much to move it in x direction.
226 * @param dy How much to move it in y direction.
227 */
228 private void moveSelection(int dx, int dy) {
229 mousePosStart.x += dx;
230 mousePosStart.y += dy;
231 lasso.translate(dx, dy);
232 }
233
234 /**
235 * Check the state of the keys and buttons and set the selection accordingly.
236 */
237 @Override
238 public void mouseReleased(MouseEvent e) {
239 if (e.getButton() == MouseEvent.BUTTON1) {
240 endSelecting(e);
241 }
242 }
243
244 /**
245 * Ends the selection of the current area. This simulates a release of mouse button 1.
246 * @param e A mouse event that caused this. Needed for backward compatibility.
247 */
248 public void endSelecting(MouseEvent e) {
249 mousePos = e.getPoint();
250 if (lassoMode) {
251 addLassoPoint(e.getPoint());
252 }
253
254 // Left mouse was released while right is still pressed.
255 boolean rightMouseStillPressed = (e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) != 0;
256
257 if (!rightMouseStillPressed) {
258 selectingDone(e);
259 }
260 abortSelecting();
261 }
262
263 private void addLassoPoint(Point point) {
264 if (isNoSelection()) {
265 return;
266 }
267 lasso.addPoint(point.x, point.y);
268 }
269
270 private boolean isNoSelection() {
271 return mousePos == null || mousePosStart == null || mousePos == mousePosStart;
272 }
273
274 /**
275 * Calculate and return the current selection rectangle
276 * @return A rectangle that spans from mousePos to mouseStartPos
277 */
278 private Rectangle getSelectionRectangle() {
279 int x = mousePosStart.x;
280 int y = mousePosStart.y;
281 int w = mousePos.x - mousePosStart.x;
282 int h = mousePos.y - mousePosStart.y;
283 if (w < 0) {
284 x += w;
285 w = -w;
286 }
287 if (h < 0) {
288 y += h;
289 h = -h;
290 }
291
292 if (aspectRatio) {
293 /* Keep the aspect ratio by growing the rectangle; the
294 * rectangle is always under the cursor. */
295 double aspectRatio = (double) nc.getWidth()/nc.getHeight();
296 if ((double) w/h < aspectRatio) {
297 int neww = (int) (h*aspectRatio);
298 if (mousePos.x < mousePosStart.x) {
299 x += w - neww;
300 }
301 w = neww;
302 } else {
303 int newh = (int) (w/aspectRatio);
304 if (mousePos.y < mousePosStart.y) {
305 y += h - newh;
306 }
307 h = newh;
308 }
309 }
310
311 return new Rectangle(x, y, w, h);
312 }
313
314 /**
315 * If the action goes inactive, remove the selection rectangle from screen
316 */
317 @Override
318 public void propertyChange(PropertyChangeEvent evt) {
319 if ("active".equals(evt.getPropertyName()) && !(Boolean) evt.getNewValue()) {
320 abortSelecting();
321 }
322 }
323
324 /**
325 * Stores the current selection and stores the result in {@link #selectionResult} to be retrieved by
326 * {@link #getSelectedObjects(boolean)} later.
327 * @param e The mouse event that caused the selection to be finished.
328 */
329 private void selectingDone(MouseEvent e) {
330 if (isNoSelection()) {
331 // Nothing selected.
332 return;
333 }
334 Rectangle r;
335 if (lassoMode) {
336 r = lasso.getBounds();
337
338 selectionResult = new Polygon(lasso.xpoints, lasso.ypoints, lasso.npoints);
339 } else {
340 r = getSelectionRectangle();
341
342 selectionResult = rectToPolygon(r);
343 }
344 selectionEndedListener.selectionEnded(r, e);
345 }
346
347 private void abortSelecting() {
348 if (mousePosStart != null) {
349 mousePos = mousePosStart = null;
350 lasso.reset();
351 selectionAreaChanged();
352 }
353 }
354
355 private void selectionAreaChanged() {
356 selectionHintLayer.invalidate();
357 }
358
359 /**
360 * Return a list of all objects in the active/last selection, respecting the different
361 * modifier.
362 *
363 * @param alt Whether the alt key was pressed, which means select all
364 * objects that are touched, instead those which are completely covered.
365 * @return The collection of selected objects.
366 */
367 public Collection<OsmPrimitive> getSelectedObjects(boolean alt) {
368 Collection<OsmPrimitive> selection = new LinkedList<>();
369
370 // whether user only clicked, not dragged.
371 boolean clicked = false;
372 Rectangle bounding = selectionResult.getBounds();
373 if (bounding.height <= 2 && bounding.width <= 2) {
374 clicked = true;
375 }
376
377 DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
378 if (clicked) {
379 Point center = new Point(selectionResult.xpoints[0], selectionResult.ypoints[0]);
380 OsmPrimitive osm = nc.getNearestNodeOrWay(center, OsmPrimitive::isSelectable, false);
381 if (osm != null) {
382 selection.add(osm);
383 }
384 } else if (ds != null) {
385 // nodes
386 for (Node n : ds.getNodes()) {
387 if (n.isSelectable() && selectionResult.contains(nc.getPoint2D(n))) {
388 selection.add(n);
389 }
390 }
391
392 // ways
393 for (Way w : ds.getWays()) {
394 if (!w.isSelectable() || w.isEmpty()) {
395 continue;
396 }
397 if (alt) {
398 if (w.getNodes().stream().anyMatch(n -> !n.isIncomplete() && selectionResult.contains(nc.getPoint2D(n)))) {
399 selection.add(w);
400 }
401 } else {
402 boolean allIn = w.getNodes().stream().allMatch(n -> n.isIncomplete() || selectionResult.contains(nc.getPoint(n)));
403 if (allIn) {
404 selection.add(w);
405 }
406 }
407 }
408 }
409 return selection;
410 }
411
412 private static Polygon rectToPolygon(Rectangle r) {
413 Polygon poly = new Polygon();
414
415 poly.addPoint(r.x, r.y);
416 poly.addPoint(r.x, r.y + r.height);
417 poly.addPoint(r.x + r.width, r.y + r.height);
418 poly.addPoint(r.x + r.width, r.y);
419
420 return poly;
421 }
422
423 /**
424 * Enables or disables the lasso mode.
425 * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it.
426 */
427 public void setLassoMode(boolean lassoMode) {
428 this.lassoMode = lassoMode;
429 }
430
431 @Override
432 public void mouseClicked(MouseEvent e) {
433 // Do nothing
434 }
435
436 @Override
437 public void mouseEntered(MouseEvent e) {
438 // Do nothing
439 }
440
441 @Override
442 public void mouseExited(MouseEvent e) {
443 // Do nothing
444 }
445
446 @Override
447 public void mouseMoved(MouseEvent e) {
448 // Do nothing
449 }
450}
Note: See TracBrowser for help on using the repository browser.