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