001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2001, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.gui.swing;
018:
019: // Geometry
020: import java.awt.Shape;
021: import java.awt.Rectangle;
022: import java.awt.geom.Line2D;
023: import java.awt.geom.Point2D;
024: import java.awt.geom.Ellipse2D;
025: import java.awt.geom.Rectangle2D;
026: import java.awt.geom.RoundRectangle2D;
027: import java.awt.geom.RectangularShape;
028: import java.awt.geom.AffineTransform;
029: import java.awt.geom.NoninvertibleTransformException;
030:
031: // Graphics
032: import java.awt.Color;
033: import java.awt.Component;
034: import java.awt.Graphics2D;
035:
036: // Events
037: import java.awt.event.MouseEvent;
038: import javax.swing.event.MouseInputAdapter;
039:
040: /**
041: * Controller which allows the user to select a region of a component. The user must click on a
042: * point in the component, then drag the mouse pointer whilst keeping the button pressed. During
043: * the dragging, the shape which is drawn will normally be a rectangle. Other shapes could always
044: * be used such as, for example, an ellipse. To use this class, it is necessary to create a derived
045: * class which defines the following methods:
046: *
047: * <ul>
048: * <li>{@link #selectionPerformed} (obligatory)</li>
049: * <li>{@link #getModel} (optional)</li>
050: * </ul>
051: *
052: * This controller should then be registered with one, and only one, component
053: * using the following syntax:
054: *
055: * <blockquote><pre>
056: * {@link Component} component=...
057: * MouseSelectionTracker control=...
058: * component.addMouseListener(control);
059: * </pre></blockquote>
060: *
061: * @since 2.0
062: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/MouseSelectionTracker.java $
063: * @version $Id: MouseSelectionTracker.java 22482 2006-10-31 02:58:00Z desruisseaux $
064: * @author Martin Desruisseaux
065: */
066: abstract class MouseSelectionTracker extends MouseInputAdapter {
067: /**
068: * Stippled rectangle representing the region which the user is currently
069: * selecting. This rectangle can be empty. These coordinates are only
070: * significant in the period between the user pressing the mouse button
071: * and then releasing it to outline a region. Conventionally, the
072: * {@code null} value indicates that a line should be used instead of
073: * a rectangular shape. The coordinates are always expressed in pixels.
074: */
075: private transient RectangularShape mouseSelectedArea;
076:
077: /**
078: * Colour to replace during XOR drawings on a graphic.
079: * This colour is specified in {@link Graphics2D#setColor}.
080: */
081: private Color backXORColor = Color.white;
082:
083: /**
084: * Colour to replace with during the XOR drawings on a graphic.
085: * This colour is specified in {@link Graphics2D#setXORMode}.
086: */
087: private Color lineXORColor = Color.black;
088:
089: /**
090: * <var>x</var> coordinate of the mouse when the button is pressed.
091: */
092: private transient int ox;
093:
094: /**
095: * <var>y</var> coordinate of the mouse when the button is pressed.
096: */
097: private transient int oy;
098:
099: /**
100: * <var>x</var> coordinate of the mouse during the last drag.
101: */
102: private transient int px;
103:
104: /**
105: * <var>y</var> coordinate of the mouse during the last drag.
106: */
107: private transient int py;
108:
109: /**
110: * Indicates whether a selection is underway.
111: */
112: private transient boolean isDragging;
113:
114: /**
115: * Constructs an object which will allow rectangular regions to be selected using the mouse.
116: */
117: public MouseSelectionTracker() {
118: }
119:
120: /**
121: * Specifies the colours to be used for drawing the outline of a box when
122: * the user selects a region. All {@code a} colours will be replaced
123: * by {@code b} colours and vice versa.
124: */
125: public void setXORColors(final Color a, final Color b) {
126: backXORColor = a;
127: lineXORColor = b;
128: }
129:
130: /**
131: * Returns the geometric shape to use for marking the boundaries of a region. This shape is
132: * normally a rectangle but could also be an ellipse, an arrow or even other shapes. The
133: * coordinates of the returned shape will not be taken into account. In fact, these coordinates
134: * will regularly be discarded. Only the class of the returned shape will count (for example,
135: * {@link java.awt.geom.Ellipse2D} vs {@link java.awt.geom.Rectangle2D}) and their parameters
136: * which are not linked to their position (for example, the rounding of a rectangle's
137: * corners).
138: * <p>
139: * The shape returned will normally be from a class derived from {@link RectangularShape},
140: * but could also be from the {@link Line2D} class. <strong>Any other class risks throwing a
141: * {@link ClassCastException} when executed</strong>.
142: *
143: * The default implementation always returns an object {@link Rectangle}.
144: *
145: * @param event Mouse coordinate when the button is pressed. This information can be used by
146: * the derived classes which like to be informed of the position of the mouse before
147: * chosing a geometric shape.
148: * @return Shape from the class {link RectangularShape} or {link Line2D}, or {@code null}
149: * to indicate that we do not want to make a selection.
150: */
151: protected Shape getModel(final MouseEvent event) {
152: return new Rectangle();
153: }
154:
155: /**
156: * Method which is automatically called after the user selects a region with the mouse.
157: * All coordinates passed in as parameters are expressed in pixels.
158: *
159: * @param ox <var>x</var> coordinate of the mouse when the user pressed the mouse button.
160: * @param oy <var>y</var> coordinate of the mouse when the user pressed the mouse button.
161: * @param px <var>x</var> coordinate of the mouse when the user released the mouse button.
162: * @param py <var>y</var> coordinate of the mouse when the user released the mouse button.
163: */
164: protected abstract void selectionPerformed(int ox, int oy, int px,
165: int py);
166:
167: /**
168: * Returns the geometric shape surrounding the last region to be selected by the user. An
169: * optional affine transform can be specified to convert the region selected by the user
170: * into logical coordinates. The class of the shape returned depends on the model returned by
171: * {@link #getModel}:
172: *
173: * <ul>
174: * <li>If the model is null (which means that this {@code MouseSelectionTracker} object only
175: * draws a line between points), the object returned will belong to the {@link Line2D}
176: * class.</li>
177: * <li>If the model is not null, the object returned can be from the same class (most often
178: * {@link java.awt.geom.Rectangle2D}). There could always be situations where the object
179: * returned is from another class, for example if the affine transform carries out a
180: * rotation.</li>
181: * </ul>
182: *
183: * @param transform Affine transform which converts logical coordinates into pixel coordinates.
184: * It is usually an affine transform which is used in a {@code paint(...)} method to
185: * draw shapes expressed in logical coordinates.
186: * @return A geometric shape enclosing the last region to be selected by the user, or
187: * {@code null} if no selection has yet been made.
188: * @throws NoninvertibleTransformException If the affine transform can't be inverted.
189: */
190: public Shape getSelectedArea(final AffineTransform transform)
191: throws NoninvertibleTransformException {
192: if (ox == px && oy == py) {
193: return null;
194: }
195: RectangularShape shape = mouseSelectedArea;
196: if (transform != null && !transform.isIdentity()) {
197: if (shape == null) {
198: final Point2D.Float po = new Point2D.Float(ox, oy);
199: final Point2D.Float pp = new Point2D.Float(px, py);
200: transform.inverseTransform(po, po);
201: transform.inverseTransform(pp, pp);
202: return new Line2D.Float(po, pp);
203: } else {
204: if (canReshape(shape, transform)) {
205: final Point2D.Double point = new Point2D.Double();
206: double xmin = Double.POSITIVE_INFINITY;
207: double ymin = Double.POSITIVE_INFINITY;
208: double xmax = Double.NEGATIVE_INFINITY;
209: double ymax = Double.NEGATIVE_INFINITY;
210: for (int i = 0; i < 4; i++) {
211: point.x = (i & 1) == 0 ? shape.getMinX()
212: : shape.getMaxX();
213: point.y = (i & 2) == 0 ? shape.getMinY()
214: : shape.getMaxY();
215: transform.inverseTransform(point, point);
216: if (point.x < xmin)
217: xmin = point.x;
218: if (point.x > xmax)
219: xmax = point.x;
220: if (point.y < ymin)
221: ymin = point.y;
222: if (point.y > ymax)
223: ymax = point.y;
224: }
225: if (shape instanceof Rectangle) {
226: return new Rectangle2D.Float((float) xmin,
227: (float) ymin, (float) (xmax - xmin),
228: (float) (ymax - ymin));
229: } else {
230: shape = (RectangularShape) shape.clone();
231: shape.setFrame(xmin, ymin, xmax - xmin, ymax
232: - ymin);
233: return shape;
234: }
235: } else {
236: return transform.createInverse()
237: .createTransformedShape(shape);
238: }
239: }
240: } else {
241: return (shape != null) ? (Shape) shape.clone()
242: : new Line2D.Float(ox, oy, px, py);
243: }
244: }
245:
246: /**
247: * Indicates whether we can transform {@code shape} simply by calling its
248: * {@code shape.setFrame(...)} method rather than by using the heavy artillery
249: * that is the {@code transform.createTransformedShape(shape)} method.
250: */
251: private static boolean canReshape(final RectangularShape shape,
252: final AffineTransform transform) {
253: final int type = transform.getType();
254: if ((type & AffineTransform.TYPE_GENERAL_TRANSFORM) != 0)
255: return false;
256: if ((type & AffineTransform.TYPE_MASK_ROTATION) != 0)
257: return false;
258: if ((type & AffineTransform.TYPE_FLIP) != 0) {
259: if (shape instanceof Rectangle2D)
260: return true;
261: if (shape instanceof Ellipse2D)
262: return true;
263: if (shape instanceof RoundRectangle2D)
264: return true;
265: return false;
266: }
267: return true;
268: }
269:
270: /**
271: * Returns a {@link Graphics2D} object to be used for drawing in the specified component. We
272: * must not forget to call {@link Graphics2D#dispose} when the graphics object is no longer
273: * needed.
274: */
275: private Graphics2D getGraphics(final Component c) {
276: final Graphics2D graphics = (Graphics2D) c.getGraphics();
277: graphics.setXORMode(lineXORColor);
278: graphics.setColor(backXORColor);
279: return graphics;
280: }
281:
282: /**
283: * Informs this controller that the mouse button has been pressed.
284: * The default implementation retains the mouse coordinate (which will
285: * become one of the corners of the future rectangle to be drawn)
286: * and prepares {@code this} to observe the mouse movements.
287: *
288: * @throws ClassCastException if {@link #getModel} doesn't return a shape
289: * from the class {link RectangularShape} or {link Line2D}.
290: */
291: public void mousePressed(final MouseEvent event)
292: throws ClassCastException {
293: if (!event.isConsumed()
294: && (event.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
295: final Component source = event.getComponent();
296: if (source != null) {
297: Shape model = getModel(event);
298: if (model != null) {
299: isDragging = true;
300: ox = px = event.getX();
301: oy = py = event.getY();
302: if (model instanceof Line2D) {
303: model = null;
304: }
305: mouseSelectedArea = (RectangularShape) model;
306: if (mouseSelectedArea != null) {
307: mouseSelectedArea.setFrame(ox, oy, 0, 0);
308: }
309: source.addMouseMotionListener(this );
310: }
311: source.requestFocus();
312: event.consume();
313: }
314: }
315: }
316:
317: /**
318: * Informs this controller that the mouse has been dragged. The default
319: * implementation uses this to move a corner of the rectangle used to
320: * select the region. The other corner remains fixed at the point
321: * where the mouse was at the moment its button was pressed..
322: */
323: public void mouseDragged(final MouseEvent event) {
324: if (isDragging) {
325: final Graphics2D graphics = getGraphics(event
326: .getComponent());
327: if (mouseSelectedArea == null) {
328: graphics.drawLine(ox, oy, px, py);
329: px = event.getX();
330: py = event.getY();
331: graphics.drawLine(ox, oy, px, py);
332: } else {
333: graphics.draw(mouseSelectedArea);
334: int xmin = this .ox;
335: int ymin = this .oy;
336: int xmax = px = event.getX();
337: int ymax = py = event.getY();
338: if (xmin > xmax) {
339: final int xtmp = xmin;
340: xmin = xmax;
341: xmax = xtmp;
342: }
343: if (ymin > ymax) {
344: final int ytmp = ymin;
345: ymin = ymax;
346: ymax = ytmp;
347: }
348: mouseSelectedArea.setFrame(xmin, ymin, xmax - xmin,
349: ymax - ymin);
350: graphics.draw(mouseSelectedArea);
351: }
352: graphics.dispose();
353: event.consume();
354: }
355: }
356:
357: /**
358: * Informs this controller that the mouse button has been released.
359: * The default implementation calls {@link #selectionPerformed} with
360: * the bounds of the selected region as parameters.
361: */
362: public void mouseReleased(final MouseEvent event) {
363: if (isDragging
364: && (event.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
365: isDragging = false;
366: final Component component = event.getComponent();
367: component.removeMouseMotionListener(this );
368:
369: final Graphics2D graphics = getGraphics(event
370: .getComponent());
371: if (mouseSelectedArea == null) {
372: graphics.drawLine(ox, oy, px, py);
373: } else {
374: graphics.draw(mouseSelectedArea);
375: }
376: graphics.dispose();
377: px = event.getX();
378: py = event.getY();
379: selectionPerformed(ox, oy, px, py);
380: event.consume();
381: }
382: }
383:
384: /**
385: * Informs this controller that the mouse has been moved but not as a
386: * result of the user selecting a region. The default implementation
387: * signals to the source component that {@code this} is no longer
388: * interested in being informed about mouse movements.
389: */
390: public void mouseMoved(final MouseEvent event) {
391: // Normally not necessary, but it seems that this "listener"
392: // sometimes stays in place when it shouldn't.
393: event.getComponent().removeMouseMotionListener(this);
394: }
395: }
|