001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui; 003 004import java.awt.Color; 005import java.awt.Graphics2D; 006import java.awt.Point; 007import java.awt.Polygon; 008import java.awt.Rectangle; 009import java.awt.event.InputEvent; 010import java.awt.event.MouseEvent; 011import java.awt.event.MouseListener; 012import java.awt.event.MouseMotionListener; 013import java.beans.PropertyChangeEvent; 014import java.beans.PropertyChangeListener; 015import java.util.Collection; 016import java.util.LinkedList; 017 018import org.openstreetmap.josm.Main; 019import org.openstreetmap.josm.actions.SelectByInternalPointAction; 020import org.openstreetmap.josm.data.Bounds; 021import org.openstreetmap.josm.data.osm.Node; 022import org.openstreetmap.josm.data.osm.OsmPrimitive; 023import org.openstreetmap.josm.data.osm.Way; 024import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 025import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable; 026import org.openstreetmap.josm.tools.Utils; 027 028/** 029 * Manages the selection of a rectangle or a lasso loop. Listening to left and right mouse button 030 * presses and to mouse motions and draw the rectangle accordingly. 031 * 032 * Left mouse button selects a rectangle from the press until release. Pressing 033 * right mouse button while left is still pressed enable the selection area to move 034 * around. Releasing the left button fires an action event to the listener given 035 * at constructor, except if the right is still pressed, which just remove the 036 * selection rectangle and does nothing. 037 * 038 * It is possible to switch between lasso selection and rectangle selection by using {@link #setLassoMode(boolean)}. 039 * 040 * The point where the left mouse button was pressed and the current mouse 041 * position are two opposite corners of the selection rectangle. 042 * 043 * For rectangle mode, it is possible to specify an aspect ratio (width per height) which the 044 * selection rectangle always must have. In this case, the selection rectangle 045 * will be the largest window with this aspect ratio, where the position the left 046 * mouse button was pressed and the corner of the current mouse position are at 047 * opposite sites (the mouse position corner is the corner nearest to the mouse 048 * cursor). 049 * 050 * When the left mouse button was released, an ActionEvent is send to the 051 * ActionListener given at constructor. The source of this event is this manager. 052 * 053 * @author imi 054 */ 055public class SelectionManager implements MouseListener, MouseMotionListener, PropertyChangeListener { 056 057 /** 058 * This is the interface that an user of SelectionManager has to implement 059 * to get informed when a selection closes. 060 * @author imi 061 */ 062 public interface SelectionEnded { 063 /** 064 * Called, when the left mouse button was released. 065 * @param r The rectangle that encloses the current selection. 066 * @param e The mouse event. 067 * @see InputEvent#getModifiersEx() 068 * @see SelectionManager#getSelectedObjects(boolean) 069 */ 070 void selectionEnded(Rectangle r, MouseEvent e); 071 072 /** 073 * Called to register the selection manager for "active" property. 074 * @param listener The listener to register 075 */ 076 void addPropertyChangeListener(PropertyChangeListener listener); 077 078 /** 079 * Called to remove the selection manager from the listener list 080 * for "active" property. 081 * @param listener The listener to register 082 */ 083 void removePropertyChangeListener(PropertyChangeListener listener); 084 } 085 086 /** 087 * This draws the selection hint (rectangle or lasso polygon) on the screen. 088 * 089 * @author Michael Zangl 090 */ 091 private class SelectionHintLayer extends AbstractMapViewPaintable { 092 @Override 093 public void paint(Graphics2D g, MapView mv, Bounds bbox) { 094 if (mousePos == null || mousePosStart == null || mousePos == mousePosStart) 095 return; 096 Color color = Utils.complement(PaintColors.getBackgroundColor()); 097 g.setColor(color); 098 if (lassoMode) { 099 g.drawPolygon(lasso); 100 101 g.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha() / 8)); 102 g.fillPolygon(lasso); 103 } else { 104 Rectangle paintRect = getSelectionRectangle(); 105 g.drawRect(paintRect.x, paintRect.y, paintRect.width, paintRect.height); 106 } 107 } 108 } 109 110 /** 111 * The listener that receives the events after left mouse button is released. 112 */ 113 private final SelectionEnded selectionEndedListener; 114 /** 115 * Position of the map when the mouse button was pressed. 116 * If this is not <code>null</code>, a rectangle/lasso line is drawn on screen. 117 * If this is <code>null</code>, no selection is active. 118 */ 119 private Point mousePosStart; 120 /** 121 * The last position of the mouse while the mouse button was pressed. 122 */ 123 private Point mousePos; 124 /** 125 * The Component that provides us with OSM data and the aspect is taken from. 126 */ 127 private final NavigatableComponent nc; 128 /** 129 * Whether the selection rectangle must obtain the aspect ratio of the drawComponent. 130 */ 131 private final boolean aspectRatio; 132 133 /** 134 * <code>true</code> if we should paint a lasso instead of a rectangle. 135 */ 136 private boolean lassoMode; 137 /** 138 * The polygon to store the selection outline if {@link #lassoMode} is used. 139 */ 140 private final Polygon lasso = new Polygon(); 141 142 /** 143 * The result of the last selection. 144 */ 145 private Polygon selectionResult = new Polygon(); 146 147 private final SelectionHintLayer selectionHintLayer = new SelectionHintLayer(); 148 149 /** 150 * Create a new SelectionManager. 151 * 152 * @param selectionEndedListener The action listener that receives the event when 153 * the left button is released. 154 * @param aspectRatio If true, the selection window must obtain the aspect 155 * ratio of the drawComponent. 156 * @param navComp The component that provides us with OSM data and the aspect is taken from. 157 */ 158 public SelectionManager(SelectionEnded selectionEndedListener, boolean aspectRatio, NavigatableComponent navComp) { 159 this.selectionEndedListener = selectionEndedListener; 160 this.aspectRatio = aspectRatio; 161 this.nc = navComp; 162 } 163 164 /** 165 * Register itself at the given event source and add a hint layer. 166 * @param eventSource The emitter of the mouse events. 167 * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it. 168 */ 169 public void register(MapView eventSource, boolean lassoMode) { 170 this.lassoMode = lassoMode; 171 eventSource.addMouseListener(this); 172 eventSource.addMouseMotionListener(this); 173 selectionEndedListener.addPropertyChangeListener(this); 174 eventSource.addPropertyChangeListener("scale", new PropertyChangeListener() { 175 @Override 176 public void propertyChange(PropertyChangeEvent evt) { 177 abortSelecting(); 178 } 179 }); 180 eventSource.addTemporaryLayer(selectionHintLayer); 181 } 182 183 /** 184 * Unregister itself from the given event source and hide the selection hint layer. 185 * 186 * @param eventSource The emitter of the mouse events. 187 */ 188 public void unregister(MapView eventSource) { 189 abortSelecting(); 190 eventSource.removeTemporaryLayer(selectionHintLayer); 191 eventSource.removeMouseListener(this); 192 eventSource.removeMouseMotionListener(this); 193 selectionEndedListener.removePropertyChangeListener(this); 194 } 195 196 /** 197 * If the correct button, from the "drawing rectangle" mode 198 */ 199 @Override 200 public void mousePressed(MouseEvent e) { 201 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() > 1 && Main.getLayerManager().getEditDataSet() != null) { 202 SelectByInternalPointAction.performSelection(Main.map.mapView.getEastNorth(e.getX(), e.getY()), 203 (e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) != 0, 204 (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) != 0); 205 } else if (e.getButton() == MouseEvent.BUTTON1) { 206 mousePosStart = mousePos = e.getPoint(); 207 208 lasso.reset(); 209 lasso.addPoint(mousePosStart.x, mousePosStart.y); 210 } 211 } 212 213 /** 214 * If the correct button is hold, draw the rectangle. 215 */ 216 @Override 217 public void mouseDragged(MouseEvent e) { 218 int buttonPressed = e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK); 219 220 if (buttonPressed != 0) { 221 if (mousePosStart == null) { 222 mousePosStart = mousePos = e.getPoint(); 223 } 224 selectionAreaChanged(); 225 } 226 227 if (buttonPressed == MouseEvent.BUTTON1_DOWN_MASK) { 228 mousePos = e.getPoint(); 229 addLassoPoint(e.getPoint()); 230 selectionAreaChanged(); 231 } else if (buttonPressed == (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) { 232 moveSelection(e.getX()-mousePos.x, e.getY()-mousePos.y); 233 mousePos = e.getPoint(); 234 selectionAreaChanged(); 235 } 236 } 237 238 /** 239 * Moves the current selection by some pixels. 240 * @param dx How much to move it in x direction. 241 * @param dy How much to move it in y direction. 242 */ 243 private void moveSelection(int dx, int dy) { 244 mousePosStart.x += dx; 245 mousePosStart.y += dy; 246 lasso.translate(dx, dy); 247 } 248 249 /** 250 * Check the state of the keys and buttons and set the selection accordingly. 251 */ 252 @Override 253 public void mouseReleased(MouseEvent e) { 254 if (e.getButton() == MouseEvent.BUTTON1) { 255 endSelecting(e); 256 } 257 } 258 259 /** 260 * Ends the selection of the current area. This simulates a release of mouse button 1. 261 * @param e A mouse event that caused this. Needed for backward compatibility. 262 */ 263 public void endSelecting(MouseEvent e) { 264 mousePos = e.getPoint(); 265 if (lassoMode) { 266 addLassoPoint(e.getPoint()); 267 } 268 269 // Left mouse was released while right is still pressed. 270 boolean rightMouseStillPressed = (e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) != 0; 271 272 if (!rightMouseStillPressed) { 273 selectingDone(e); 274 } 275 abortSelecting(); 276 } 277 278 private void addLassoPoint(Point point) { 279 if (isNoSelection()) { 280 return; 281 } 282 lasso.addPoint(point.x, point.y); 283 } 284 285 private boolean isNoSelection() { 286 return mousePos == null || mousePosStart == null || mousePos == mousePosStart; 287 } 288 289 /** 290 * Calculate and return the current selection rectangle 291 * @return A rectangle that spans from mousePos to mouseStartPos 292 */ 293 private Rectangle getSelectionRectangle() { 294 int x = mousePosStart.x; 295 int y = mousePosStart.y; 296 int w = mousePos.x - mousePosStart.x; 297 int h = mousePos.y - mousePosStart.y; 298 if (w < 0) { 299 x += w; 300 w = -w; 301 } 302 if (h < 0) { 303 y += h; 304 h = -h; 305 } 306 307 if (aspectRatio) { 308 /* Keep the aspect ratio by growing the rectangle; the 309 * rectangle is always under the cursor. */ 310 double aspectRatio = (double) nc.getWidth()/nc.getHeight(); 311 if ((double) w/h < aspectRatio) { 312 int neww = (int) (h*aspectRatio); 313 if (mousePos.x < mousePosStart.x) { 314 x += w - neww; 315 } 316 w = neww; 317 } else { 318 int newh = (int) (w/aspectRatio); 319 if (mousePos.y < mousePosStart.y) { 320 y += h - newh; 321 } 322 h = newh; 323 } 324 } 325 326 return new Rectangle(x, y, w, h); 327 } 328 329 /** 330 * If the action goes inactive, remove the selection rectangle from screen 331 */ 332 @Override 333 public void propertyChange(PropertyChangeEvent evt) { 334 if ("active".equals(evt.getPropertyName()) && !(Boolean) evt.getNewValue()) { 335 abortSelecting(); 336 } 337 } 338 339 /** 340 * Stores the current selection and stores the result in {@link #selectionResult} to be retrieved by 341 * {@link #getSelectedObjects(boolean)} later. 342 * @param e The mouse event that caused the selection to be finished. 343 */ 344 private void selectingDone(MouseEvent e) { 345 if (isNoSelection()) { 346 // Nothing selected. 347 return; 348 } 349 Rectangle r; 350 if (lassoMode) { 351 r = lasso.getBounds(); 352 353 selectionResult = new Polygon(lasso.xpoints, lasso.ypoints, lasso.npoints); 354 } else { 355 r = getSelectionRectangle(); 356 357 selectionResult = rectToPolygon(r); 358 } 359 selectionEndedListener.selectionEnded(r, e); 360 } 361 362 private void abortSelecting() { 363 if (mousePosStart != null) { 364 mousePos = mousePosStart = null; 365 lasso.reset(); 366 selectionAreaChanged(); 367 } 368 } 369 370 private void selectionAreaChanged() { 371 selectionHintLayer.invalidate(); 372 } 373 374 /** 375 * Return a list of all objects in the active/last selection, respecting the different 376 * modifier. 377 * 378 * @param alt Whether the alt key was pressed, which means select all 379 * objects that are touched, instead those which are completely covered. 380 * @return The collection of selected objects. 381 */ 382 public Collection<OsmPrimitive> getSelectedObjects(boolean alt) { 383 Collection<OsmPrimitive> selection = new LinkedList<>(); 384 385 // whether user only clicked, not dragged. 386 boolean clicked = false; 387 Rectangle bounding = selectionResult.getBounds(); 388 if (bounding.height <= 2 && bounding.width <= 2) { 389 clicked = true; 390 } 391 392 if (clicked) { 393 Point center = new Point(selectionResult.xpoints[0], selectionResult.ypoints[0]); 394 OsmPrimitive osm = nc.getNearestNodeOrWay(center, OsmPrimitive.isSelectablePredicate, false); 395 if (osm != null) { 396 selection.add(osm); 397 } 398 } else { 399 // nodes 400 for (Node n : Main.getLayerManager().getEditDataSet().getNodes()) { 401 if (n.isSelectable() && selectionResult.contains(nc.getPoint2D(n))) { 402 selection.add(n); 403 } 404 } 405 406 // ways 407 for (Way w : Main.getLayerManager().getEditDataSet().getWays()) { 408 if (!w.isSelectable() || w.getNodesCount() == 0) { 409 continue; 410 } 411 if (alt) { 412 for (Node n : w.getNodes()) { 413 if (!n.isIncomplete() && selectionResult.contains(nc.getPoint2D(n))) { 414 selection.add(w); 415 break; 416 } 417 } 418 } else { 419 boolean allIn = true; 420 for (Node n : w.getNodes()) { 421 if (!n.isIncomplete() && !selectionResult.contains(nc.getPoint(n))) { 422 allIn = false; 423 break; 424 } 425 } 426 if (allIn) { 427 selection.add(w); 428 } 429 } 430 } 431 } 432 return selection; 433 } 434 435 private static Polygon rectToPolygon(Rectangle r) { 436 Polygon poly = new Polygon(); 437 438 poly.addPoint(r.x, r.y); 439 poly.addPoint(r.x, r.y + r.height); 440 poly.addPoint(r.x + r.width, r.y + r.height); 441 poly.addPoint(r.x + r.width, r.y); 442 443 return poly; 444 } 445 446 /** 447 * Enables or disables the lasso mode. 448 * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it. 449 */ 450 public void setLassoMode(boolean lassoMode) { 451 this.lassoMode = lassoMode; 452 } 453 454 @Override 455 public void mouseClicked(MouseEvent e) { 456 // Do nothing 457 } 458 459 @Override 460 public void mouseEntered(MouseEvent e) { 461 // Do nothing 462 } 463 464 @Override 465 public void mouseExited(MouseEvent e) { 466 // Do nothing 467 } 468 469 @Override 470 public void mouseMoved(MouseEvent e) { 471 // Do nothing 472 } 473}