1 |
// Migration process to Geotools 2.6: |
2 |
// Because not included in gt2-2.6-M2 this class is taken 1:1 |
3 |
// from Geotools 2.4.5 and made public. |
4 |
// TODO: It should be removed or included in SCHMITZM. |
5 |
|
6 |
/* |
7 |
* GeoTools - OpenSource mapping toolkit |
8 |
* http://geotools.org |
9 |
* (C) 2003-2006, Geotools Project Managment Committee (PMC) |
10 |
* (C) 2001, Institut de Recherche pour le D�veloppement |
11 |
* |
12 |
* This library is free software; you can redistribute it and/or |
13 |
* modify it under the terms of the GNU Lesser General Public |
14 |
* License as published by the Free Software Foundation; either |
15 |
* version 2.1 of the License, or (at your option) any later version. |
16 |
* |
17 |
* This library is distributed in the hope that it will be useful, |
18 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
19 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
20 |
* Lesser General Public License for more details. |
21 |
*/ |
22 |
package gtmig.org.geotools.swing; |
23 |
|
24 |
// Geometry |
25 |
import java.awt.Color; |
26 |
import java.awt.Component; |
27 |
import java.awt.Graphics2D; |
28 |
import java.awt.Rectangle; |
29 |
import java.awt.Shape; |
30 |
import java.awt.event.MouseEvent; |
31 |
import java.awt.geom.AffineTransform; |
32 |
import java.awt.geom.Ellipse2D; |
33 |
import java.awt.geom.Line2D; |
34 |
import java.awt.geom.NoninvertibleTransformException; |
35 |
import java.awt.geom.Point2D; |
36 |
import java.awt.geom.Rectangle2D; |
37 |
import java.awt.geom.RectangularShape; |
38 |
import java.awt.geom.RoundRectangle2D; |
39 |
|
40 |
import javax.swing.event.MouseInputAdapter; |
41 |
|
42 |
|
43 |
/** |
44 |
* Controller which allows the user to select a region of a component. The user must click on a |
45 |
* point in the component, then drag the mouse pointer whilst keeping the button pressed. During |
46 |
* the dragging, the shape which is drawn will normally be a rectangle. Other shapes could always |
47 |
* be used such as, for example, an ellipse. To use this class, it is necessary to create a derived |
48 |
* class which defines the following methods: |
49 |
* |
50 |
* <ul> |
51 |
* <li>{@link #selectionPerformed} (obligatory)</li> |
52 |
* <li>{@link #getModel} (optional)</li> |
53 |
* </ul> |
54 |
* |
55 |
* This controller should then be registered with one, and only one, component |
56 |
* using the following syntax: |
57 |
* |
58 |
* <blockquote><pre> |
59 |
* {@link Component} component=... |
60 |
* MouseSelectionTracker control=... |
61 |
* component.addMouseListener(control); |
62 |
* </pre></blockquote> |
63 |
* |
64 |
* @since 2.0 |
65 |
* @source $URL: http://svn.geotools.org/tags/2.4.5/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/MouseSelectionTracker.java $ |
66 |
* @version $Id: MouseSelectionTracker.java 22482 2006-10-31 02:58:00Z desruisseaux $ |
67 |
* @author Martin Desruisseaux |
68 |
*/ |
69 |
public abstract class MouseSelectionTracker extends MouseInputAdapter { |
70 |
/** |
71 |
* Stippled rectangle representing the region which the user is currently |
72 |
* selecting. This rectangle can be empty. These coordinates are only |
73 |
* significant in the period between the user pressing the mouse button |
74 |
* and then releasing it to outline a region. Conventionally, the |
75 |
* {@code null} value indicates that a line should be used instead of |
76 |
* a rectangular shape. The coordinates are always expressed in pixels. |
77 |
*/ |
78 |
private transient RectangularShape mouseSelectedArea; |
79 |
|
80 |
/** |
81 |
* Colour to replace during XOR drawings on a graphic. |
82 |
* This colour is specified in {@link Graphics2D#setColor}. |
83 |
*/ |
84 |
private Color backXORColor = Color.white; |
85 |
|
86 |
/** |
87 |
* Colour to replace with during the XOR drawings on a graphic. |
88 |
* This colour is specified in {@link Graphics2D#setXORMode}. |
89 |
*/ |
90 |
private Color lineXORColor = Color.black; |
91 |
|
92 |
/** |
93 |
* <var>x</var> coordinate of the mouse when the button is pressed. |
94 |
*/ |
95 |
private transient int ox; |
96 |
|
97 |
/** |
98 |
* <var>y</var> coordinate of the mouse when the button is pressed. |
99 |
*/ |
100 |
private transient int oy; |
101 |
|
102 |
/** |
103 |
* <var>x</var> coordinate of the mouse during the last drag. |
104 |
*/ |
105 |
private transient int px; |
106 |
|
107 |
/** |
108 |
* <var>y</var> coordinate of the mouse during the last drag. |
109 |
*/ |
110 |
private transient int py; |
111 |
|
112 |
/** |
113 |
* Indicates whether a selection is underway. |
114 |
*/ |
115 |
private transient boolean isDragging; |
116 |
|
117 |
/** |
118 |
* Constructs an object which will allow rectangular regions to be selected using the mouse. |
119 |
*/ |
120 |
public MouseSelectionTracker() { |
121 |
} |
122 |
|
123 |
/** |
124 |
* Specifies the colours to be used for drawing the outline of a box when |
125 |
* the user selects a region. All {@code a} colours will be replaced |
126 |
* by {@code b} colours and vice versa. |
127 |
*/ |
128 |
public void setXORColors(final Color a, final Color b) { |
129 |
backXORColor = a; |
130 |
lineXORColor = b; |
131 |
} |
132 |
|
133 |
/** |
134 |
* Returns the geometric shape to use for marking the boundaries of a region. This shape is |
135 |
* normally a rectangle but could also be an ellipse, an arrow or even other shapes. The |
136 |
* coordinates of the returned shape will not be taken into account. In fact, these coordinates |
137 |
* will regularly be discarded. Only the class of the returned shape will count (for example, |
138 |
* {@link java.awt.geom.Ellipse2D} vs {@link java.awt.geom.Rectangle2D}) and their parameters |
139 |
* which are not linked to their position (for example, the rounding of a rectangle's |
140 |
* corners). |
141 |
* <p> |
142 |
* The shape returned will normally be from a class derived from {@link RectangularShape}, |
143 |
* but could also be from the {@link Line2D} class. <strong>Any other class risks throwing a |
144 |
* {@link ClassCastException} when executed</strong>. |
145 |
* |
146 |
* The default implementation always returns an object {@link Rectangle}. |
147 |
* |
148 |
* @param event Mouse coordinate when the button is pressed. This information can be used by |
149 |
* the derived classes which like to be informed of the position of the mouse before |
150 |
* chosing a geometric shape. |
151 |
* @return Shape from the class {link RectangularShape} or {link Line2D}, or {@code null} |
152 |
* to indicate that we do not want to make a selection. |
153 |
*/ |
154 |
protected Shape getModel(final MouseEvent event) { |
155 |
return new Rectangle(); |
156 |
} |
157 |
|
158 |
/** |
159 |
* Method which is automatically called after the user selects a region with the mouse. |
160 |
* All coordinates passed in as parameters are expressed in pixels. |
161 |
* |
162 |
* @param ox <var>x</var> coordinate of the mouse when the user pressed the mouse button. |
163 |
* @param oy <var>y</var> coordinate of the mouse when the user pressed the mouse button. |
164 |
* @param px <var>x</var> coordinate of the mouse when the user released the mouse button. |
165 |
* @param py <var>y</var> coordinate of the mouse when the user released the mouse button. |
166 |
*/ |
167 |
protected abstract void selectionPerformed(int ox, int oy, int px, int py); |
168 |
|
169 |
/** |
170 |
* Returns the geometric shape surrounding the last region to be selected by the user. An |
171 |
* optional affine transform can be specified to convert the region selected by the user |
172 |
* into logical coordinates. The class of the shape returned depends on the model returned by |
173 |
* {@link #getModel}: |
174 |
* |
175 |
* <ul> |
176 |
* <li>If the model is null (which means that this {@code MouseSelectionTracker} object only |
177 |
* draws a line between points), the object returned will belong to the {@link Line2D} |
178 |
* class.</li> |
179 |
* <li>If the model is not null, the object returned can be from the same class (most often |
180 |
* {@link java.awt.geom.Rectangle2D}). There could always be situations where the object |
181 |
* returned is from another class, for example if the affine transform carries out a |
182 |
* rotation.</li> |
183 |
* </ul> |
184 |
* |
185 |
* @param transform Affine transform which converts logical coordinates into pixel coordinates. |
186 |
* It is usually an affine transform which is used in a {@code paint(...)} method to |
187 |
* draw shapes expressed in logical coordinates. |
188 |
* @return A geometric shape enclosing the last region to be selected by the user, or |
189 |
* {@code null} if no selection has yet been made. |
190 |
* @throws NoninvertibleTransformException If the affine transform can't be inverted. |
191 |
*/ |
192 |
public Shape getSelectedArea(final AffineTransform transform) throws NoninvertibleTransformException { |
193 |
if (ox == px && oy == py) { |
194 |
return null; |
195 |
} |
196 |
RectangularShape shape = mouseSelectedArea; |
197 |
if (transform != null && !transform.isIdentity()) { |
198 |
if (shape == null) { |
199 |
final Point2D.Float po = new Point2D.Float(ox, oy); |
200 |
final Point2D.Float pp = new Point2D.Float(px, py); |
201 |
transform.inverseTransform(po, po); |
202 |
transform.inverseTransform(pp, pp); |
203 |
return new Line2D.Float(po, pp); |
204 |
} else { |
205 |
if (canReshape(shape, transform)) { |
206 |
final Point2D.Double point = new Point2D.Double(); |
207 |
double xmin = Double.POSITIVE_INFINITY; |
208 |
double ymin = Double.POSITIVE_INFINITY; |
209 |
double xmax = Double.NEGATIVE_INFINITY; |
210 |
double ymax = Double.NEGATIVE_INFINITY; |
211 |
for (int i = 0; i < 4; i++) { |
212 |
point.x = (i&1) == 0 ? shape.getMinX() : shape.getMaxX(); |
213 |
point.y = (i&2) == 0 ? shape.getMinY() : shape.getMaxY(); |
214 |
transform.inverseTransform(point, point); |
215 |
if (point.x < xmin) xmin = point.x; |
216 |
if (point.x > xmax) xmax = point.x; |
217 |
if (point.y < ymin) ymin = point.y; |
218 |
if (point.y > ymax) ymax = point.y; |
219 |
} |
220 |
if (shape instanceof Rectangle) { |
221 |
return new Rectangle2D.Float((float) xmin, |
222 |
(float) ymin, |
223 |
(float) (xmax - xmin), |
224 |
(float) (ymax - ymin)); |
225 |
} else { |
226 |
shape = (RectangularShape) shape.clone(); |
227 |
shape.setFrame(xmin, ymin, xmax - xmin, ymax - ymin); |
228 |
return shape; |
229 |
} |
230 |
} |
231 |
else { |
232 |
return transform.createInverse().createTransformedShape(shape); |
233 |
} |
234 |
} |
235 |
} |
236 |
else { |
237 |
return (shape != null) ? (Shape) shape.clone() : new Line2D.Float(ox, oy, px, py); |
238 |
} |
239 |
} |
240 |
|
241 |
/** |
242 |
* Indicates whether we can transform {@code shape} simply by calling its |
243 |
* {@code shape.setFrame(...)} method rather than by using the heavy artillery |
244 |
* that is the {@code transform.createTransformedShape(shape)} method. |
245 |
*/ |
246 |
private static boolean canReshape(final RectangularShape shape, |
247 |
final AffineTransform transform) { |
248 |
final int type=transform.getType(); |
249 |
if ((type & AffineTransform.TYPE_GENERAL_TRANSFORM) != 0) return false; |
250 |
if ((type & AffineTransform.TYPE_MASK_ROTATION) != 0) return false; |
251 |
if ((type & AffineTransform.TYPE_FLIP) != 0) { |
252 |
if (shape instanceof Rectangle2D) return true; |
253 |
if (shape instanceof Ellipse2D) return true; |
254 |
if (shape instanceof RoundRectangle2D) return true; |
255 |
return false; |
256 |
} |
257 |
return true; |
258 |
} |
259 |
|
260 |
/** |
261 |
* Returns a {@link Graphics2D} object to be used for drawing in the specified component. We |
262 |
* must not forget to call {@link Graphics2D#dispose} when the graphics object is no longer |
263 |
* needed. |
264 |
*/ |
265 |
private Graphics2D getGraphics(final Component c) { |
266 |
final Graphics2D graphics = (Graphics2D) c.getGraphics(); |
267 |
graphics.setXORMode(lineXORColor); |
268 |
graphics.setColor (backXORColor); |
269 |
return graphics; |
270 |
} |
271 |
|
272 |
/** |
273 |
* Informs this controller that the mouse button has been pressed. |
274 |
* The default implementation retains the mouse coordinate (which will |
275 |
* become one of the corners of the future rectangle to be drawn) |
276 |
* and prepares {@code this} to observe the mouse movements. |
277 |
* |
278 |
* @throws ClassCastException if {@link #getModel} doesn't return a shape |
279 |
* from the class {link RectangularShape} or {link Line2D}. |
280 |
*/ |
281 |
public void mousePressed(final MouseEvent event) throws ClassCastException { |
282 |
if (!event.isConsumed() && (event.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) { |
283 |
final Component source = event.getComponent(); |
284 |
if (source != null) { |
285 |
Shape model = getModel(event); |
286 |
if (model != null) { |
287 |
isDragging = true; |
288 |
ox = px = event.getX(); |
289 |
oy = py = event.getY(); |
290 |
if (model instanceof Line2D) { |
291 |
model = null; |
292 |
} |
293 |
mouseSelectedArea = (RectangularShape) model; |
294 |
if (mouseSelectedArea != null) { |
295 |
mouseSelectedArea.setFrame(ox, oy, 0, 0); |
296 |
} |
297 |
source.addMouseMotionListener(this); |
298 |
} |
299 |
source.requestFocus(); |
300 |
event.consume(); |
301 |
} |
302 |
} |
303 |
} |
304 |
|
305 |
/** |
306 |
* Informs this controller that the mouse has been dragged. The default |
307 |
* implementation uses this to move a corner of the rectangle used to |
308 |
* select the region. The other corner remains fixed at the point |
309 |
* where the mouse was at the moment its button was pressed.. |
310 |
*/ |
311 |
public void mouseDragged(final MouseEvent event) { |
312 |
if (isDragging) { |
313 |
final Graphics2D graphics = getGraphics(event.getComponent()); |
314 |
if (mouseSelectedArea == null) { |
315 |
graphics.drawLine(ox, oy, px, py); |
316 |
px = event.getX(); |
317 |
py = event.getY(); |
318 |
graphics.drawLine(ox, oy, px, py); |
319 |
} else { |
320 |
graphics.draw(mouseSelectedArea); |
321 |
int xmin = this.ox; |
322 |
int ymin = this.oy; |
323 |
int xmax = px = event.getX(); |
324 |
int ymax = py = event.getY(); |
325 |
if (xmin > xmax) { |
326 |
final int xtmp = xmin; |
327 |
xmin = xmax; xmax = xtmp; |
328 |
} |
329 |
if (ymin > ymax) { |
330 |
final int ytmp = ymin; |
331 |
ymin = ymax; ymax = ytmp; |
332 |
} |
333 |
mouseSelectedArea.setFrame(xmin, ymin, xmax - xmin, ymax - ymin); |
334 |
graphics.draw(mouseSelectedArea); |
335 |
} |
336 |
graphics.dispose(); |
337 |
event.consume(); |
338 |
} |
339 |
} |
340 |
|
341 |
/** |
342 |
* Informs this controller that the mouse button has been released. |
343 |
* The default implementation calls {@link #selectionPerformed} with |
344 |
* the bounds of the selected region as parameters. |
345 |
*/ |
346 |
public void mouseReleased(final MouseEvent event) { |
347 |
if (isDragging && (event.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) { |
348 |
isDragging = false; |
349 |
final Component component = event.getComponent(); |
350 |
component.removeMouseMotionListener(this); |
351 |
|
352 |
final Graphics2D graphics = getGraphics(event.getComponent()); |
353 |
if (mouseSelectedArea == null) { |
354 |
graphics.drawLine(ox, oy, px, py); |
355 |
} else { |
356 |
graphics.draw(mouseSelectedArea); |
357 |
} |
358 |
graphics.dispose(); |
359 |
px = event.getX(); |
360 |
py = event.getY(); |
361 |
selectionPerformed(ox, oy, px, py); |
362 |
event.consume(); |
363 |
} |
364 |
} |
365 |
|
366 |
/** |
367 |
* Informs this controller that the mouse has been moved but not as a |
368 |
* result of the user selecting a region. The default implementation |
369 |
* signals to the source component that {@code this} is no longer |
370 |
* interested in being informed about mouse movements. |
371 |
*/ |
372 |
public void mouseMoved(final MouseEvent event) { |
373 |
// Normally not necessary, but it seems that this "listener" |
374 |
// sometimes stays in place when it shouldn't. |
375 |
event.getComponent().removeMouseMotionListener(this); |
376 |
} |
377 |
} |