001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui; 003 004import java.awt.Component; 005import java.awt.Point; 006import java.awt.Polygon; 007import java.awt.Rectangle; 008import java.awt.event.InputEvent; 009import java.awt.event.MouseEvent; 010import java.awt.event.MouseListener; 011import java.awt.event.MouseMotionListener; 012import java.beans.PropertyChangeEvent; 013import java.beans.PropertyChangeListener; 014import java.util.Collection; 015import java.util.LinkedList; 016 017import org.openstreetmap.josm.Main; 018import org.openstreetmap.josm.actions.SelectByInternalPointAction; 019import org.openstreetmap.josm.data.osm.Node; 020import org.openstreetmap.josm.data.osm.OsmPrimitive; 021import org.openstreetmap.josm.data.osm.Way; 022 023/** 024 * Manages the selection of a rectangle. Listening to left and right mouse button 025 * presses and to mouse motions and draw the rectangle accordingly. 026 * 027 * Left mouse button selects a rectangle from the press until release. Pressing 028 * right mouse button while left is still pressed enable the rectangle to move 029 * around. Releasing the left button fires an action event to the listener given 030 * at constructor, except if the right is still pressed, which just remove the 031 * selection rectangle and does nothing. 032 * 033 * The point where the left mouse button was pressed and the current mouse 034 * position are two opposite corners of the selection rectangle. 035 * 036 * It is possible to specify an aspect ratio (width per height) which the 037 * selection rectangle always must have. In this case, the selection rectangle 038 * will be the largest window with this aspect ratio, where the position the left 039 * mouse button was pressed and the corner of the current mouse position are at 040 * opposite sites (the mouse position corner is the corner nearest to the mouse 041 * cursor). 042 * 043 * When the left mouse button was released, an ActionEvent is send to the 044 * ActionListener given at constructor. The source of this event is this manager. 045 * 046 * @author imi 047 */ 048public class SelectionManager implements MouseListener, MouseMotionListener, PropertyChangeListener { 049 050 /** 051 * This is the interface that an user of SelectionManager has to implement 052 * to get informed when a selection closes. 053 * @author imi 054 */ 055 public interface SelectionEnded { 056 /** 057 * Called, when the left mouse button was released. 058 * @param r The rectangle that is currently the selection. 059 * @param e The mouse event. 060 * @see InputEvent#getModifiersEx() 061 */ 062 public void selectionEnded(Rectangle r, MouseEvent e); 063 /** 064 * Called to register the selection manager for "active" property. 065 * @param listener The listener to register 066 */ 067 public void addPropertyChangeListener(PropertyChangeListener listener); 068 /** 069 * Called to remove the selection manager from the listener list 070 * for "active" property. 071 * @param listener The listener to register 072 */ 073 public void removePropertyChangeListener(PropertyChangeListener listener); 074 } 075 /** 076 * The listener that receives the events after left mouse button is released. 077 */ 078 private final SelectionEnded selectionEndedListener; 079 /** 080 * Position of the map when the mouse button was pressed. 081 * If this is not <code>null</code>, a rectangle is drawn on screen. 082 */ 083 private Point mousePosStart; 084 /** 085 * Position of the map when the selection rectangle was last drawn. 086 */ 087 private Point mousePos; 088 /** 089 * The Component, the selection rectangle is drawn onto. 090 */ 091 private final NavigatableComponent nc; 092 /** 093 * Whether the selection rectangle must obtain the aspect ratio of the 094 * drawComponent. 095 */ 096 private boolean aspectRatio; 097 098 private boolean lassoMode; 099 private Polygon lasso = new Polygon(); 100 101 /** 102 * Create a new SelectionManager. 103 * 104 * @param selectionEndedListener The action listener that receives the event when 105 * the left button is released. 106 * @param aspectRatio If true, the selection window must obtain the aspect 107 * ratio of the drawComponent. 108 * @param navComp The component, the rectangle is drawn onto. 109 */ 110 public SelectionManager(SelectionEnded selectionEndedListener, boolean aspectRatio, NavigatableComponent navComp) { 111 this.selectionEndedListener = selectionEndedListener; 112 this.aspectRatio = aspectRatio; 113 this.nc = navComp; 114 } 115 116 /** 117 * Register itself at the given event source. 118 * @param eventSource The emitter of the mouse events. 119 * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it. 120 */ 121 public void register(NavigatableComponent eventSource, boolean lassoMode) { 122 this.lassoMode = lassoMode; 123 eventSource.addMouseListener(this); 124 eventSource.addMouseMotionListener(this); 125 selectionEndedListener.addPropertyChangeListener(this); 126 eventSource.addPropertyChangeListener("scale", new PropertyChangeListener(){ 127 @Override 128 public void propertyChange(PropertyChangeEvent evt) { 129 if (mousePosStart != null) { 130 paintRect(); 131 mousePos = mousePosStart = null; 132 } 133 } 134 }); 135 } 136 /** 137 * Unregister itself from the given event source. If a selection rectangle is 138 * shown, hide it first. 139 * 140 * @param eventSource The emitter of the mouse events. 141 */ 142 public void unregister(Component eventSource) { 143 eventSource.removeMouseListener(this); 144 eventSource.removeMouseMotionListener(this); 145 selectionEndedListener.removePropertyChangeListener(this); 146 } 147 148 /** 149 * If the correct button, from the "drawing rectangle" mode 150 */ 151 @Override 152 public void mousePressed(MouseEvent e) { 153 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() > 1 && Main.main.getCurrentDataSet() != null) { 154 SelectByInternalPointAction.performSelection(Main.map.mapView.getEastNorth(e.getX(), e.getY()), 155 (e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) > 0, 156 (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) > 0); 157 } else if (e.getButton() == MouseEvent.BUTTON1) { 158 mousePosStart = mousePos = e.getPoint(); 159 160 lasso.reset(); 161 lasso.addPoint(mousePosStart.x, mousePosStart.y); 162 } 163 } 164 165 /** 166 * If the correct button is hold, draw the rectangle. 167 */ 168 @Override 169 public void mouseDragged(MouseEvent e) { 170 int buttonPressed = e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK); 171 172 if (buttonPressed != 0) { 173 if (mousePosStart == null) { 174 mousePosStart = mousePos = e.getPoint(); 175 } 176 if (!lassoMode) { 177 paintRect(); 178 } 179 } 180 181 if (buttonPressed == MouseEvent.BUTTON1_DOWN_MASK) { 182 mousePos = e.getPoint(); 183 if (lassoMode) { 184 paintLasso(); 185 } else { 186 paintRect(); 187 } 188 } else if (buttonPressed == (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) { 189 mousePosStart.x += e.getX()-mousePos.x; 190 mousePosStart.y += e.getY()-mousePos.y; 191 mousePos = e.getPoint(); 192 paintRect(); 193 } 194 } 195 196 /** 197 * Check the state of the keys and buttons and set the selection accordingly. 198 */ 199 @Override 200 public void mouseReleased(MouseEvent e) { 201 if (e.getButton() != MouseEvent.BUTTON1) 202 return; 203 if (mousePos == null || mousePosStart == null) 204 return; // injected release from outside 205 // disable the selection rect 206 Rectangle r; 207 if (!lassoMode) { 208 nc.requestClearRect(); 209 r = getSelectionRectangle(); 210 211 lasso = rectToPolygon(r); 212 } else { 213 nc.requestClearPoly(); 214 lasso.addPoint(mousePos.x, mousePos.y); 215 r = lasso.getBounds(); 216 } 217 mousePosStart = null; 218 mousePos = null; 219 220 if ((e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) == 0) { 221 selectionEndedListener.selectionEnded(r, e); 222 } 223 } 224 225 /** 226 * Draws a selection rectangle on screen. 227 */ 228 private void paintRect() { 229 if (mousePos == null || mousePosStart == null || mousePos == mousePosStart) 230 return; 231 nc.requestPaintRect(getSelectionRectangle()); 232 } 233 234 private void paintLasso() { 235 if (mousePos == null || mousePosStart == null || mousePos == mousePosStart) { 236 return; 237 } 238 lasso.addPoint(mousePos.x, mousePos.y); 239 nc.requestPaintPoly(lasso); 240 } 241 242 /** 243 * Calculate and return the current selection rectangle 244 * @return A rectangle that spans from mousePos to mouseStartPos 245 */ 246 private Rectangle getSelectionRectangle() { 247 int x = mousePosStart.x; 248 int y = mousePosStart.y; 249 int w = mousePos.x - mousePosStart.x; 250 int h = mousePos.y - mousePosStart.y; 251 if (w < 0) { 252 x += w; 253 w = -w; 254 } 255 if (h < 0) { 256 y += h; 257 h = -h; 258 } 259 260 if (aspectRatio) { 261 /* Keep the aspect ratio by growing the rectangle; the 262 * rectangle is always under the cursor. */ 263 double aspectRatio = (double)nc.getWidth()/nc.getHeight(); 264 if ((double)w/h < aspectRatio) { 265 int neww = (int)(h*aspectRatio); 266 if (mousePos.x < mousePosStart.x) { 267 x += w - neww; 268 } 269 w = neww; 270 } else { 271 int newh = (int)(w/aspectRatio); 272 if (mousePos.y < mousePosStart.y) { 273 y += h - newh; 274 } 275 h = newh; 276 } 277 } 278 279 return new Rectangle(x,y,w,h); 280 } 281 282 /** 283 * If the action goes inactive, remove the selection rectangle from screen 284 */ 285 @Override 286 public void propertyChange(PropertyChangeEvent evt) { 287 if ("active".equals(evt.getPropertyName()) && !(Boolean)evt.getNewValue() && mousePosStart != null) { 288 paintRect(); 289 mousePosStart = null; 290 mousePos = null; 291 } 292 } 293 294 /** 295 * Return a list of all objects in the selection, respecting the different 296 * modifier. 297 * 298 * @param alt Whether the alt key was pressed, which means select all 299 * objects that are touched, instead those which are completely covered. 300 * @return The collection of selected objects. 301 */ 302 public Collection<OsmPrimitive> getSelectedObjects(boolean alt) { 303 304 Collection<OsmPrimitive> selection = new LinkedList<>(); 305 306 // whether user only clicked, not dragged. 307 boolean clicked = false; 308 Rectangle bounding = lasso.getBounds(); 309 if (bounding.height <= 2 && bounding.width <= 2) { 310 clicked = true; 311 } 312 313 if (clicked) { 314 Point center = new Point(lasso.xpoints[0], lasso.ypoints[0]); 315 OsmPrimitive osm = nc.getNearestNodeOrWay(center, OsmPrimitive.isSelectablePredicate, false); 316 if (osm != null) { 317 selection.add(osm); 318 } 319 } else { 320 // nodes 321 for (Node n : nc.getCurrentDataSet().getNodes()) { 322 if (n.isSelectable() && lasso.contains(nc.getPoint2D(n))) { 323 selection.add(n); 324 } 325 } 326 327 // ways 328 for (Way w : nc.getCurrentDataSet().getWays()) { 329 if (!w.isSelectable() || w.getNodesCount() == 0) { 330 continue; 331 } 332 if (alt) { 333 for (Node n : w.getNodes()) { 334 if (!n.isIncomplete() && lasso.contains(nc.getPoint2D(n))) { 335 selection.add(w); 336 break; 337 } 338 } 339 } else { 340 boolean allIn = true; 341 for (Node n : w.getNodes()) { 342 if (!n.isIncomplete() && !lasso.contains(nc.getPoint(n))) { 343 allIn = false; 344 break; 345 } 346 } 347 if (allIn) { 348 selection.add(w); 349 } 350 } 351 } 352 } 353 return selection; 354 } 355 356 private Polygon rectToPolygon(Rectangle r) { 357 Polygon poly = new Polygon(); 358 359 poly.addPoint(r.x, r.y); 360 poly.addPoint(r.x, r.y + r.height); 361 poly.addPoint(r.x + r.width, r.y + r.height); 362 poly.addPoint(r.x + r.width, r.y); 363 364 return poly; 365 } 366 367 /** 368 * Enables or disables the lasso mode. 369 * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it. 370 */ 371 public void setLassoMode(boolean lassoMode) { 372 this.lassoMode = lassoMode; 373 } 374 375 @Override 376 public void mouseClicked(MouseEvent e) {} 377 @Override 378 public void mouseEntered(MouseEvent e) {} 379 @Override 380 public void mouseExited(MouseEvent e) {} 381 @Override 382 public void mouseMoved(MouseEvent e) {} 383}