0001: /*
0002: * GeoTools - OpenSource mapping toolkit
0003: * http://geotools.org
0004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
0005: * (C) 2001, Institut de Recherche pour le Développement
0006: *
0007: * This library is free software; you can redistribute it and/or
0008: * modify it under the terms of the GNU Lesser General Public
0009: * License as published by the Free Software Foundation; either
0010: * version 2.1 of the License, or (at your option) any later version.
0011: *
0012: * This library is distributed in the hope that it will be useful,
0013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
0014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0015: * Lesser General Public License for more details.
0016: */
0017: package org.geotools.gui.swing;
0018:
0019: // Geometry
0020: import java.awt.Shape;
0021: import java.awt.Rectangle;
0022: import java.awt.geom.Point2D;
0023: import java.awt.geom.Rectangle2D;
0024: import java.awt.geom.PathIterator;
0025: import java.awt.geom.AffineTransform;
0026: import java.awt.geom.RectangularShape;
0027: import java.awt.geom.NoninvertibleTransformException;
0028: import org.geotools.referencing.operation.matrix.XAffineTransform;
0029:
0030: // Graphics
0031: import java.awt.Paint;
0032: import java.awt.Color;
0033: import java.awt.Graphics;
0034: import java.awt.Graphics2D;
0035:
0036: // User interface
0037: import java.awt.Cursor;
0038: import java.awt.Insets;
0039: import java.awt.Component;
0040: import javax.swing.JFrame;
0041: import javax.swing.JSpinner;
0042: import javax.swing.KeyStroke;
0043: import javax.swing.JComponent;
0044: import javax.swing.JTextField;
0045: import javax.swing.SwingConstants;
0046: import javax.swing.SpinnerDateModel;
0047: import javax.swing.SpinnerNumberModel;
0048: import javax.swing.text.JTextComponent;
0049: import javax.swing.JFormattedTextField;
0050:
0051: // Events
0052: import java.awt.event.KeyEvent;
0053: import java.awt.event.MouseEvent;
0054: import java.awt.event.ActionEvent;
0055: import java.beans.PropertyChangeEvent;
0056: import java.beans.PropertyChangeListener;
0057: import javax.swing.event.MouseInputAdapter;
0058:
0059: // Formats
0060: import java.util.Date;
0061: import java.text.Format;
0062: import java.text.DateFormat;
0063: import java.text.DecimalFormat;
0064: import java.text.ParseException;
0065: import java.text.SimpleDateFormat;
0066:
0067: // Miscellaneous
0068: import java.lang.Double;
0069: import java.io.IOException;
0070: import java.io.ObjectInputStream;
0071:
0072: // Resources
0073: import org.geotools.resources.XMath;
0074: import org.geotools.resources.XArray;
0075: import org.geotools.resources.Utilities;
0076:
0077: /**
0078: * Controls the position and size of a rectangle which the user can move
0079: * with their mouse. For example, this class can be used as follows:
0080: *
0081: * <blockquote><pre>
0082: * public class MyClass extends JPanel
0083: * {
0084: * private final MouseReshapeTracker <em>slider</em> = new MouseReshapeTracker()
0085: * {
0086: * protected void {@link #clipChangeRequested clipChangeRequested}(double xmin, double xmax, double ymin, double ymax) {
0087: * // Indicates what must be done if the user tries to move the
0088: * // rectangle outside the permitted limits.
0089: * // This method is optional.
0090: * }
0091: *
0092: * protected void {@link #stateChanged stateChanged}(boolean isAdjusting) {
0093: * // Method automatically called each time the user
0094: * // changes the position of the rectangle.
0095: * // Code here what it should do in this case.
0096: * }
0097: * };
0098: *
0099: * private final AffineTransform transform = AffineTransform.getScaleInstance(10, 10);
0100: *
0101: * public MyClass() {
0102: * <em>slider</em>.{@link #setFrame setFrame}(0, 0, 1, 1);
0103: * <em>slider</em>.{@link #setClip setClip}(0, 100, 0, 1);
0104: * <em>slider</em>.{@link #setTransform setTransform}(transform);
0105: * addMouseMotionListener(<em>slider</em>);
0106: * addMouseListener (<em>slider</em>);
0107: * }
0108: *
0109: * public void paintComponent(Graphics graphics) {
0110: * AffineTransform tr=...
0111: * Graphics2D g = (Graphics2D) graphics;
0112: * g.transform(transform);
0113: * g.setColor(new Color(128, 64, 92, 64));
0114: * g.fill (<em>slider</em>);
0115: * }
0116: * }
0117: * </pre></blockquote>
0118: *
0119: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/MouseReshapeTracker.java $
0120: * @version $Id: MouseReshapeTracker.java 22710 2006-11-12 18:04:54Z desruisseaux $
0121: * @author Martin Desruisseaux
0122: */
0123: class MouseReshapeTracker extends MouseInputAdapter implements Shape {
0124: /**
0125: * Minimum width the rectangle should have, in pixels.
0126: */
0127: private static final int MIN_WIDTH = 12;
0128:
0129: /**
0130: * Minimum height the rectangle should have, in pixels.
0131: */
0132: private static final int MIN_HEIGHT = 12;
0133:
0134: /**
0135: * If the user moves the mouse by less than RESIZE_POS, then we assume the
0136: * user wants to resize rather than move the rectangle. This distance is
0137: * measured in pixels from one of the rectangle's edges.
0138: */
0139: private static final int RESIZE_POS = 4;
0140:
0141: /**
0142: * Minimum value of the <code>(clipped rectangle size)/(full rectangle
0143: * size)</code> ratio. This minimum value will only be taken into
0144: * account when the user modifies the rectangle's position using the values
0145: * entered in the fields. This number must be greater than or equal to 1.
0146: */
0147: private static final double MINSIZE_RATIO = 1.25;
0148:
0149: /**
0150: * Minimum <var>x</var> coordinate permitted for the rectangle. The default
0151: * value is {@link java.lang.Double#NEGATIVE_INFINITY}.
0152: */
0153: private double xmin = Double.NEGATIVE_INFINITY;
0154:
0155: /**
0156: * Minimum <var>y</var> coordinate permitted for the rectangle. The default
0157: * value is {@link java.lang.Double#NEGATIVE_INFINITY}.
0158: */
0159: private double ymin = Double.NEGATIVE_INFINITY;
0160:
0161: /**
0162: * Maximum <var>x</var> coordinate permitted for the rectangle. The default
0163: * value is {@link java.lang.Double#POSITIVE_INFINITY}.
0164: */
0165: private double xmax = Double.POSITIVE_INFINITY;
0166:
0167: /**
0168: * Maximum <var>y</var> coordinate permitted for the rectangle. The default
0169: * value is {@link java.lang.Double#POSITIVE_INFINITY}.
0170: */
0171: private double ymax = Double.POSITIVE_INFINITY;
0172:
0173: /**
0174: * The rectangle to control. The coordinates of this rectangle must be logical coordinates
0175: * (for example, coordinates in metres), and not screen pixel coordinates. An empty rectangle
0176: * means that no region is currently selected.
0177: */
0178: private final RectangularShape logicalShape;
0179:
0180: /**
0181: * Rectangle to be drawn in the component. This rectange can be different to
0182: * {@link #logicalShape} and the latter is so small that it is preferable to draw it a little
0183: * bit bigger than the user has requested. In this case, {@code drawnShape} will serve as
0184: * a temporary rectangle with extended coordinates.
0185: *
0186: * Note: this rectangle should be read only, except in the case of
0187: * {@link #update} which is the only method permitted to update it.
0188: */
0189: private transient RectangularShape drawnShape;
0190:
0191: /**
0192: * Affine transform which changes logical coordinates into pixel coordinates. It is guaranteed
0193: * that no method except {@link #setTransform} will modify this transformation.
0194: */
0195: private final AffineTransform transform = new AffineTransform();
0196:
0197: /**
0198: * Last <em>relative</em> mouse coordinates. This information is expressed in logical
0199: * coordinates (according to the {@link #getTransform} inverse affine transform). The
0200: * coordinates are relative to (<var>x</var>,<var>y</var>) corner of the rectangle.
0201: */
0202: private transient double mouseDX, mouseDY;
0203:
0204: /**
0205: * {@code x}, {@code y}, {@code width} and {@code height} coordinates of a
0206: * box which completely encloses {@link #drawnShape}. These coordinates must be expressed in
0207: * <strong>pixels</strong>. If need be, the affine transform {@link #getTransform} can be used
0208: * to change pixel coordinates into logical coordinates and vice versa.
0209: */
0210: private transient int x, y, width, height;
0211:
0212: /**
0213: * Indicates whether the mouse pointer is over the rectangle.
0214: */
0215: private transient boolean mouseOverRect;
0216:
0217: /**
0218: * Point used internally by certain calculations in order to avoid
0219: * the frequent creation of several temporary {@link Point2D} objects.
0220: */
0221: private final transient Point2D.Double tmp = new Point2D.Double();
0222:
0223: /**
0224: * Indicates if the user is currently dragging the rectangle.
0225: * For this field to become {@code true}, the mouse must
0226: * have been over the rectangle as the user pressed the mouse button.
0227: */
0228: private transient boolean isDragging;
0229:
0230: /**
0231: * Indicates which edges the user is currently adjusting with the mouse.
0232: * This field is often identical to {@link #adjustingSides}. However,
0233: * unlike {@link #adjustingSides}, it designates an edge of the shape
0234: * {@link #logicalShape} and not an edge of the shape in pixels appearing
0235: * on the screen. It is different, for example, if the affine transform
0236: * {@link #transform} contains a 90° rotation.
0237: */
0238: private transient int adjustingLogicalSides;
0239:
0240: /**
0241: * Indicates which edges the user is currently adjusting with the mouse.
0242: * Permitted values are binary combinations of {@link #NORTH},
0243: * {@link #SOUTH}, {@link #EAST} and {@link #WEST}.
0244: */
0245: private transient int adjustingSides;
0246:
0247: /**
0248: * Indicates which edges are allowed to be adjusted. Permitted
0249: * values are binary combinations of {@link #NORTH},
0250: * {@link #SOUTH}, {@link #EAST} and {@link #WEST}.
0251: */
0252: private int adjustableSides;
0253:
0254: /**
0255: * Indicates if the geometric shape can be moved.
0256: */
0257: private boolean moveable = true;
0258:
0259: /**
0260: * When the position of the left or right-hand edge of the rectangle
0261: * is manually edited, this indicates whether the position of the
0262: * opposite edge should be automatically adjusted. The default value is
0263: * {@code false}.
0264: */
0265: private boolean synchronizeX;
0266:
0267: /**
0268: * When the position of the top or bottom edge of the rectangle is
0269: * manually edited, this indicates whether the position of the
0270: * opposite edge should be automatically adjusted. The default value is
0271: * {@code false}.
0272: */
0273: private boolean synchronizeY;
0274:
0275: /** Bit representing north. */
0276: private static final int NORTH = 1;
0277: /** Bit representing south. */
0278: private static final int SOUTH = 2;
0279: /** Bit representing east. */
0280: private static final int EAST = 4;
0281: /** Bit representing west. */
0282: private static final int WEST = 8;
0283:
0284: /**
0285: * Cursor codes corresponding to a given {@link adjustingSides} value.
0286: */
0287: private static final int[] CURSORS = new int[] {
0288: Cursor.MOVE_CURSOR, // 0000 = | | |
0289: Cursor.N_RESIZE_CURSOR, // 0001 = | | | NORTH
0290: Cursor.S_RESIZE_CURSOR, // 0010 = | | SOUTH |
0291: Cursor.DEFAULT_CURSOR, // 0011 = | | SOUTH | NORTH
0292: Cursor.E_RESIZE_CURSOR, // 0100 = | EAST | |
0293: Cursor.NE_RESIZE_CURSOR, // 0101 = | EAST | | NORTH
0294: Cursor.SE_RESIZE_CURSOR, // 0110 = | EAST | SOUTH |
0295: Cursor.DEFAULT_CURSOR, // 0111 = | EAST | SOUTH | NORTH
0296: Cursor.W_RESIZE_CURSOR, // 1000 = WEST | | |
0297: Cursor.NW_RESIZE_CURSOR, // 1001 = WEST | | | NORTH
0298: Cursor.SW_RESIZE_CURSOR // 1010 = WEST | | SOUTH |
0299: };
0300:
0301: /**
0302: * Lookup table which converts <i>Swing</i> constants into
0303: * combinations of {@link #NORTH}, {@link #SOUTH},
0304: * {@link #EAST} and {@link #WEST} constants. We cannot use
0305: * <i>Swing</i> constants directly because, unfortunately, they do
0306: * not correspond to the binary combinations of the four
0307: * cardinal corners.
0308: */
0309: private static final int[] SWING_TO_CUSTOM = new int[] {
0310: SwingConstants.NORTH, NORTH, SwingConstants.SOUTH, SOUTH,
0311: SwingConstants.EAST, EAST, SwingConstants.WEST, WEST,
0312: SwingConstants.NORTH_EAST, NORTH | EAST,
0313: SwingConstants.SOUTH_EAST, SOUTH | EAST,
0314: SwingConstants.NORTH_WEST, NORTH | WEST,
0315: SwingConstants.SOUTH_WEST, SOUTH | WEST };
0316:
0317: /**
0318: * List of text fields which represent the coordinates of the
0319: * rectangle's edges.
0320: */
0321: private Control[] editors;
0322:
0323: /**
0324: * Constructs an object capable of moving and resizing a rectangular
0325: * shape through mouse movements. The rectangle will be positioned, by
0326: * default at the coordinates (0,0). Its width and height will be null.
0327: */
0328: public MouseReshapeTracker() {
0329: this (new Rectangle2D.Double());
0330: }
0331:
0332: /**
0333: * Constructs an object capable of moving and resizing a rectangular shape
0334: * through mouse movements.
0335: *
0336: * @param shape Rectangular geometric shape. This shape does not have to be
0337: * a rectangle. It could, for example, be a circle. The
0338: * coordinates of this shape will be the initial coordinates of the
0339: * visor. They are logical coordinates and not pixel coordinates
0340: * Note that the constructor retains a direct reference to this
0341: * shape, without creating a clone. As a consequence, any
0342: * modification carried out on the geometric shape will have
0343: * repercussions for this objet {@code MouseReshapeTracker}
0344: * and vice versa.
0345: */
0346: public MouseReshapeTracker(final RectangularShape shape) {
0347: this .logicalShape = shape;
0348: this .drawnShape = shape;
0349: update();
0350: }
0351:
0352: /**
0353: * Method called automatically after reading this object
0354: * in order to finish the construction of certain fields.
0355: */
0356: private void readObject(final ObjectInputStream in)
0357: throws IOException, ClassNotFoundException {
0358: in.defaultReadObject();
0359: drawnShape = logicalShape;
0360: update();
0361: }
0362:
0363: /**
0364: * Updates the internal fields of this object. The adjusted fields will be:
0365: *
0366: * <ul>
0367: * <li>{@link #drawnShape} for the rectangle to be drawn.</li>
0368: * <li>{@link #x}, {@link #y}, {@link #width} and {@link #height}
0369: * for the pixel coordinates of {@link #drawnShape}.</li>
0370: * </ul>
0371: */
0372: private void update() {
0373: /*
0374: * Takes into account cases where the affine transform
0375: * contains a rotation of 90° or any other.
0376: */
0377: adjustingLogicalSides = inverseTransform(adjustingSides);
0378: /*
0379: * Obtains the geometric shape to draw. Normally it will be a
0380: * {@link #logicalShape}, except if the latter is so small that we
0381: * have considered it preferable to create a temporary shape which
0382: * will be slightly bigger.
0383: */
0384: tmp.x = logicalShape.getWidth();
0385: tmp.y = logicalShape.getHeight();
0386: transform.deltaTransform(tmp, tmp);
0387: if (Math.abs(tmp.x) < MIN_WIDTH || Math.abs(tmp.y) < MIN_HEIGHT) {
0388: if (Math.abs(tmp.x) < MIN_WIDTH)
0389: tmp.x = (tmp.x < 0) ? -MIN_WIDTH : MIN_WIDTH;
0390: if (Math.abs(tmp.y) < MIN_HEIGHT)
0391: tmp.y = (tmp.y < 0) ? -MIN_HEIGHT : MIN_HEIGHT;
0392: try {
0393: XAffineTransform.inverseDeltaTransform(transform, tmp,
0394: tmp);
0395: double x = logicalShape.getX();
0396: double y = logicalShape.getY();
0397: if ((adjustingLogicalSides & WEST) != 0) {
0398: x += logicalShape.getWidth() - tmp.x;
0399: }
0400: if ((adjustingLogicalSides & NORTH) != 0) {
0401: y += logicalShape.getHeight() - tmp.y;
0402: }
0403: if (drawnShape == logicalShape) {
0404: drawnShape = (RectangularShape) logicalShape
0405: .clone();
0406: }
0407: drawnShape.setFrame(x, y, tmp.x, tmp.y);
0408: } catch (NoninvertibleTransformException exception) {
0409: drawnShape = logicalShape;
0410: }
0411: } else {
0412: drawnShape = logicalShape;
0413: }
0414: /*
0415: * NOTE: the condition 'drawnShape==logicalShape' indicates that it has
0416: * not been necessary to modify the shape. The method
0417: * 'mouseDragged' will use this information.
0418: *
0419: * Now retains the pixel coordinates of the new position of the
0420: * rectangle.
0421: */
0422: double xmin = Double.POSITIVE_INFINITY;
0423: double ymin = Double.POSITIVE_INFINITY;
0424: double xmax = Double.NEGATIVE_INFINITY;
0425: double ymax = Double.NEGATIVE_INFINITY;
0426: for (int i = 0; i < 4; i++) {
0427: tmp.x = (i & 1) == 0 ? drawnShape.getMinX() : drawnShape
0428: .getMaxX();
0429: tmp.y = (i & 2) == 0 ? drawnShape.getMinY() : drawnShape
0430: .getMaxY();
0431: transform.transform(tmp, tmp);
0432: if (tmp.x < xmin) {
0433: xmin = tmp.x;
0434: }
0435: if (tmp.x > xmax) {
0436: xmax = tmp.x;
0437: }
0438: if (tmp.y < ymin) {
0439: ymin = tmp.y;
0440: }
0441: if (tmp.y > ymax) {
0442: ymax = tmp.y;
0443: }
0444: }
0445: x = (int) Math.floor(xmin) - 1;
0446: y = (int) Math.floor(ymin) - 1;
0447: width = (int) Math.ceil(xmax - xmin) + 2;
0448: height = (int) Math.ceil(ymax - ymin) + 2;
0449: }
0450:
0451: /**
0452: * Returns the transform of {@code adjusting}.
0453: * @param adjusting to transform (generally {@link #adjustingSides}).
0454: */
0455: private int inverseTransform(int adjusting) {
0456: switch (adjusting & (WEST | EAST)) {
0457: case WEST:
0458: tmp.x = -1;
0459: break;
0460: case EAST:
0461: tmp.x = +1;
0462: break;
0463: default:
0464: tmp.x = 0;
0465: break;
0466: }
0467: switch (adjusting & (NORTH | SOUTH)) {
0468: case NORTH:
0469: tmp.y = -1;
0470: break;
0471: case SOUTH:
0472: tmp.y = +1;
0473: break;
0474: default:
0475: tmp.y = 0;
0476: break;
0477: }
0478: try {
0479: XAffineTransform.inverseDeltaTransform(transform, tmp, tmp);
0480: final double normalize = 0.25 * XMath.hypot(tmp.x, tmp.y);
0481: tmp.x /= normalize;
0482: tmp.y /= normalize;
0483: adjusting = 0;
0484: switch (XMath.sgn(Math.rint(tmp.x))) {
0485: case -1:
0486: adjusting |= WEST;
0487: break;
0488: case +1:
0489: adjusting |= EAST;
0490: break;
0491: }
0492: switch (XMath.sgn(Math.rint(tmp.y))) {
0493: case -1:
0494: adjusting |= NORTH;
0495: break;
0496: case +1:
0497: adjusting |= SOUTH;
0498: break;
0499: }
0500: return adjusting;
0501: } catch (NoninvertibleTransformException exception) {
0502: return adjusting;
0503: }
0504: }
0505:
0506: /**
0507: * Declares the affine transform which will transform the logical
0508: * coordinates into pixel coordinates. This is the affine transform
0509: * specified in {@link java.awt.Graphics2D#transform} at the moment that
0510: * {@code this} is drawn. The information contained in this affine
0511: * transform is necessary for several of this class's methods to work.
0512: * It is the programmer's responsability to ensure that this
0513: * information is always up-to-date. By default,
0514: * {@code MouseReshapeTracker} uses an identity transform.
0515: */
0516: public void setTransform(final AffineTransform newTransform) {
0517: if (!this .transform.equals(newTransform)) {
0518: fireStateWillChange();
0519: this .transform.setTransform(newTransform);
0520: update();
0521: fireStateChanged();
0522: }
0523: }
0524:
0525: /**
0526: * Returns the position and the bounds of the rectangle. These
0527: * bounds can be slightly bigger than those returned by
0528: * {@link #getFrame} since {@code getBounds2D()} returns the
0529: * bounds of the rectangle visible on screen, which can have certain
0530: * minimum bounds.
0531: */
0532: public Rectangle getBounds() {
0533: return drawnShape.getBounds();
0534: }
0535:
0536: /**
0537: * Returns the position and the bounds of the rectangle. These
0538: * bounds can be slightly bigger than those returned by
0539: * {@link #getFrame} since {@code getBounds2D()} returns the
0540: * bounds of the rectangle visible on screen, which can have certain
0541: * minimum bounds.
0542: */
0543: public Rectangle2D getBounds2D() {
0544: return drawnShape.getBounds2D();
0545: }
0546:
0547: /**
0548: * Returns the position and the bounds of the rectangle.
0549: * This information is expressed in logical coordinates.
0550: *
0551: * @see #getCenterX
0552: * @see #getCenterY
0553: * @see #getMinX
0554: * @see #getMaxX
0555: * @see #getMinY
0556: * @see #getMaxY
0557: */
0558: public Rectangle2D getFrame() {
0559: return logicalShape.getFrame();
0560: }
0561:
0562: /**
0563: * Defines a new position and bounds for the rectangle. The coordinates
0564: * passed to this method should be logical coordinates rather than pixel
0565: * coordinates. If the range of values covered by the rectangle is
0566: * limited by a call to {@link #setClip}, the rectangle will be
0567: * moved and resized as needed to fit into the permitted region.
0568: *
0569: * @return {@code true} if the rectangle's coordinates have changed.
0570: *
0571: * @see #getFrame
0572: */
0573: public final boolean setFrame(final Rectangle2D frame) {
0574: return setFrame(frame.getX(), frame.getY(), frame.getWidth(),
0575: frame.getHeight());
0576: }
0577:
0578: /**
0579: * Defines a new position and bounds for the rectangle. The coordinates
0580: * passed to this method should be logical coordinates rather than pixel
0581: * coordinates. If the range of values covered by the rectangle is
0582: * limited by a call to {@link #setClip}, the rectangle will be
0583: * moved and resized as needed to fit into the permitted region.
0584: *
0585: * @return {@code true} if the rectangle's coordinates have changed.
0586: *
0587: * @see #setX
0588: * @see #setY
0589: */
0590: public boolean setFrame(double x, double y, double width,
0591: double height) {
0592: final double oldX = logicalShape.getX();
0593: final double oldY = logicalShape.getY();
0594: final double oldW = logicalShape.getWidth();
0595: final double oldH = logicalShape.getHeight();
0596: if (x < xmin)
0597: x = xmin;
0598: if (y < ymin)
0599: y = ymin;
0600: if (x + width > xmax) {
0601: x = Math.max(xmin, xmax - width);
0602: width = xmax - x;
0603: }
0604: if (y + height > ymax) {
0605: y = Math.max(ymin, ymax - height);
0606: height = ymax - y;
0607: }
0608: fireStateWillChange();
0609: logicalShape.setFrame(x, y, width, height);
0610: if (oldX != logicalShape.getX() || oldY != logicalShape.getY()
0611: || oldW != logicalShape.getWidth()
0612: || oldH != logicalShape.getHeight()) {
0613: update();
0614: fireStateChanged();
0615: return true;
0616: }
0617: return false;
0618: }
0619:
0620: /**
0621: * Defines the new range of values covered by the rectangle according to
0622: * the <var>x</var> axis. The values covered along the <var>y</var> axis
0623: * will not be changed. The values must be expressed in logical coordinates
0624: *
0625: * @see #getMinX
0626: * @see #getMaxX
0627: * @see #getCenterX
0628: */
0629: public final void setX(final double min, final double max) {
0630: setFrame(Math.min(min, max), logicalShape.getY(), Math.abs(max
0631: - min), logicalShape.getHeight());
0632: }
0633:
0634: /**
0635: * Defines the new range of values covered by the rectangle according to
0636: * the <var>y</var> axis. The values covered along the <var>x</var> axis
0637: * will not be changed. The values must be expressed in logical coordinates
0638: *
0639: * @see #getMinY
0640: * @see #getMaxY
0641: * @see #getCenterY
0642: */
0643: public final void setY(final double min, final double max) {
0644: setFrame(logicalShape.getX(), Math.min(min, max), logicalShape
0645: .getWidth(), Math.abs(max - min));
0646: }
0647:
0648: /**
0649: * Returns the minimum <var>x</var> coordinate of the rectangle
0650: * (the logical coordinate, not the pixel coordinate).
0651: */
0652: public double getMinX() {
0653: return logicalShape.getMinX();
0654: }
0655:
0656: /**
0657: * Returns the minimum <var>y</var> coordinate of the rectangle
0658: * (the logical coordinate, not the pixel coordinate).
0659: */
0660: public double getMinY() {
0661: return logicalShape.getMinY();
0662: }
0663:
0664: /**
0665: * Returns the maximum <var>x</var> coordinate of the rectangle
0666: * (the logical coordinate, not the pixel coordinate).
0667: */
0668: public double getMaxX() {
0669: return logicalShape.getMaxX();
0670: }
0671:
0672: /**
0673: * Returns the maximum <var>y</var> coordinate of the rectangle
0674: * (the logical coordinate, not the pixel coordinate).
0675: */
0676: public double getMaxY() {
0677: return logicalShape.getMaxY();
0678: }
0679:
0680: /**
0681: * Returns the width of the rectangle. This width is expressed
0682: * in logical coordinates, not pixel coordinates.
0683: */
0684: public double getWidth() {
0685: return logicalShape.getWidth();
0686: }
0687:
0688: /**
0689: * Returns the height of the rectangle. This height is expressed
0690: * in logical coordinates, not pixel coordinates.
0691: */
0692: public double getHeight() {
0693: return logicalShape.getHeight();
0694: }
0695:
0696: /**
0697: * Returns the <var>x</var> coordinate of the centre of the rectangle
0698: * (logical coordinate, not pixel coordinate).
0699: */
0700: public double getCenterX() {
0701: return logicalShape.getCenterX();
0702: }
0703:
0704: /**
0705: * Returns the <var>y</var> coordinate of the centre of the rectangle
0706: * (logical coordinate, not pixel coordinate).
0707: */
0708: public double getCenterY() {
0709: return logicalShape.getCenterY();
0710: }
0711:
0712: /**
0713: * Indicates whether the rectangle is empty. This will be
0714: * the case if the width and / or height is null.
0715: */
0716: public boolean isEmpty() {
0717: return logicalShape.isEmpty();
0718: }
0719:
0720: /**
0721: * Indicates whether the rectangular shape contains the specified point.
0722: * This point should be expressed in logical coordinates.
0723: */
0724: public boolean contains(final Point2D point) {
0725: return logicalShape.contains(point);
0726: }
0727:
0728: /**
0729: * Indicates whether the rectangular shape contains the specified point.
0730: * This point should be expressed in logical coordinates.
0731: */
0732: public boolean contains(final double x, final double y) {
0733: return logicalShape.contains(x, y);
0734: }
0735:
0736: /**
0737: * Indicates whether the rectangular shape contains the specified
0738: * rectangle. This rectangle should be expressed in logical
0739: * coordinates. This method can conservatively return
0740: * {@code false} as permitted by the {@link Shape} specification.
0741: */
0742: public boolean contains(final Rectangle2D rect) {
0743: return logicalShape.contains(rect);
0744: }
0745:
0746: /**
0747: * Indicates whether the rectangular shape contains the specified
0748: * rectangle. This rectangle must be expressed in logical
0749: * coordinates. This method can conservatively return
0750: * {@code false} as permitted by the {@link Shape} specification.
0751: */
0752: public boolean contains(double x, double y, double width,
0753: double height) {
0754: return logicalShape.contains(x, y, width, height);
0755: }
0756:
0757: /**
0758: * Indicates whether the rectangular shape intersects the specified
0759: * rectangle. This rectangle must be expressed in logical coordinates.
0760: * This method can conservatively return {@code true} as permitted by
0761: * the {@link Shape} specification.
0762: */
0763: public boolean intersects(final Rectangle2D rect) {
0764: return drawnShape.intersects(rect);
0765: }
0766:
0767: /**
0768: * Indicates whether the rectangular shape intersects the specified
0769: * rectangle. This rectangle must be expressed in logical coordinates.
0770: * This method can conservatively return {@code true} as permitted by
0771: * the {@link Shape} specification.
0772: */
0773: public boolean intersects(double x, double y, double width,
0774: double height) {
0775: return drawnShape.intersects(x, y, width, height);
0776: }
0777:
0778: /**
0779: * Returns a path iterator for the rectangular shape to be drawn.
0780: */
0781: public PathIterator getPathIterator(final AffineTransform transform) {
0782: return drawnShape.getPathIterator(transform);
0783: }
0784:
0785: /**
0786: * Returns a path iterator for the rectangular shape to be drawn.
0787: */
0788: public PathIterator getPathIterator(
0789: final AffineTransform transform, final double flatness) {
0790: return drawnShape.getPathIterator(transform, flatness);
0791: }
0792:
0793: /**
0794: * Returns the bounds between which the rectangle can move.
0795: * These bounds are specified in logical coordinates.
0796: */
0797: public Rectangle2D getClip() {
0798: return new Rectangle2D.Double(xmin, ymin, xmax - xmin, ymax
0799: - ymin);
0800: }
0801:
0802: /**
0803: * Defines the bounds between which the rectangle can move.
0804: * This method manages infinities correctly if the specified
0805: * rectangle has redefined its {@code getMaxX()}
0806: * and {@code getMaxY()} methods correctly.
0807: *
0808: * @see #setClipMinMax
0809: */
0810: public final void setClip(final Rectangle2D rect) {
0811: setClipMinMax(rect.getMinX(), rect.getMaxX(), rect.getMinY(),
0812: rect.getMaxY());
0813: }
0814:
0815: /**
0816: * Defines the bounds between which the rectangle can move. This method
0817: * simply calls {@link #setClipMinMax setClipMinMax(...)} with the
0818: * appropriate parameters. It is defined in order to avoid confusion
0819: * amongst programmers used to <em>Java2D</em> conventions. If you want to
0820: * specify infinite values (in order to widen the visor's bounds to all
0821: * possible values along certain axes), you <u>must</u> use
0822: * {@link #setClipMinMax setClipMinMax(...)} rather than
0823: * {@code setClip(...)}.
0824: */
0825: public final void setClip(final double x, final double y,
0826: final double width, final double height) {
0827: setClipMinMax(x, x + width, y, y + height);
0828: }
0829:
0830: /**
0831: * Defines the bounds between which the rectangle can move. Note that this
0832: * method's arguments don't correspond to the normal arguments of
0833: * {@link java.awt.geom.Rectangle2D}. <em>Java2D</em> convention demands
0834: * that we specify a rectangle using a quadruplet
0835: * ({@code x},{@code y},{@code width},{@code height})
0836: * However, this is a bad choice in the context of almost all the methods
0837: * in our library. As well as complicating most calculations (if you need
0838: * convincing, just count the number of occurrences of the expression
0839: * {@code x+width} even in the geometric classes of <em>Java2D</em>),
0840: * it is incapable of correctly representing a rectangle which has one or
0841: * more coordinates stretching to infinity. A better convention would
0842: * have been to use the minimum and maximum values according to
0843: * <var>x</var> and <var>y</var>, as this method does.
0844: * <br><br>
0845: * This method's arguments define the minimum and maximum values that the
0846: * logical coordinates of the rectangle can take.
0847: * The values {@link java.lang.Double#NEGATIVE_INFINITY} and
0848: * {@link java.lang.Double#POSITIVE_INFINITY} are valid for indicating
0849: * that the visor can extend across all values according to certain axes.
0850: * The value {@link java.lang.Double#NaN} for a given argument indicates
0851: * that we want to keep the old value. If the visor doesn't fit
0852: * completely within the new bounds, it will be moved and resized as needed
0853: * in order to make it fit.
0854: */
0855: public void setClipMinMax(double xmin, double xmax, double ymin,
0856: double ymax) {
0857: if (xmin > xmax) {
0858: final double tmp = xmin;
0859: xmin = xmax;
0860: xmax = tmp;
0861: }
0862: if (ymin > ymax) {
0863: final double tmp = ymin;
0864: ymin = ymax;
0865: ymax = tmp;
0866: }
0867: if (!Double.isNaN(xmin)) {
0868: this .xmin = xmin;
0869: }
0870: if (!Double.isNaN(xmax)) {
0871: this .xmax = xmax;
0872: }
0873: if (!Double.isNaN(ymin)) {
0874: this .ymin = ymin;
0875: }
0876: if (!Double.isNaN(ymax)) {
0877: this .ymax = ymax;
0878: }
0879: setFrame(logicalShape.getX(), logicalShape.getY(), logicalShape
0880: .getWidth(), logicalShape.getHeight());
0881: }
0882:
0883: /**
0884: * Method called automatically when a change in the clip is required.
0885: * This method can be called, for example, when the user manually edits
0886: * the position of the rectangle in a text field, and the new position
0887: * falls outside the current clip. This method does <u>not</u> have to
0888: * accept a clip change. It can do nothing, which is the same as
0889: * refusing any change. It can also always unconditionally accept any
0890: * change by calling {@link #setClipMinMax}. Finally, it can reach a
0891: * compromise solution by imposing certain conditions on the changes.
0892: * The default implementation does nothing, which means that no
0893: * automatic change in the clip will be authorised.
0894: */
0895: protected void clipChangeRequested(double xmin, double xmax,
0896: double ymin, double ymax) {
0897: }
0898:
0899: /**
0900: * Indicates whether the rectangle can be moved with the mouse. By default,
0901: * it can be moved but not resized.
0902: */
0903: public boolean isMoveable() {
0904: return moveable;
0905: }
0906:
0907: /**
0908: * Specifies whether the rectangle can be moved with the mouse. The value
0909: * {@code false} indicates that the rectangle cannot be moved, but can
0910: * still be resized if {@link #setAdjustable} has been called with the
0911: * appropriate parameters.
0912: */
0913: public void setMoveable(final boolean moveable) {
0914: this .moveable = moveable;
0915: }
0916:
0917: /**
0918: * Indicates whether the size of a rectangle can be modified using
0919: * a specified edge. The specified edge must be one of the following
0920: * constants:
0921: *
0922: * <table border align=center cellpadding=8 bgcolor=floralwhite>
0923: * <tr><td>{@link SwingConstants#NORTH_WEST}</td><td>{@link SwingConstants#NORTH}</td><td>{@link SwingConstants#NORTH_EAST}</td></tr>
0924: * <tr><td>{@link SwingConstants#WEST }</td><td> </td><td>{@link SwingConstants#EAST }</td></tr>
0925: * <tr><td>{@link SwingConstants#SOUTH_WEST}</td><td>{@link SwingConstants#SOUTH}</td><td>{@link SwingConstants#SOUTH_EAST}</td></tr>
0926: * </table>
0927: *
0928: * These constants designate the edge which is visible on screen. For
0929: * example, {@code NORTH} always designates the top edge on the
0930: * screen. However, this could correspond to another edge of the logical
0931: * shape {@code this} depending on the affine transform which was
0932: * specified during the last call to {@link #setTransform}. For example,
0933: * {@code AffineTransform.getScaleInstance(+1,-1)} has the effect of
0934: * inverting the y axis so that the <var>y</var><sub>max</sub> values
0935: * appear to the North rather than the <var>y</var><sub>min</sub> values.
0936: */
0937: public boolean isAdjustable(int side) {
0938: side = convertSwingConstant(side);
0939: return (adjustableSides & side) == side;
0940: }
0941:
0942: /**
0943: * Specifies whether the size of the rectangle can be modified using the
0944: * specified edge. The specified edge must be one of the following
0945: * constants:
0946: *
0947: * <table border align=center cellpadding=8 bgcolor=floralwhite>
0948: * <tr><td>{@link SwingConstants#NORTH_WEST}</td><td>{@link SwingConstants#NORTH}</td><td>{@link SwingConstants#NORTH_EAST}</td></tr>
0949: * <tr><td>{@link SwingConstants#WEST }</td><td> </td><td>{@link SwingConstants#EAST }</td></tr>
0950: * <tr><td>{@link SwingConstants#SOUTH_WEST}</td><td>{@link SwingConstants#SOUTH}</td><td>{@link SwingConstants#SOUTH_EAST}</td></tr>
0951: * </table>
0952: *
0953: * These constants designate the edge which is visible on screen. For
0954: * example, {@code NORTH} always designates the top edge on the
0955: * screen. However, this could correspond to another edge of the logical
0956: * shape {@code this} depending on the affine transform which was
0957: * specified during the last call to {@link #setTransform}. For example,
0958: * {@code AffineTransform.getScaleInstance(+1,-1)} has the effect of
0959: * inverting the y axis so that the <var>y</var><sub>max</sub> values
0960: * appear to the North rather than the <var>y</var><sub>min</sub> values.
0961: */
0962: public void setAdjustable(int side, final boolean adjustable) {
0963: side = convertSwingConstant(side);
0964: if (adjustable) {
0965: adjustableSides |= side;
0966: } else {
0967: adjustableSides &= ~side;
0968: }
0969: }
0970:
0971: /*
0972: * Converts a Swing edge constant to system used by this package.
0973: * We cannot use <i>Swing</i> constants directly because,
0974: * unfortunately, they do not correspond to the binary combinations of the
0975: * four cardinal corners.
0976: */
0977: private int convertSwingConstant(final int side) {
0978: for (int i = 0; i < SWING_TO_CUSTOM.length; i += 2) {
0979: if (SWING_TO_CUSTOM[i] == side) {
0980: return SWING_TO_CUSTOM[i + 1];
0981: }
0982: }
0983: throw new IllegalArgumentException(String.valueOf(side));
0984: }
0985:
0986: /**
0987: * Method called automatically during mouse movements. The default
0988: * implementation checks whether the cursor is inside the rectangle or on
0989: * one of its edges, and adjusts the mouse pointer icon accordingly.
0990: */
0991: public void mouseMoved(final MouseEvent event) {
0992: if (!isDragging) {
0993: final Component source = event.getComponent();
0994: if (source != null) {
0995: int x = event.getX();
0996: tmp.x = x;
0997: int y = event.getY();
0998: tmp.y = y;
0999: final boolean mouseOverRect;
1000: try {
1001: mouseOverRect = drawnShape.contains(transform
1002: .inverseTransform(tmp, tmp));
1003: } catch (NoninvertibleTransformException exception) {
1004: // Ignore this exception.
1005: return;
1006: }
1007: final boolean mouseOverRectChanged = (mouseOverRect != this .mouseOverRect);
1008: if (mouseOverRect) {
1009: /*
1010: * We do not use "adjustingLogicalSides" because we are working
1011: * with pixel coordinates and not logical coordinates.
1012: */
1013: final int old = adjustingSides;
1014: adjustingSides = 0;
1015: if (Math.abs(x -= this .x) <= RESIZE_POS) {
1016: adjustingSides |= WEST;
1017: }
1018: if (Math.abs(y -= this .y) <= RESIZE_POS) {
1019: adjustingSides |= NORTH;
1020: }
1021: if (Math.abs(x - this .width) <= RESIZE_POS) {
1022: adjustingSides |= EAST;
1023: }
1024: if (Math.abs(y - this .height) <= RESIZE_POS) {
1025: adjustingSides |= SOUTH;
1026: }
1027:
1028: adjustingSides &= adjustableSides;
1029: if (adjustingSides != old || mouseOverRectChanged) {
1030: if (adjustingSides == 0 && !moveable) {
1031: source.setCursor(null);
1032: } else {
1033: adjustingLogicalSides = inverseTransform(adjustingSides);
1034: source
1035: .setCursor(Cursor
1036: .getPredefinedCursor(adjustingSides < CURSORS.length ? CURSORS[adjustingSides]
1037: : Cursor.DEFAULT_CURSOR));
1038: }
1039: }
1040: if (mouseOverRectChanged) {
1041: // Adding and removing listeners worked well, but had
1042: // the disadvantage of changing the order of the
1043: // listeners. This caused problems when the order was
1044: // important.
1045:
1046: //source.addMouseListener(this);
1047: this .mouseOverRect = mouseOverRect;
1048: }
1049: } else if (mouseOverRectChanged) {
1050: adjustingSides = 0;
1051: source.setCursor(null);
1052: //source.removeMouseListener(this);
1053: this .mouseOverRect = mouseOverRect;
1054: }
1055: }
1056: }
1057: }
1058:
1059: /**
1060: * Method called automatically when the user presses a mouse button
1061: * anywhere within the component. The default implementation
1062: * checks if the button was pressed whilst the mouse cursor was
1063: * within the rectangle. If so, this object will track the mouse drags
1064: * to move or resize the rectangle.
1065: */
1066: public void mousePressed(final MouseEvent e) {
1067: if (!e.isConsumed()
1068: && (e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
1069: if (adjustingSides != 0 || moveable) {
1070: tmp.x = e.getX();
1071: tmp.y = e.getY();
1072: try {
1073: if (drawnShape.contains(transform.inverseTransform(
1074: tmp, tmp))) {
1075: mouseDX = tmp.x - drawnShape.getX();
1076: mouseDY = tmp.y - drawnShape.getY();
1077: isDragging = true;
1078: e.consume();
1079: }
1080: } catch (NoninvertibleTransformException exception) {
1081: // Pas besoin de gérer cette exception.
1082: // L'ignorer est correct.
1083: }
1084: }
1085: }
1086: }
1087:
1088: /**
1089: * Method called automatically during mouse drags. The default
1090: * implementation applies the mouse movement to the rectangle and notifies
1091: * the component where the event which it needs to redraw, at least in
1092: * part, came from.
1093: */
1094: public void mouseDragged(final MouseEvent e) {
1095: if (isDragging) {
1096: final int adjustingLogicalSides = this .adjustingLogicalSides;
1097: final Component source = e.getComponent();
1098: if (source != null)
1099: try {
1100: tmp.x = e.getX();
1101: tmp.y = e.getY();
1102: transform.inverseTransform(tmp, tmp);
1103: /*
1104: * Calculates the (x0,y0) coordinates of the corner of the
1105: * rectangle. The (mouseDX, mouseDY) coordinates represent the
1106: * position of the mouse at the moment the button is pressed
1107: * and don't normally change (except during certain
1108: * adjustments). In determining (mouseDX, mouseDY), they is
1109: * calculated as if the user began to drag the rectangle at
1110: * the very corner, though in reality they could have clicked
1111: * anywhere.
1112: */
1113: double x0 = tmp.x - mouseDX;
1114: double y0 = tmp.y - mouseDY;
1115: double dx = drawnShape.getWidth();
1116: double dy = drawnShape.getHeight();
1117: final double oldWidth = dx;
1118: final double oldHeight = dy;
1119: /*
1120: * Deals with cases where, instead of dragging the rectangle,
1121: * the user is in the process of resizing it.
1122: */
1123: switch (adjustingLogicalSides & (EAST | WEST)) {
1124: case WEST: {
1125: if (x0 < xmin) {
1126: x0 = xmin;
1127: }
1128: dx += drawnShape.getX() - x0;
1129: if (!(dx > 0)) {
1130: dx = drawnShape.getWidth();
1131: x0 = drawnShape.getX();
1132: }
1133: break;
1134: }
1135: case EAST: {
1136: dx += x0 - (x0 = drawnShape.getX());
1137: final double limit = xmax - x0;
1138: if (dx > limit) {
1139: dx = limit;
1140: }
1141: if (!(dx > 0)) {
1142: dx = drawnShape.getWidth();
1143: x0 = drawnShape.getX();
1144: }
1145: break;
1146: }
1147: }
1148: switch (adjustingLogicalSides & (NORTH | SOUTH)) {
1149: case NORTH: {
1150: if (y0 < ymin) {
1151: y0 = ymin;
1152: }
1153: dy += drawnShape.getY() - y0;
1154: if (!(dy > 0)) {
1155: dy = drawnShape.getHeight();
1156: y0 = drawnShape.getY();
1157: }
1158: break;
1159: }
1160: case SOUTH: {
1161: dy += y0 - (y0 = drawnShape.getY());
1162: final double limit = ymax - y0;
1163: if (dy > limit)
1164: dy = limit;
1165: if (!(dy > 0)) {
1166: dy = drawnShape.getHeight();
1167: y0 = drawnShape.getY();
1168: }
1169: break;
1170: }
1171: }
1172: /*
1173: * The (x0, y0, dx, dy) coordinates now give the new position
1174: * and size of the rectangle. But, before making the change,
1175: * check whether only one edge was being adjusted. If so, we
1176: * cancel the changes with respect to the other edge (if not,
1177: * the user could move the rectangle vertically at the same
1178: * time as adjusting its right or left edge, which is not at
1179: * all practical...)
1180: */
1181: if ((adjustingLogicalSides & (NORTH | SOUTH)) != 0
1182: && (adjustingLogicalSides & (EAST | WEST)) == 0) {
1183: x0 = drawnShape.getX();
1184: dx = drawnShape.getWidth();
1185: }
1186: if ((adjustingLogicalSides & (NORTH | SOUTH)) == 0
1187: && (adjustingLogicalSides & (EAST | WEST)) != 0) {
1188: y0 = drawnShape.getY();
1189: dy = drawnShape.getHeight();
1190: }
1191: /*
1192: * If the user didn't adjusted any side, then make sure
1193: * that the logical size is conserved (i.e. discard the
1194: * "drawing" size if it was different).
1195: */
1196: if (adjustingLogicalSides == 0) {
1197: final double old_dx = logicalShape.getWidth();
1198: final double old_dy = logicalShape.getHeight();
1199: x0 += (dx - old_dx) / 2;
1200: y0 += (dy - old_dy) / 2;
1201: dx = old_dx;
1202: dy = old_dy;
1203: }
1204: /*
1205: * Modifies the rectangle's coordinates and signals that the
1206: * component needs redrawing.
1207: * Note: 'repaint' should be called before and after
1208: * 'setFrame' because the coordinates change.
1209: */
1210: source.repaint(x, y, width, height);
1211: try {
1212: setFrame(x0, y0, dx, dy);
1213: } catch (RuntimeException exception) {
1214: exception.printStackTrace();
1215: }
1216: source.repaint(x, y, width, height);
1217: /*
1218: * Adjustment for special cases.
1219: */
1220: if ((adjustingLogicalSides & EAST) != 0) {
1221: mouseDX += (drawnShape.getWidth() - oldWidth);
1222: }
1223: if ((adjustingLogicalSides & SOUTH) != 0) {
1224: mouseDY += (drawnShape.getHeight() - oldHeight);
1225: }
1226: } catch (NoninvertibleTransformException exception) {
1227: // Ignore.
1228: }
1229: }
1230: }
1231:
1232: /**
1233: * Method called automatically when the user releases the mouse button.
1234: * The default implementation calls {@link #stateChanged} with the
1235: * argument {@code false}, in order to inform the derived classes
1236: * that the changes are finished.
1237: */
1238: public void mouseReleased(final MouseEvent event) {
1239: if (isDragging
1240: && (event.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
1241: isDragging = false;
1242: final Component source = event.getComponent();
1243: try {
1244: tmp.x = event.getX();
1245: tmp.y = event.getY();
1246: mouseOverRect = drawnShape.contains(transform
1247: .inverseTransform(tmp, tmp));
1248: if (!mouseOverRect && source != null)
1249: source.setCursor(null);
1250: event.consume();
1251: } catch (NoninvertibleTransformException exception) {
1252: // Ignore this exception.
1253: }
1254: try {
1255: // It is essential that 'isDragging=false'.
1256: fireStateChanged();
1257: } catch (RuntimeException exception) {
1258: ExceptionMonitor.show(source, exception);
1259: }
1260: }
1261: }
1262:
1263: /**
1264: * Method called automatically <strong>before</strong> the position
1265: * or the size of the visor has changed. A call to
1266: * {@code stateWillChange} is normally followed by a call to
1267: * {@link #stateChanged}, <u>except</u> if the expected change
1268: * didn't ultimately occur. The derived classes can redefine this method
1269: * to take the necessary actions when a change is on the point of being
1270: * actioned. They must not, however, call any method which risks modifying
1271: * the state of this object. The default implementation does nothing.
1272: *
1273: * @param isAdjusting {@code true} if the user is still
1274: * modifying the position of the visor, {@code false}
1275: * if they have released the mouse button.
1276: */
1277: protected void stateWillChange(final boolean isAdjusting) {
1278: }
1279:
1280: /**
1281: * Method called automatically <strong>after</strong> the position and
1282: * size of the visor has changed. The call to {@code stateChanged}
1283: * must have been preceded by a call to {@link #stateWillChange}. The
1284: * derived classes can redefine this method to take the necessary
1285: * actions when a change has just been actioned. They must not, however,
1286: * call any method which risks modifying the state of this object. The
1287: * default implementation does nothing.
1288: *
1289: * @param isAdjusting {@code true} if the user is still
1290: * modifying the position of the visor, {@code false}
1291: * if they have released the mouse button.
1292: */
1293: protected void stateChanged(final boolean isAdjusting) {
1294: }
1295:
1296: /**
1297: * Method called automatically before the position or the
1298: * size of the visor has changed.
1299: */
1300: private void fireStateWillChange() {
1301: stateWillChange(isDragging);
1302: }
1303:
1304: /**
1305: * Method called automatically after the position or the
1306: * size of the visor has changed.
1307: */
1308: private void fireStateChanged() {
1309: updateEditors();
1310: stateChanged(isDragging);
1311: }
1312:
1313: /**
1314: * Updates the text in the editors. Each editor added by the
1315: * method {@link #addEditor addEditor(...)} will have its
1316: * text reformatted. This method can be called, for example,
1317: * after changing the format used by the editors. It is not
1318: * necessary to call this method each time the mouse moves;
1319: * it is done automatically.
1320: */
1321: public void updateEditors() {
1322: if (editors != null) {
1323: for (int i = 0; i < editors.length; i++) {
1324: editors[i].updateText();
1325: }
1326: }
1327: }
1328:
1329: /**
1330: * Adds an editor in which the user can explicitly specify the
1331: * coordinates of one of the edges of the rectangle. Each time
1332: * the user drags the rectangle, the text appearing in this editor
1333: * will automatically be updated. If the user explicitly enters
1334: * a new value in this editor, the position of the rectangle will be
1335: * adjusted.
1336: *
1337: * @param format Format to use for writing and interpreting the values
1338: * in the editor.
1339: * @param side Edge of the rectangle whose coordinates will be
1340: * controlled by the editor. It should be one of the
1341: * following constants:
1342: *
1343: * <table border align=center cellpadding=8 bgcolor=floralwhite>
1344: * <tr><td>{@link SwingConstants#NORTH_WEST}</td><td>{@link SwingConstants#NORTH}</td><td>{@link SwingConstants#NORTH_EAST}</td></tr>
1345: * <tr><td>{@link SwingConstants#WEST }</td><td> </td><td>{@link SwingConstants#EAST }</td></tr>
1346: * <tr><td>{@link SwingConstants#SOUTH_WEST}</td><td>{@link SwingConstants#SOUTH}</td><td>{@link SwingConstants#SOUTH_EAST}</td></tr>
1347: * </table>
1348: *
1349: * These constants designate the edge visible on screen. For example,
1350: * {@code NORTH} always designates the top edge on the screen.
1351: * However, this could correspond to another edge of the logical
1352: * shape {@code this} depending on the affine transform which was
1353: * specified during the last call to {@link #setTransform}. For example,
1354: * {@code AffineTransform.getScaleInstance(+1,-1)} has the effect of
1355: * inverting the y axis so that the <var>y</var><sub>max</sub> values
1356: * appear to the North rather than the <var>y</var><sub>min</sub> values.
1357: *
1358: * @param toRepaint Component to repaint after a field has been edited,
1359: * or {@code null} if there isn't one.
1360: *
1361: * @return An editor in which the user can specify the position of
1362: * one of the edges of the geometric shape.
1363: * @throws IllegalArgumentException if {@code side} isn't one
1364: * of the recognised codes.
1365: */
1366: public synchronized JComponent addEditor(final Format format,
1367: final int side, Component toRepaint)
1368: throws IllegalArgumentException {
1369: final JComponent component;
1370: final JFormattedTextField editor;
1371: if (format instanceof DecimalFormat) {
1372: final SpinnerNumberModel model = new SpinnerNumberModel();
1373: final JSpinner spinner = new JSpinner(model);
1374: final JSpinner.NumberEditor sedt = (JSpinner.NumberEditor) spinner
1375: .getEditor();
1376: final DecimalFormat targetFormat = sedt.getFormat();
1377: final DecimalFormat sourceFormat = (DecimalFormat) format;
1378: // TODO: Next lines would be much more efficient if only we had a
1379: // NumberEditor.setFormat(NumberFormat) method (See RFE #4520587)
1380: targetFormat.setDecimalFormatSymbols(sourceFormat
1381: .getDecimalFormatSymbols());
1382: targetFormat.applyPattern(sourceFormat.toPattern());
1383: editor = sedt.getTextField();
1384: component = spinner;
1385: } else if (format instanceof SimpleDateFormat) {
1386: final SpinnerDateModel model = new SpinnerDateModel();
1387: final JSpinner spinner = new JSpinner(model);
1388: final JSpinner.DateEditor sedt = (JSpinner.DateEditor) spinner
1389: .getEditor();
1390: final SimpleDateFormat targetFormat = sedt.getFormat();
1391: final SimpleDateFormat sourceFormat = (SimpleDateFormat) format;
1392: // TODO: Next lines would be much more efficient if only we had a
1393: // DateEditor.setFormat(DateFormat) method... (See RFE #4520587)
1394: targetFormat.setDateFormatSymbols(sourceFormat
1395: .getDateFormatSymbols());
1396: targetFormat.applyPattern(sourceFormat.toPattern());
1397: editor = sedt.getTextField();
1398: component = spinner;
1399: } else {
1400: component = editor = new JFormattedTextField(format);
1401: }
1402: /**
1403: * "9" is the default width of text fields. These widths are expressed
1404: * in number of columns. <i>Swing</i> does not appear to measure these
1405: * widths very accurately; it seems to provide more than requested.
1406: * For that reason, we specify a narrower width.
1407: */
1408: editor.setColumns(5);
1409: editor.setHorizontalAlignment(JTextField.RIGHT);
1410: Insets insets = editor.getMargin();
1411: insets.right += 2;
1412: editor.setMargin(insets);
1413: /*
1414: * Adds the editor to the list of editors to control. Increasing the
1415: * 'editors' array length each time is not a very efficient strategy,
1416: * but it will do because it is unlikely that we will ever add more
1417: * than 4 editors.
1418: */
1419: final Control control = new Control(editor,
1420: (format instanceof DateFormat),
1421: convertSwingConstant(side), toRepaint);
1422: if (editors == null) {
1423: editors = new Control[1];
1424: } else {
1425: editors = (Control[]) XArray.resize(editors,
1426: editors.length + 1);
1427: }
1428: editors[editors.length - 1] = control;
1429: return component;
1430: }
1431:
1432: /**
1433: * Removes an editor from the list of those which display the
1434: * coordinates of the visor.
1435: *
1436: * @param editor Editor to remove.
1437: */
1438: public synchronized void removeEditor(final JComponent editor) {
1439: if (editors != null) {
1440: for (int i = 0; i < editors.length; i++) {
1441: if (editors[i].editor == editor) {
1442: editors = (Control[]) XArray.remove(editors, i, 1);
1443: /*
1444: * In principal, there should be no more objects to
1445: * remove from the table. But we let the loop continue
1446: * anyway, just in case...
1447: */
1448: }
1449: }
1450: if (editors.length == 0) {
1451: editors = null;
1452: }
1453: }
1454: }
1455:
1456: /**
1457: * When the position of one of the rectangle's edges is edited manually,
1458: * specifies whether the opposite edge should also be adjusted. By default,
1459: * the edges are not synchronised.
1460: *
1461: * @param axis {@link SwingConstants#HORIZONTAL} to change the
1462: * synchronisation of the left and right edges, or
1463: * {@link SwingConstants#VERTICAL} to change the
1464: * synchronisation of the top and bottom edges.
1465: * @param state {@code true} to synchronise the edges, or
1466: * {@code false} to desynchronise.
1467: * @throws IllegalArgumentException if {@code axis}
1468: * isn't one of the valid codes.
1469: */
1470: public void setEditorsSynchronized(final int axis,
1471: final boolean state) throws IllegalArgumentException {
1472: switch (axis) {
1473: case SwingConstants.HORIZONTAL:
1474: synchronizeX = state;
1475: break;
1476: case SwingConstants.VERTICAL:
1477: synchronizeY = state;
1478: break;
1479: default:
1480: throw new IllegalArgumentException();
1481: }
1482: }
1483:
1484: /**
1485: * When the position of one of the rectangle's edges is edited manually,
1486: * specifies whether the opposite edge should also be adjusted. By default,
1487: * the edges are not synchronised.
1488: *
1489: * @param axis {@link SwingConstants#HORIZONTAL} to determine the
1490: * synchronisation of the left and right edges, or
1491: * {@link SwingConstants#VERTICAL} to determine the
1492: * synchronisation of the top and bottom edges.
1493: * @return {@code true} if the specified edges are synchronised,
1494: * or {@code false} if not
1495: * @throws IllegalArgumentException if {@code axis}
1496: * isn't one of the valid codes.
1497: */
1498: public boolean isEditorsSynchronized(final int axis)
1499: throws IllegalArgumentException {
1500: switch (axis) {
1501: case SwingConstants.HORIZONTAL:
1502: return synchronizeX;
1503: case SwingConstants.VERTICAL:
1504: return synchronizeY;
1505: default:
1506: throw new IllegalArgumentException();
1507: }
1508: }
1509:
1510: /**
1511: * Returns a character string representing this object.
1512: */
1513: public String toString() {
1514: return Utilities.getShortClassName(this ) + '['
1515: + Utilities.getShortClassName(logicalShape) + ']';
1516: }
1517:
1518: /**
1519: * Synchronises one of the rectangle's edges with a text field. Each time
1520: * the visor moves, the text will be updated. If, on the contrary, it is
1521: * the text which is manually edited, the visor will be repositioned.
1522: *
1523: * @version 1.0
1524: * @author Martin Desruisseaux
1525: */
1526: private final class Control implements PropertyChangeListener {
1527: /**
1528: * Text field representing the coordinate of one of the visor's
1529: * edges.
1530: */
1531: public final JFormattedTextField editor;
1532:
1533: /**
1534: * {@code true} if the field {@link #editor} formats dates,
1535: * or {@code false} if it formats numbers.
1536: */
1537: private final boolean isDate;
1538:
1539: /**
1540: * Side of the rectangle to be controlled. This field designates the
1541: * edge which is visible on screen. For example, {@code NORTH}
1542: * always designates the top edge on the screen. However, this could
1543: * correspond to another edge of the logical shape
1544: * {@link MouseReshapeTracker} depending on the affine transform that
1545: * was specified during the last call to
1546: * {@link MouseReshapeTracker#setTransform}. For example,
1547: * {@code AffineTransform.getScaleInstance(+1,-1)} has the effect
1548: * of inverting the y axis so that the <var>y</var><sub>max</sub>
1549: * values appear to the North rather than the
1550: * <var>y</var><sub>min</sub> values.
1551: */
1552: private final int side;
1553:
1554: /**
1555: * Component to repaint after the field is edited, or {@code null}
1556: * if there isn't one.
1557: */
1558: private final Component toRepaint;
1559:
1560: /**
1561: * Constructs an object which will control one of the rectangle's edges.
1562: *
1563: * @param editor Field which will contain the coordinate of the
1564: * rectangle's edge.
1565: * @param isDate {@code true} if the field {@link #editor} formats
1566: * dates, or {@code false} if it formats numbers.
1567: * @param side Edge of the rectangle to control. This argument
1568: * designates the edge visible on screen. For example,
1569: * {@code NORTH} always designates the top edge on the
1570: * screen. However, it can correspond to another edge of the
1571: * logical shape {@link MouseReshapeTracker} depending on the
1572: * affine transform which was specified during the last call
1573: * to {@link MouseReshapeTracker#setTransform}. For example,
1574: * {@code AffineTransform.getScaleInstance(+1,-1)} has the
1575: * effect of making the <var>y</var><sub>max</sub> values
1576: * appear to the "North" rather than the
1577: * <var>y</var><sub>min</sub> values.
1578: * @param toRepaint Component to repaint after the field has been
1579: * edited, or {@code null} if there isn't one.
1580: */
1581: public Control(final JFormattedTextField editor,
1582: final boolean isDate, final int side,
1583: final Component toRepaint) {
1584: this .editor = editor;
1585: this .isDate = isDate;
1586: this .side = side;
1587: this .toRepaint = toRepaint;
1588: updateText(editor);
1589: editor.addPropertyChangeListener("value", this );
1590: }
1591:
1592: /**
1593: * Method called automatically each time the value in the editor
1594: * changes.
1595: */
1596: public void propertyChange(final PropertyChangeEvent event) {
1597: final Object source = event.getSource();
1598: if (source instanceof JFormattedTextField) {
1599: final JFormattedTextField editor = (JFormattedTextField) source;
1600: final Object value = editor.getValue();
1601: if (value != null) {
1602: final double v = (value instanceof Date) ? ((Date) value)
1603: .getTime()
1604: : ((Number) value).doubleValue();
1605: if (!Double.isNaN(v)) {
1606: /*
1607: * Obtains the new coordinates of the rectangle,
1608: * taking into account the coordinates changed by the
1609: * user as well as the old coordinates which have not
1610: * changed.
1611: */
1612: final int side = inverseTransform(this .side);
1613: double Vxmin = (side & WEST) == 0 ? logicalShape
1614: .getMinX()
1615: : v;
1616: double Vxmax = (side & EAST) == 0 ? logicalShape
1617: .getMaxX()
1618: : v;
1619: double Vymin = (side & NORTH) == 0 ? logicalShape
1620: .getMinY()
1621: : v;
1622: double Vymax = (side & SOUTH) == 0 ? logicalShape
1623: .getMaxY()
1624: : v;
1625: if (synchronizeX || Vxmin > Vxmax) {
1626: final double dx = logicalShape.getWidth();
1627: if ((side & WEST) != 0)
1628: Vxmax = Vxmin + dx;
1629: if ((side & EAST) != 0)
1630: Vxmin = Vxmax - dx;
1631: }
1632: if (synchronizeY || Vymin > Vymax) {
1633: final double dy = logicalShape.getHeight();
1634: if ((side & NORTH) != 0)
1635: Vymax = Vymin + dy;
1636: if ((side & SOUTH) != 0)
1637: Vymin = Vymax - dy;
1638: }
1639: /*
1640: * Checks whether the new coordinates need a clip
1641: * adjustment. If so, we ask the method
1642: * 'clipChangeRequested' to make the change. This
1643: * 'clipChangeRequested' method doesn't have to accept
1644: * the change. The rest of the code will be correct
1645: * even if the clip hasn't changed (in that case the
1646: * position of the rectangle will still be adjusted
1647: * by 'setFrame').
1648: */
1649: if (Vxmin < xmin) {
1650: final double dx = Math.max(xmax - xmin,
1651: MINSIZE_RATIO * (Vxmax - Vxmin));
1652: final double margin = Vxmax + dx
1653: * ((MINSIZE_RATIO - 1) * 0.5);
1654: clipChangeRequested(margin - dx, margin,
1655: ymin, ymax);
1656: } else if (Vxmax > xmax) {
1657: final double dx = Math.max(xmax - xmin,
1658: MINSIZE_RATIO * (Vxmax - Vxmin));
1659: final double margin = Vxmin - dx
1660: * ((MINSIZE_RATIO - 1) * 0.5);
1661: clipChangeRequested(margin, margin + dx,
1662: ymin, ymax);
1663: }
1664: if (Vymin < ymin) {
1665: final double dy = Math.max(ymax - ymin,
1666: MINSIZE_RATIO * (Vymax - Vymin));
1667: final double margin = Vymax + dy
1668: * ((MINSIZE_RATIO - 1) * 0.5);
1669: clipChangeRequested(xmin, xmax,
1670: margin - dy, margin);
1671: } else if (Vymax > ymax) {
1672: final double dy = Math.max(ymax - ymin,
1673: MINSIZE_RATIO * (Vymax - Vymin));
1674: final double margin = Vymin - dy
1675: * ((MINSIZE_RATIO - 1) * 0.5);
1676: clipChangeRequested(xmin, xmax, margin,
1677: margin + dy);
1678: }
1679: /*
1680: * Repositions the rectangle based on the new
1681: * coordinates.
1682: */
1683: if (setFrame(Vxmin, Vymin, Vxmax - Vxmin, Vymax
1684: - Vymin)) {
1685: if (toRepaint != null)
1686: toRepaint.repaint();
1687: }
1688: }
1689: }
1690: updateText(editor);
1691: }
1692: }
1693:
1694: /**
1695: * Called each time the position of the rectangle is adjusted. This
1696: * method will adjust the value displayed in the text field
1697: * based on the position of the rectangle.
1698: */
1699: private void updateText(final JFormattedTextField editor) {
1700: String text;
1701: if (!logicalShape.isEmpty()
1702: || ((text = editor.getText()) != null && text
1703: .trim().length() != 0)) {
1704: double value;
1705: switch (inverseTransform(side)) {
1706: case NORTH:
1707: value = logicalShape.getMinY();
1708: break;
1709: case SOUTH:
1710: value = logicalShape.getMaxY();
1711: break;
1712: case WEST:
1713: value = logicalShape.getMinX();
1714: break;
1715: case EAST:
1716: value = logicalShape.getMaxX();
1717: break;
1718: default:
1719: return;
1720: }
1721: editor.setValue(isDate ? (Object) new Date(Math
1722: .round(value)) : (Object) new Double(value));
1723: }
1724: }
1725:
1726: /**
1727: * Updates the text which appears in {@link #editor}
1728: * based on the current position of the rectangle.
1729: */
1730: public void updateText() {
1731: updateText(editor);
1732: }
1733: }
1734: }
|