// Charles A. Loomis, Jr., and University of California, Santa Cruz,
// Copyright (c) 2000
package org.freehep.swing.graphics;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import javax.swing.SwingUtilities;
import org.freehep.swing.images.FreeHepImage;
/**
* This abstract class defines the majority of the functionality
* needed to make selections of arbitrary parallelogram regions
* on the screen.
*
* @author Charles Loomis
* @author Mark Donszelmann
* @version $Id: AbstractRegionSelectionPanel.java 8584 2006-08-10 23:06:37Z duns $ */
abstract public class AbstractRegionSelectionPanel
extends GraphicalSelectionPanel {
/**
* A constant which flags that no control point was near the
* mouse-pressed event. */
final public static int NO_CONTROL_POINT = -1;
/**
* Flag indicating whether or not additional guide lines should be
* visible. */
protected boolean visibleGuides;
/**
* The bounding rectangle of the next box to be drawn. */
protected Rectangle rectToDraw = new Rectangle();
/**
* The bounding rectangle of the last box which was drawn. */
protected Rectangle lastDrawnRect = new Rectangle();
/**
* The bounding box of the region to repaint, usually the union of
* the rectToDraw and lastDrawnRect rectangles. */
protected Rectangle updateRect = new Rectangle();
/**
* Flag indicating whether or not the selection box is visible. */
protected boolean visible;
/**
* Flag indicating whether or not the last drawn rectangle is
* valid. (Not valid when the box is first made visible.) */
protected boolean lastDrawnRectValid;
/**
* The maximum distance from a control point the cursor can be and
* still be selected. */
protected int hitThreshold = 10;
/**
* The size of the control point boxes. */
protected static int ctrlPtSize = 1;
/**
* The number of control points for this component. */
protected int nCtrlPts;
/**
* Which control point is the active one, or which one can be
* controlled from the arrow keys on the keyboard? */
protected int activeCtrlPt;
/**
* The x-coordinates of the control points. The first four of
* these control points MUST define the outer boundries of the
* selected region. */
protected int[] xCtrlPts;
/**
* The y-coordinates of the control points. The first four of
* these control points MUST define the outer boundries of the
* selected region. */
protected int[] yCtrlPts;
/**
* This constructor makes a new AbstractRegionSelectionPanel.
* This constructor only sets the visiblilty flag and the
* last-drawn rectangle valid flag to false. */
public AbstractRegionSelectionPanel() {
// First make the selection region invisible and invalidate
// the last-drawn rectangle.
visible = false;
lastDrawnRectValid = false;
setSelectionActionsEnabled(false);
// The guides are by default visible.
visibleGuides = true;
// Create the arrays of the x- and y-coordinates. There must
// be at least four control points.
nCtrlPts = Math.max(4,getNumberOfControlPoints());
xCtrlPts = new int[nCtrlPts];
yCtrlPts = new int[nCtrlPts];
activeCtrlPt = NO_CONTROL_POINT;
// set the default cursor
setCursor();
}
/**
* Determine whether or not to display guide lines. */
public void setVisibleGuides(boolean visibleGuides) {
this.visibleGuides = visibleGuides;
repaintPanel();
}
/**
* Get whether or not the guides are visible. */
public boolean getVisibleGuides() {
return visibleGuides;
}
/**
* Process key-released events. This allows selection panels
* which derive from this one to automatically have the default
* behaviour.
*
*<pre>
* arrow keys: move the active control point in the specified
* direction.
* backspace key: reset selection region (make invisible).
* delete key: reset selection region (make invisible).
* escape key: leave selection mode (make component invisible).
* tab key: next selection mode (make next component visible).
* enter key: accept selection region (send off region selected
* event)
* spacebar: accept selection region (send off region selected
* event)
* </pre>
*
* @param e KeyEvent describing the key which has been released */
public void keyReleased(KeyEvent e) {
// Change the size of the increment in the given direction.
int increment = (e.isShiftDown()) ? 1 : 2;
switch (e.getKeyCode()) {
case KeyEvent.VK_UP:
moveActiveControlPoint(0,-increment);
break;
case KeyEvent.VK_DOWN:
moveActiveControlPoint(0,increment);
break;
case KeyEvent.VK_RIGHT:
moveActiveControlPoint(increment,0);
break;
case KeyEvent.VK_LEFT:
moveActiveControlPoint(-increment,0);
break;
default:
super.keyReleased(e);
break;
}
}
/**
* A utility function which creates an appropriate selection event
* when the user accepts the current selection. */
protected void makeSelectionEvent(int actionCode) {
switch (actionCode) {
case GraphicalSelectionEvent.DEFAULT_MODE:
resetSelection();
setVisible(false);
fireGraphicalSelectionMade(new
GraphicalSelectionEvent(this,
GraphicalSelectionEvent.DEFAULT_MODE,
null,null));
break;
case GraphicalSelectionEvent.NEXT_MODE:
resetSelection();
setVisible(false);
fireGraphicalSelectionMade(new
GraphicalSelectionEvent(this,
GraphicalSelectionEvent.NEXT_MODE,
null,null));
break;
case GraphicalSelectionEvent.PREVIOUS_MODE:
resetSelection();
setVisible(false);
fireGraphicalSelectionMade(new
GraphicalSelectionEvent(this,
GraphicalSelectionEvent.PREVIOUS_MODE,
null,null));
break;
default:
if (visible) {
// Make an array of points describing the corners of this
// rectangular region. NOTE: the first four control
// points must define the outer extent of the selected
// region.
Point[] points = new Point[4];
for (int i=0; i<4; i++) {
points[i] = new Point(xCtrlPts[i], yCtrlPts[i]);
}
// Send off the event to interested parties.
fireGraphicalSelectionMade(new
RegionSelectionEvent(this,actionCode,
makeOutlinePolygon(),
makeAffineTransform()));
}
resetSelection();
break;
}
}
/**
* Sets the cursor to whatever the current active control point dictates.
*/
private void setCursor() {
// active cursor
Cursor cursor = getControlPointCursor(activeCtrlPt);
// default cursor if no active cursor
if (cursor == null) {
cursor = getControlPointCursor(NO_CONTROL_POINT);
}
// set only when available
if (cursor != null) {
setCursor(cursor);
}
}
/**
* Changes the active control point according to mouse movements
*
*/
public void mouseMoved(MouseEvent e) {
if (!isProcessingPopup(e)) {
if (visible) {
int newCtrlPt = nearWhichControlPoint(e.getX(), e.getY(),
hitThreshold);
if (newCtrlPt != activeCtrlPt) {
activeCtrlPt = newCtrlPt;
setCursor();
repaintPanel();
return;
}
}
}
}
/**
* Handle the mousePressed events. */
public void mousePressed(MouseEvent e) {
// Only do something if this isn't part of a popup menu
// selection.
if (!isProcessingPopup(e)) {
// If the selection box is visible AND the user has
// clicked near one of the existing control points, make
// the nearest one the active control point and update the
// current selection. Return when finished.
if (visible) {
int newCtrlPt = nearWhichControlPoint(e.getX(), e.getY(),
hitThreshold);
if (newCtrlPt>=0) {
activeCtrlPt = newCtrlPt;
setCursor();
repaintPanel();
return;
}
}
// User wants to start a new selection. So first set the
// flag to make the selection region visible.
visible = true;
setSelectionActionsEnabled(true);
// Get the mouse point and force point within boundries.
int x = forceXCoordinateWithinBounds(e.getX());
int y = forceYCoordinateWithinBounds(e.getY());
// The initialize method is responsible for setting all of
// the control points to reasonable values and for setting
// which point should be the active one.
activeCtrlPt = NO_CONTROL_POINT;
initializeControlPoints(x,y);
setCursor();
// Update the display.
repaintPanel();
}
}
/**
* A utility method which forces the x-coordinate to be within the
* component boundries.
*
* @param x x-coordinate to force within boundries
* @return modified x-value */
public int forceXCoordinateWithinBounds(int x) {
int xmin = 0;
int xmax = getWidth()-1;
return Math.max(Math.min(x,xmax),xmin);
}
/**
* A utility method which forces the y-coordinate to be within the
* component boundries.
*
* @param y y-coordinate to force within boundries
* @return modified y-value */
public int forceYCoordinateWithinBounds(int y) {
int ymin = 0;
int ymax = getHeight()-1;
return Math.max(Math.min(y,ymax),ymin);
}
public void mouseDragged(MouseEvent e) {
if (!isProcessingPopup(e)) {
updateActiveControlPoint(e.getX(),e.getY());
setCursor();
}
}
public void mouseReleased(MouseEvent e) {
if (!isProcessingPopup(e)) {
updateActiveControlPoint(e.getX(),e.getY());
if (!isValidSelection()) resetSelection();
setCursor();
} else {
activeCtrlPt = NO_CONTROL_POINT;
setCursor();
}
}
/**
* This returns whether the current selected region is valid.
* Generally if the area has zero volume, then this method should
* return false. */
abstract public boolean isValidSelection();
/**
* A utility method which moves the currently active control point
* by the given delta-x and delta-y. It does this by calling
* updateActiveControlPoint(x,y), so subclasses shouldn't normally
* need to override this method.
*
* @param dx the distance to move the x-coordinate
* @param dy the distance to move the y-coordinate */
protected void moveActiveControlPoint(int dx, int dy) {
if (activeCtrlPt>=0) {
int x = xCtrlPts[activeCtrlPt] + dx;
int y = yCtrlPts[activeCtrlPt] + dy;
updateActiveControlPoint(x,y);
}
}
/**
* Check to see if the point (x,y) is near one of the control
* points. If it is, return the index of the nearest one,
* otherwise return NO_CONTROL_POINT.
*
* @param x x-coordinate to compare to control points
* @param y y-coordinate to compare to control points
* @param maxDist the maximum distance from a control point which
* still selects it
*
* @return the index of the nearest control point */
protected int nearWhichControlPoint(int x, int y, int maxDist) {
// Initialize to no control point selected.
int nearestCtrlPt = NO_CONTROL_POINT;
int minDist2 = -1;
// Loop over all control points and get the closest one.
// (Actually calculate distance-squared here.)
for (int i=0; i<nCtrlPts; i++) {
int dx = x-xCtrlPts[i];
int dy = y-yCtrlPts[i];
int dist = dx*dx + dy*dy;
if (dist<minDist2 || i==0) {
minDist2 = dist;
nearestCtrlPt = i;
}
}
// If the closest one isn't close enough, delete the index.
if (minDist2>maxDist*maxDist)
nearestCtrlPt = NO_CONTROL_POINT;
return nearestCtrlPt;
}
/**
* Make the selection box invisible. */
public void resetSelection() {
visible = false;
lastDrawnRectValid = false;
setSelectionActionsEnabled(false);
activeCtrlPt = NO_CONTROL_POINT;
setCursor();
repaintPanel();
}
/**
* Repaint the panel. Calculate the bounding box of the selection
* box along with associated control points. */
protected void repaintPanel() {
// Find the bounding points for the polygon.
int x0 = xCtrlPts[0];
int y0 = yCtrlPts[0];
int x1 = xCtrlPts[0];
int y1 = yCtrlPts[0];
for (int i=1; i<nCtrlPts; i++) {
if (xCtrlPts[i]<x0) x0 = xCtrlPts[i];
if (yCtrlPts[i]<y0) y0 = yCtrlPts[i];
if (xCtrlPts[i]>x1) x1 = xCtrlPts[i];
if (yCtrlPts[i]>y1) y1 = yCtrlPts[i];
}
// Adjust for the size of the active control point and line
// widths.
x0 -= ctrlPtSize+2;
y0 -= ctrlPtSize+2;
x1 += ctrlPtSize+2;
y1 += ctrlPtSize+2;
// Set the bounds of the current polygon.
rectToDraw.setRect(x0,y0,x1-x0,y1-y0);
// Repaint the new bounds.
updateRect.setBounds(rectToDraw);
if (lastDrawnRectValid) {
updateRect = SwingUtilities.computeUnion(lastDrawnRect.x,
lastDrawnRect.y,
lastDrawnRect.width,
lastDrawnRect.height,
updateRect);
}
repaint(updateRect);
}
/**
* Initialize the control points. Subclasses must provide an
* implementation of this method which initializes the control
* points to reasonable values given the first mouse-pressed
* coordinates, and must also set the activeCtrlPt to the index of
* the control point which should be active.
*
* @param x x-coordinate of initial mouse-pressed event
* @param y y-coordinate of initial mouse-pressed event */
abstract public void initializeControlPoints(int x, int y);
/**
* Change the active control point to the point (x,y). Subclasses
* should implement this routine to get the behaviour which is
* desired. This is the place to impose constraints on how the
* control points can move. NOTE: repaintPanel() should be called
* at the end of this method to update the display.
*
* @param x x-coordinate of the new point
* @param y y-coordinate of the new point */
abstract public void updateActiveControlPoint(int x, int y);
/**
* Useful subclasses must define the number of control points on
* the selected region. The first four control points define the
* outer extent of the selected region. This method MUST NOT
* return a number less than four. */
abstract public int getNumberOfControlPoints();
/**
* Returns the Cursor to be displayed for a certain control point
* and the default cursor for this SelectionPanel for an index of
* NO_CONTROL_POINT. Return of null will not change the cursor.
* Subclasses should override this method to provide a default
* cursor and/or to provide cursors for the different control points.
*/
public Cursor getControlPointCursor(int index) {
return null;
}
/**
* Repaint this component. This must be overridden by subclasses
* so that the selection region appears correctly. The subclass
* should check the visibility flag (visible) to decide if any
* painting needs to be done.
*
* @param g Graphics context in which to draw */
public void paintComponent(Graphics g) {
super.paintComponent(g);
// Update the last drawn rectangle.
lastDrawnRectValid = true;
lastDrawnRect.setBounds(rectToDraw);
}
/**
* Make the outline of the selection. Note that the order of the
* points is not guaranteed.
*
* @return a polygon object which describes the outline of
* the selection */
public Polygon makeOutlinePolygon() {
Polygon polygon = new Polygon();
// Only take the first four control points, since these define
// the outline of the selection.
for (int i=0; i<4; i++) {
polygon.addPoint(xCtrlPts[i], yCtrlPts[i]);
}
return polygon;
}
/**
* Make the affine transform which corresponds to this rectangular
* selection.
*
* @return AffineTransform which describes the selected region */
abstract public AffineTransform makeAffineTransform();
/**
* A utility which makes an AffineTransform given three corner
* points. The first point must be the upper, left-hand corner
* point, the second, the upper, right-hand corner point, and the
* third, the lower, right-hand corner point.
*
* @return AffineTransform which does the appropriate mapping */
protected AffineTransform makeTransform(double x0, double y0,
double x1, double y1,
double x2, double y2) {
double sx = 0.;
double kx = 0.;
double tx = 0.;
double sy = 0.;
double ky = 0.;
double ty = 0.;
double delta = (x2*(y1-y0)-x1*(y2-y0)+x0*(y2-y1));
if (delta==0) {
return null;
} else {
delta = 1./delta;
double w = getWidth();
double h = getHeight();
sx = -(delta*w)*(y2-y1);
kx = (delta*w)*(x2-x1);
tx = -(x0*sx+y0*kx);
ky = (delta*h)*(y1-y0);
sy = -(delta*h)*(x1-x0);
ty = -(x0*ky+y0*sy);
return new AffineTransform(sx,ky,kx,sy,tx,ty);
}
}
/**
* returns the appropriate cursor for any of the
* compass points. If both dx and dy are zero, null is returned
*
* @param type type of cursor (Resize/Rotation)
* @param dx screen x of direction
* @param dy screen y of direction (positive is down)
* @param n number of compass points (4 or 8)
* @param diagonal in case n = 4, a diagonal compass point is returned
* @return XX_RESIZE_CURSOR
*/
public static Cursor compassCursor(String type, int dx, int dy, int n, boolean diagonal) {
if ((dx == 0) && (dy == 0)) return null;
double offset;
if (n == 4) {
offset = (diagonal) ? 0 : Math.PI/4;
} else {
n = 8;
offset = Math.PI/8;
}
double delta = 2*Math.PI/n;
double alpha = (Math.atan2(-dy, dx) + 2*Math.PI + offset) % (2*Math.PI);
int d = (int)(alpha / delta);
if (n == 4) {
d = (diagonal) ? d * 2 + 1 : d * 2;
}
switch(d) {
case 0: return FreeHepImage.getCursor("E_"+type+"Cursor", 16, 16);
case 1: return FreeHepImage.getCursor("NE_"+type+"Cursor", 16, 16);
case 2: return FreeHepImage.getCursor("N_"+type+"Cursor", 16, 16);
case 3: return FreeHepImage.getCursor("NW_"+type+"Cursor", 16, 16);
case 4: return FreeHepImage.getCursor("W_"+type+"Cursor", 16, 16);
case 5: return FreeHepImage.getCursor("SW_"+type+"Cursor", 16, 16);
case 6: return FreeHepImage.getCursor("S_"+type+"Cursor", 16, 16);
case 7: return FreeHepImage.getCursor("SE_"+type+"Cursor", 16, 16);
}
System.err.println("compassCursor invalid value: "+d);
return Cursor.getDefaultCursor();
}
}