001 // License: GPL. Copyright 2007 by Immanuel Scholz and others 002 package org.openstreetmap.josm.gui; 003 004 import java.awt.Color; 005 import java.awt.Component; 006 import java.awt.Graphics; 007 import java.awt.Point; 008 import java.awt.Polygon; 009 import java.awt.Rectangle; 010 import java.awt.event.InputEvent; 011 import java.awt.event.MouseEvent; 012 import java.awt.event.MouseListener; 013 import java.awt.event.MouseMotionListener; 014 import java.beans.PropertyChangeEvent; 015 import java.beans.PropertyChangeListener; 016 import java.util.Collection; 017 import java.util.LinkedList; 018 019 import org.openstreetmap.josm.data.osm.Node; 020 import org.openstreetmap.josm.data.osm.OsmPrimitive; 021 import 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 */ 048 public 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 alt Whether the alt key was pressed 060 * @param shift Whether the shift key was pressed 061 * @param ctrl Whether the ctrl key was pressed 062 * @see InputEvent#getModifiersEx() 063 */ 064 public void selectionEnded(Rectangle r, MouseEvent e); 065 /** 066 * Called to register the selection manager for "active" property. 067 * @param listener The listener to register 068 */ 069 public void addPropertyChangeListener(PropertyChangeListener listener); 070 /** 071 * Called to remove the selection manager from the listener list 072 * for "active" property. 073 * @param listener The listener to register 074 */ 075 public void removePropertyChangeListener(PropertyChangeListener listener); 076 } 077 /** 078 * The listener that receives the events after left mouse button is released. 079 */ 080 private final SelectionEnded selectionEndedListener; 081 /** 082 * Position of the map when the mouse button was pressed. 083 * If this is not <code>null</code>, a rectangle is drawn on screen. 084 */ 085 private Point mousePosStart; 086 /** 087 * Position of the map when the selection rectangle was last drawn. 088 */ 089 private Point mousePos; 090 /** 091 * The Component, the selection rectangle is drawn onto. 092 */ 093 private final NavigatableComponent nc; 094 /** 095 * Whether the selection rectangle must obtain the aspect ratio of the 096 * drawComponent. 097 */ 098 private boolean aspectRatio; 099 100 private boolean lassoMode; 101 private Polygon lasso = new Polygon(); 102 103 /** 104 * Create a new SelectionManager. 105 * 106 * @param selectionEndedListener The action listener that receives the event when 107 * the left button is released. 108 * @param aspectRatio If true, the selection window must obtain the aspect 109 * ratio of the drawComponent. 110 * @param navComp The component, the rectangle is drawn onto. 111 */ 112 public SelectionManager(SelectionEnded selectionEndedListener, boolean aspectRatio, NavigatableComponent navComp) { 113 this.selectionEndedListener = selectionEndedListener; 114 this.aspectRatio = aspectRatio; 115 this.nc = navComp; 116 } 117 118 /** 119 * Register itself at the given event source. 120 * @param eventSource The emitter of the mouse events. 121 */ 122 public void register(NavigatableComponent eventSource, boolean lassoMode) { 123 this.lassoMode = lassoMode; 124 eventSource.addMouseListener(this); 125 eventSource.addMouseMotionListener(this); 126 selectionEndedListener.addPropertyChangeListener(this); 127 eventSource.addPropertyChangeListener("scale", new PropertyChangeListener(){ 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 public void mousePressed(MouseEvent e) { 152 if (e.getButton() == MouseEvent.BUTTON1) { 153 mousePosStart = mousePos = e.getPoint(); 154 155 lasso.reset(); 156 lasso.addPoint(mousePosStart.x, mousePosStart.y); 157 } 158 } 159 160 /** 161 * If the correct button is hold, draw the rectangle. 162 */ 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 public void mouseReleased(MouseEvent e) { 194 if (e.getButton() != MouseEvent.BUTTON1) 195 return; 196 if (mousePos == null || mousePosStart == null) 197 return; // injected release from outside 198 // disable the selection rect 199 Rectangle r; 200 if (!lassoMode) { 201 paintRect(); 202 r = getSelectionRectangle(); 203 204 lasso = rectToPolygon(r); 205 } else { 206 lasso.addPoint(mousePos.x, mousePos.y); 207 r = lasso.getBounds(); 208 } 209 mousePosStart = null; 210 mousePos = null; 211 212 if ((e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) == 0) { 213 selectionEndedListener.selectionEnded(r, e); 214 } 215 } 216 217 /** 218 * Draw a selection rectangle on screen. If already a rectangle is drawn, 219 * it is removed instead. 220 */ 221 private void paintRect() { 222 if (mousePos == null || mousePosStart == null || mousePos == mousePosStart) 223 return; 224 Graphics g = nc.getGraphics(); 225 g.setColor(Color.BLACK); 226 g.setXORMode(Color.WHITE); 227 228 Rectangle r = getSelectionRectangle(); 229 g.drawRect(r.x,r.y,r.width,r.height); 230 } 231 232 private void paintLasso() { 233 if (mousePos == null || mousePosStart == null || mousePos == mousePosStart) { 234 return; 235 } 236 237 Graphics g = nc.getGraphics(); 238 g.setColor(Color.WHITE); 239 240 int lastPosX = lasso.xpoints[lasso.npoints - 1]; 241 int lastPosY = lasso.ypoints[lasso.npoints - 1]; 242 g.drawLine(lastPosX, lastPosY, mousePos.x, mousePos.y); 243 244 lasso.addPoint(mousePos.x, mousePos.y); 245 } 246 247 /** 248 * Calculate and return the current selection rectangle 249 * @return A rectangle that spans from mousePos to mouseStartPos 250 */ 251 private Rectangle getSelectionRectangle() { 252 int x = mousePosStart.x; 253 int y = mousePosStart.y; 254 int w = mousePos.x - mousePosStart.x; 255 int h = mousePos.y - mousePosStart.y; 256 if (w < 0) { 257 x += w; 258 w = -w; 259 } 260 if (h < 0) { 261 y += h; 262 h = -h; 263 } 264 265 if (aspectRatio) { 266 /* Keep the aspect ratio by growing the rectangle; the 267 * rectangle is always under the cursor. */ 268 double aspectRatio = (double)nc.getWidth()/nc.getHeight(); 269 if ((double)w/h < aspectRatio) { 270 int neww = (int)(h*aspectRatio); 271 if (mousePos.x < mousePosStart.x) { 272 x += w - neww; 273 } 274 w = neww; 275 } else { 276 int newh = (int)(w/aspectRatio); 277 if (mousePos.y < mousePosStart.y) { 278 y += h - newh; 279 } 280 h = newh; 281 } 282 } 283 284 return new Rectangle(x,y,w,h); 285 } 286 287 /** 288 * If the action goes inactive, remove the selection rectangle from screen 289 */ 290 public void propertyChange(PropertyChangeEvent evt) { 291 if (evt.getPropertyName().equals("active") && !(Boolean)evt.getNewValue() && mousePosStart != null) { 292 paintRect(); 293 mousePosStart = null; 294 mousePos = null; 295 } 296 } 297 298 /** 299 * Return a list of all objects in the selection, respecting the different 300 * modifier. 301 * 302 * @param alt Whether the alt key was pressed, which means select all 303 * objects that are touched, instead those which are completely covered. 304 */ 305 public Collection<OsmPrimitive> getSelectedObjects(boolean alt) { 306 307 Collection<OsmPrimitive> selection = new LinkedList<OsmPrimitive>(); 308 309 // whether user only clicked, not dragged. 310 boolean clicked = false; 311 Rectangle bounding = lasso.getBounds(); 312 if (bounding.height <= 2 && bounding.width <= 2) { 313 clicked = true; 314 } 315 316 if (clicked) { 317 Point center = new Point(lasso.xpoints[0], lasso.ypoints[0]); 318 OsmPrimitive osm = nc.getNearestNodeOrWay(center, OsmPrimitive.isSelectablePredicate, false); 319 if (osm != null) { 320 selection.add(osm); 321 } 322 } else { 323 // nodes 324 for (Node n : nc.getCurrentDataSet().getNodes()) { 325 if (n.isSelectable() && lasso.contains(nc.getPoint(n))) { 326 selection.add(n); 327 } 328 } 329 330 // ways 331 for (Way w : nc.getCurrentDataSet().getWays()) { 332 if (!w.isSelectable() || w.getNodesCount() == 0) { 333 continue; 334 } 335 if (alt) { 336 for (Node n : w.getNodes()) { 337 if (!n.isIncomplete() && lasso.contains(nc.getPoint(n))) { 338 selection.add(w); 339 break; 340 } 341 } 342 } else { 343 boolean allIn = true; 344 for (Node n : w.getNodes()) { 345 if (!n.isIncomplete() && !lasso.contains(nc.getPoint(n))) { 346 allIn = false; 347 break; 348 } 349 } 350 if (allIn) { 351 selection.add(w); 352 } 353 } 354 } 355 } 356 return selection; 357 } 358 359 private Polygon rectToPolygon(Rectangle r) { 360 Polygon poly = new Polygon(); 361 362 poly.addPoint(r.x, r.y); 363 poly.addPoint(r.x, r.y + r.height); 364 poly.addPoint(r.x + r.width, r.y + r.height); 365 poly.addPoint(r.x + r.width, r.y); 366 367 return poly; 368 } 369 370 public void setLassoMode(boolean lassoMode) { 371 this.lassoMode = lassoMode; 372 } 373 374 public void mouseClicked(MouseEvent e) {} 375 public void mouseEntered(MouseEvent e) {} 376 public void mouseExited(MouseEvent e) {} 377 public void mouseMoved(MouseEvent e) {} 378 }