001 // License: GPL. Copyright 2007 by Immanuel Scholz and others 002 package org.openstreetmap.josm.gui; 003 004 import java.awt.Component; 005 import java.awt.Point; 006 import java.awt.Polygon; 007 import java.awt.Rectangle; 008 import java.awt.event.InputEvent; 009 import java.awt.event.MouseEvent; 010 import java.awt.event.MouseListener; 011 import java.awt.event.MouseMotionListener; 012 import java.beans.PropertyChangeEvent; 013 import java.beans.PropertyChangeListener; 014 import java.util.Collection; 015 import java.util.LinkedList; 016 017 import org.openstreetmap.josm.data.osm.Node; 018 import org.openstreetmap.josm.data.osm.OsmPrimitive; 019 import 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 */ 046 public 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 public void propertyChange(PropertyChangeEvent evt) { 126 if (mousePosStart != null) { 127 paintRect(); 128 mousePos = mousePosStart = null; 129 } 130 } 131 }); 132 } 133 /** 134 * Unregister itself from the given event source. If a selection rectangle is 135 * shown, hide it first. 136 * 137 * @param eventSource The emitter of the mouse events. 138 */ 139 public void unregister(Component eventSource) { 140 eventSource.removeMouseListener(this); 141 eventSource.removeMouseMotionListener(this); 142 selectionEndedListener.removePropertyChangeListener(this); 143 } 144 145 /** 146 * If the correct button, from the "drawing rectangle" mode 147 */ 148 public void mousePressed(MouseEvent e) { 149 if (e.getButton() == MouseEvent.BUTTON1) { 150 mousePosStart = mousePos = e.getPoint(); 151 152 lasso.reset(); 153 lasso.addPoint(mousePosStart.x, mousePosStart.y); 154 } 155 } 156 157 /** 158 * If the correct button is hold, draw the rectangle. 159 */ 160 public void mouseDragged(MouseEvent e) { 161 int buttonPressed = e.getModifiersEx() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK); 162 163 if (buttonPressed != 0) { 164 if (mousePosStart == null) { 165 mousePosStart = mousePos = e.getPoint(); 166 } 167 if (!lassoMode) { 168 paintRect(); 169 } 170 } 171 172 if (buttonPressed == MouseEvent.BUTTON1_DOWN_MASK) { 173 mousePos = e.getPoint(); 174 if (lassoMode) { 175 paintLasso(); 176 } else { 177 paintRect(); 178 } 179 } else if (buttonPressed == (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) { 180 mousePosStart.x += e.getX()-mousePos.x; 181 mousePosStart.y += e.getY()-mousePos.y; 182 mousePos = e.getPoint(); 183 paintRect(); 184 } 185 } 186 187 /** 188 * Check the state of the keys and buttons and set the selection accordingly. 189 */ 190 public void mouseReleased(MouseEvent e) { 191 if (e.getButton() != MouseEvent.BUTTON1) 192 return; 193 if (mousePos == null || mousePosStart == null) 194 return; // injected release from outside 195 // disable the selection rect 196 Rectangle r; 197 if (!lassoMode) { 198 nc.requestClearRect(); 199 r = getSelectionRectangle(); 200 201 lasso = rectToPolygon(r); 202 } else { 203 nc.requestClearPoly(); 204 lasso.addPoint(mousePos.x, mousePos.y); 205 r = lasso.getBounds(); 206 } 207 mousePosStart = null; 208 mousePos = null; 209 210 if ((e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) == 0) { 211 selectionEndedListener.selectionEnded(r, e); 212 } 213 } 214 215 /** 216 * Draws a selection rectangle on screen. 217 */ 218 private void paintRect() { 219 if (mousePos == null || mousePosStart == null || mousePos == mousePosStart) 220 return; 221 nc.requestPaintRect(getSelectionRectangle()); 222 } 223 224 private void paintLasso() { 225 if (mousePos == null || mousePosStart == null || mousePos == mousePosStart) { 226 return; 227 } 228 lasso.addPoint(mousePos.x, mousePos.y); 229 nc.requestPaintPoly(lasso); 230 } 231 232 /** 233 * Calculate and return the current selection rectangle 234 * @return A rectangle that spans from mousePos to mouseStartPos 235 */ 236 private Rectangle getSelectionRectangle() { 237 int x = mousePosStart.x; 238 int y = mousePosStart.y; 239 int w = mousePos.x - mousePosStart.x; 240 int h = mousePos.y - mousePosStart.y; 241 if (w < 0) { 242 x += w; 243 w = -w; 244 } 245 if (h < 0) { 246 y += h; 247 h = -h; 248 } 249 250 if (aspectRatio) { 251 /* Keep the aspect ratio by growing the rectangle; the 252 * rectangle is always under the cursor. */ 253 double aspectRatio = (double)nc.getWidth()/nc.getHeight(); 254 if ((double)w/h < aspectRatio) { 255 int neww = (int)(h*aspectRatio); 256 if (mousePos.x < mousePosStart.x) { 257 x += w - neww; 258 } 259 w = neww; 260 } else { 261 int newh = (int)(w/aspectRatio); 262 if (mousePos.y < mousePosStart.y) { 263 y += h - newh; 264 } 265 h = newh; 266 } 267 } 268 269 return new Rectangle(x,y,w,h); 270 } 271 272 /** 273 * If the action goes inactive, remove the selection rectangle from screen 274 */ 275 public void propertyChange(PropertyChangeEvent evt) { 276 if (evt.getPropertyName().equals("active") && !(Boolean)evt.getNewValue() && mousePosStart != null) { 277 paintRect(); 278 mousePosStart = null; 279 mousePos = null; 280 } 281 } 282 283 /** 284 * Return a list of all objects in the selection, respecting the different 285 * modifier. 286 * 287 * @param alt Whether the alt key was pressed, which means select all 288 * objects that are touched, instead those which are completely covered. 289 * @return The collection of selected objects. 290 */ 291 public Collection<OsmPrimitive> getSelectedObjects(boolean alt) { 292 293 Collection<OsmPrimitive> selection = new LinkedList<OsmPrimitive>(); 294 295 // whether user only clicked, not dragged. 296 boolean clicked = false; 297 Rectangle bounding = lasso.getBounds(); 298 if (bounding.height <= 2 && bounding.width <= 2) { 299 clicked = true; 300 } 301 302 if (clicked) { 303 Point center = new Point(lasso.xpoints[0], lasso.ypoints[0]); 304 OsmPrimitive osm = nc.getNearestNodeOrWay(center, OsmPrimitive.isSelectablePredicate, false); 305 if (osm != null) { 306 selection.add(osm); 307 } 308 } else { 309 // nodes 310 for (Node n : nc.getCurrentDataSet().getNodes()) { 311 if (n.isSelectable() && lasso.contains(nc.getPoint(n))) { 312 selection.add(n); 313 } 314 } 315 316 // ways 317 for (Way w : nc.getCurrentDataSet().getWays()) { 318 if (!w.isSelectable() || w.getNodesCount() == 0) { 319 continue; 320 } 321 if (alt) { 322 for (Node n : w.getNodes()) { 323 if (!n.isIncomplete() && lasso.contains(nc.getPoint(n))) { 324 selection.add(w); 325 break; 326 } 327 } 328 } else { 329 boolean allIn = true; 330 for (Node n : w.getNodes()) { 331 if (!n.isIncomplete() && !lasso.contains(nc.getPoint(n))) { 332 allIn = false; 333 break; 334 } 335 } 336 if (allIn) { 337 selection.add(w); 338 } 339 } 340 } 341 } 342 return selection; 343 } 344 345 private Polygon rectToPolygon(Rectangle r) { 346 Polygon poly = new Polygon(); 347 348 poly.addPoint(r.x, r.y); 349 poly.addPoint(r.x, r.y + r.height); 350 poly.addPoint(r.x + r.width, r.y + r.height); 351 poly.addPoint(r.x + r.width, r.y); 352 353 return poly; 354 } 355 356 /** 357 * Enables or disables the lasso mode. 358 * @param lassoMode {@code true} to enable lasso mode, {@code false} to disable it. 359 */ 360 public void setLassoMode(boolean lassoMode) { 361 this.lassoMode = lassoMode; 362 } 363 364 public void mouseClicked(MouseEvent e) {} 365 public void mouseEntered(MouseEvent e) {} 366 public void mouseExited(MouseEvent e) {} 367 public void mouseMoved(MouseEvent e) {} 368 }