/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2001-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.gui.swing;
import java.awt.Shape;
import java.awt.Rectangle;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.geom.RectangularShape;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import javax.swing.event.MouseInputAdapter;
/**
* Controller which allows the user to select a region of a component. The user must click on a
* point in the component, then drag the mouse pointer whilst keeping the button pressed. During
* the dragging, the shape which is drawn is usually a rectangle. But other shapes can be used
* such as, for example, an ellipse. To use this class, it is necessary to create a derived class
* which defines the following methods:
* <p>
* <ul>
* <li>{@link #selectionPerformed} (mandatory)</li>
* <li>{@link #getModel} (optional)</li>
* </ul>
* <p>
* This controller should then be registered with one, and only one, component
* using the following syntax:
*
* {@preformat java
* Component component = ...
* MouseSelectionTracker control = ...
* component.addMouseListener(control);
* }
*
* @author Martin Desruisseaux (IRD)
* @version 3.00
*
* @since 2.0
* @module
*/
public abstract class MouseSelectionTracker extends MouseInputAdapter {
/**
* Stippled rectangle representing the region which the user is currently
* selecting. This rectangle can be empty. These coordinates are only
* significant in the period between the user pressing the mouse button
* and then releasing it to outline a region. Conventionally, the
* {@code null} value indicates that a line should be used instead of
* a rectangular shape. The coordinates are always expressed in pixels.
*/
private transient RectangularShape mouseSelectedArea;
/**
* Color to replace during XOR drawings on a graphic.
* This colour is specified in {@link Graphics2D#setColor}.
*/
private Color backXORColor = Color.white;
/**
* Color to replace with during the XOR drawings on a graphic.
* This colour is specified in {@link Graphics2D#setXORMode}.
*/
private Color lineXORColor = Color.black;
/**
* <var>x</var> coordinate of the mouse when the button is pressed.
*/
private transient int ox;
/**
* <var>y</var> coordinate of the mouse when the button is pressed.
*/
private transient int oy;
/**
* <var>x</var> coordinate of the mouse during the last drag.
*/
private transient int px;
/**
* <var>y</var> coordinate of the mouse during the last drag.
*/
private transient int py;
/**
* Indicates whether a selection is underway.
*/
private transient boolean isDragging;
/**
* Constructs an object which will allow rectangular regions to be selected using the mouse.
*/
public MouseSelectionTracker() {
}
/**
* Specifies the colors to be used for drawing the outline of a box when the user selects
* a region. All {@code a} colors will be replaced by {@code b} colors and vice versa.
*
* @param a The color to be replaced by color <var>b</var>.
* @param b The color replacing the color <var>a</var>.
*/
public void setXORColors(final Color a, final Color b) {
backXORColor = a;
lineXORColor = b;
}
/**
* Returns the geometric shape to use for marking the boundaries of a region. This shape is
* usually a rectangle but could also be an ellipse, an arrow or other {@linkplain RectangularShape
* rectangular shapes}. The coordinates of the returned shape will not be taken into account.
* In fact, these coordinates will regularly be discarded. Only the class of the returned shape
* matter (for example, {@link Ellipse2D} vs {@link Rectangle2D}) and their parameters which are
* not related to their position (for example, the {@linkplain RoundRectangle2D#getArcWidth()
* arc size} of a rectangle with rounded corners).
* <p>
* The shape returned will usually be an instance of a class derived from {@link RectangularShape},
* but could also be an instance of the {@link Line2D} class. <strong>Any other class risks
* throwing a {@link ClassCastException} when executed</strong>.
* <p>
* The default implementation always returns an instance of {@link Rectangle}.
*
* @param event Mouse coordinate when the button is pressed. This information can be used by
* subclasses overriding this method if the mouse location is relevant to the choice
* of a geometric shape.
* @return Shape from the class {@link RectangularShape} or {@link Line2D}, or {@code null}
* to indicate that we do not want to make a selection.
*/
protected Shape getModel(final MouseEvent event) {
return new Rectangle();
}
/**
* Method which is automatically invoked after the user selects a region with the mouse.
* All coordinates passed in as parameters are expressed in pixels.
*
* @param ox <var>x</var> coordinate of the mouse when the user pressed the mouse button.
* @param oy <var>y</var> coordinate of the mouse when the user pressed the mouse button.
* @param px <var>x</var> coordinate of the mouse when the user released the mouse button.
* @param py <var>y</var> coordinate of the mouse when the user released the mouse button.
*/
protected abstract void selectionPerformed(int ox, int oy, int px, int py);
/**
* Returns the geometric shape surrounding the last region to be selected by the user. An
* optional affine transform can be specified to convert the region selected by the user
* into logical coordinates. The class of the shape returned depends on the model returned by
* {@link #getModel}:
* <p>
* <ul>
* <li>If the model is an instance of {@link Line2D} (which means that this
* {@code MouseSelectionTracker} only draws a line between points), the
* object returned will belong to the {@link Line2D} class.</li>
* <li>Otherwise the object returned is usually (but not necessarily) an instance of the same
* class, usually {@link Rectangle2D}. There could be situations where the returned object
* is an instance of an another class, for example if the affine transform performs a
* rotation.</li>
* </ul>
*
* @param transform Affine transform which converts logical coordinates into pixel coordinates.
* This is usually the same transform than the one used for drawing in a
* {@link java.awt.Graphics2D} object.
* @return A geometric shape enclosing the last region to be selected by the user, or
* {@code null} if no selection has yet been made.
* @throws NoninvertibleTransformException If the affine transform can't be inverted.
*/
public Shape getSelectedArea(final AffineTransform transform) throws NoninvertibleTransformException {
if (ox == px && oy == py) {
return null;
}
RectangularShape shape = mouseSelectedArea;
if (transform != null && !transform.isIdentity()) {
if (shape == null) {
final Point2D.Float po = new Point2D.Float(ox, oy);
final Point2D.Float pp = new Point2D.Float(px, py);
transform.inverseTransform(po, po);
transform.inverseTransform(pp, pp);
return new Line2D.Float(po, pp);
} else {
if (canReshape(shape, transform)) {
final Point2D.Double point = new Point2D.Double();
double xmin = Double.POSITIVE_INFINITY;
double ymin = Double.POSITIVE_INFINITY;
double xmax = Double.NEGATIVE_INFINITY;
double ymax = Double.NEGATIVE_INFINITY;
for (int i = 0; i < 4; i++) {
point.x = (i&1) == 0 ? shape.getMinX() : shape.getMaxX();
point.y = (i&2) == 0 ? shape.getMinY() : shape.getMaxY();
transform.inverseTransform(point, point);
if (point.x < xmin) xmin = point.x;
if (point.x > xmax) xmax = point.x;
if (point.y < ymin) ymin = point.y;
if (point.y > ymax) ymax = point.y;
}
if (shape instanceof Rectangle) {
return new Rectangle2D.Float((float) xmin,
(float) ymin,
(float) (xmax - xmin),
(float) (ymax - ymin));
} else {
shape = (RectangularShape) shape.clone();
shape.setFrame(xmin, ymin, xmax - xmin, ymax - ymin);
return shape;
}
} else {
return transform.createInverse().createTransformedShape(shape);
}
}
} else {
return (shape != null) ? (Shape) shape.clone() : new Line2D.Float(ox, oy, px, py);
}
}
/**
* Indicates whether we can transform {@code shape} simply by calling its
* {@code shape.setFrame(...)} method rather than by using the heavy artillery
* that is the {@code transform.createTransformedShape(shape)} method.
*/
private static boolean canReshape(final RectangularShape shape, final AffineTransform transform) {
final int type=transform.getType();
if ((type & AffineTransform.TYPE_GENERAL_TRANSFORM) != 0) return false;
if ((type & AffineTransform.TYPE_MASK_ROTATION) != 0) return false;
if ((type & AffineTransform.TYPE_FLIP) != 0) {
if (shape instanceof Rectangle2D) return true;
if (shape instanceof Ellipse2D) return true;
if (shape instanceof RoundRectangle2D) return true;
return false;
}
return true;
}
/**
* Returns a {@link Graphics2D} object to be used for drawing in the specified component. We
* must not forget to call {@link Graphics2D#dispose} when the graphics object is no longer
* needed.
*/
private Graphics2D getGraphics(final Component c) {
final Graphics2D graphics = (Graphics2D) c.getGraphics();
graphics.setXORMode(lineXORColor);
graphics.setColor (backXORColor);
return graphics;
}
/**
* Informs this controller that the mouse button has been pressed. The default implementation
* memorizes the mouse coordinate (which will become one of the corners of the future rectangle
* to be drawn) and prepares this {@code MouseSelectionTracker} to observe the mouse movements.
*
* @param event Contains mouse coordinates where the button has been pressed.
* @throws ClassCastException if {@link #getModel} doesn't return a shape
* from the class {@link RectangularShape} or {@link Line2D}.
*/
@Override
public void mousePressed(final MouseEvent event) throws ClassCastException {
if (!event.isConsumed() && (event.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
final Component source = event.getComponent();
if (source != null) {
Shape model = getModel(event);
if (model != null) {
isDragging = true;
ox = px = event.getX();
oy = py = event.getY();
if (model instanceof Line2D) {
model = null;
}
mouseSelectedArea = (RectangularShape) model;
if (mouseSelectedArea != null) {
mouseSelectedArea.setFrame(ox, oy, 0, 0);
}
source.addMouseMotionListener(this);
}
source.requestFocus();
event.consume();
}
}
}
/**
* Informs this controller that the mouse has been dragged. The default implementation moves
* a corner of the rectangle used to select the region. The other corner remains fixed at the
* point where the mouse was at the moment it was {@linkplain #mousePressed pressed}.
*
* @param event Contains mouse coordinates when the cursor is being dragged.
*/
@Override
public void mouseDragged(final MouseEvent event) {
if (isDragging) {
final Graphics2D graphics = getGraphics(event.getComponent());
if (mouseSelectedArea == null) {
graphics.drawLine(ox, oy, px, py);
px = event.getX();
py = event.getY();
graphics.drawLine(ox, oy, px, py);
} else {
graphics.draw(mouseSelectedArea);
int xmin = this.ox;
int ymin = this.oy;
int xmax = px = event.getX();
int ymax = py = event.getY();
if (xmin > xmax) {
final int xtmp = xmin;
xmin = xmax; xmax = xtmp;
}
if (ymin > ymax) {
final int ytmp = ymin;
ymin = ymax; ymax = ytmp;
}
mouseSelectedArea.setFrame(xmin, ymin, xmax - xmin, ymax - ymin);
graphics.draw(mouseSelectedArea);
}
graphics.dispose();
event.consume();
}
}
/**
* Informs this controller that the mouse button has been released. The default implementation
* invokes {@link #selectionPerformed} with the bounds of the selected region as parameters.
*
* @param event Contains mouse coordinates where the button has been released.
*/
@Override
public void mouseReleased(final MouseEvent event) {
if (isDragging && (event.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
isDragging = false;
final Component component = event.getComponent();
component.removeMouseMotionListener(this);
final Graphics2D graphics = getGraphics(event.getComponent());
if (mouseSelectedArea == null) {
graphics.drawLine(ox, oy, px, py);
} else {
graphics.draw(mouseSelectedArea);
}
graphics.dispose();
px = event.getX();
py = event.getY();
selectionPerformed(ox, oy, px, py);
event.consume();
}
}
/**
* Informs this controller that the mouse has been moved but not as a result of the user
* selecting a region. The default implementation signals to the source component that
* this {@code MouseSelectionTracker} is no longer interested in being informed about
* mouse movements.
*
* {@note Normally not necessary, but it seems that this listener sometimes stays
* registered when it shouldn't.}
*
* @param event Contains mouse coordinates when the cursor is being moved.
*/
@Override
public void mouseMoved(final MouseEvent event) {
event.getComponent().removeMouseMotionListener(this);
}
}