components/visual-panels/core/src/java/util/com/oracle/solaris/vp/util/swing/time/AnalogClock.java
changeset 827 0944d8c0158b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/visual-panels/core/src/java/util/com/oracle/solaris/vp/util/swing/time/AnalogClock.java	Thu May 24 04:16:47 2012 -0400
@@ -0,0 +1,1203 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License (the "License").
+ * You may not use this file except in compliance with the License.
+ *
+ * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+ * or http://www.opensolaris.org/os/licensing.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ */
+
+/*
+ * Copyright (c) 2009, 2012, Oracle and/or its affiliates. All rights reserved.
+ */
+
+package com.oracle.solaris.vp.util.swing.time;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.geom.*;
+import java.awt.image.BufferedImage;
+import java.beans.*;
+import java.util.Calendar;
+import javax.swing.*;
+import org.jdesktop.animation.timing.*;
+import com.oracle.solaris.vp.util.misc.finder.Finder;
+import com.oracle.solaris.vp.util.swing.*;
+
+@SuppressWarnings({"serial"})
+public class AnalogClock extends JPanel implements MouseListener,
+    MouseMotionListener, TimingTarget {
+
+    //
+    // Static data
+    //
+
+    private static final int _DIAMETER = 150;
+
+    //
+    // Instance data
+    //
+
+    private TimeModel model;
+    private TimeSelectionModel selModel;
+
+    private PropertyChangeListener timeListener =
+	new PropertyChangeListener() {
+	    @Override
+	    public void propertyChange(PropertyChangeEvent event) {
+		timeChanged();
+	    }
+	};
+
+    // Cached images to speed up repaints
+    private BufferedImage baseImage;
+    private BufferedImage timeImage;
+    private BufferedImage centerImage;
+
+    private int frameWidth;
+    private Point centerPoint;
+    private Point mousePoint;
+    private boolean interactive;
+    private boolean useToolTips;
+    private SunIcon sunIcon = new SunIcon();
+    private MoonIcon moonIcon = new MoonIcon();
+    private StarIcon starIcon = new StarIcon();
+    private Shape amPm;
+    private Point hourPoint;
+    private Point minPoint;
+    private Point secPoint;
+
+    private Animator amPmAnimator = new Animator(300, this);
+    private float amPmAnimOffset;
+    private int amPmAnimFactor;
+
+    // UI prefs
+    private Color centerColor = Color.black;
+    private Color frameColor = ColorUtil.darker(
+	UIManager.getColor("Panel.background"), .2f);
+    private Color faceColor = Color.white;
+    private Color hourHandColor = Color.black;
+    private Color hourMarkColor = Color.black;
+    private float lightAngle = (float)Math.toRadians(122);
+    private Color minuteHandColor = Color.black;
+    private Color minuteMarkColor = Color.gray;
+    private Color secondHandColor = Color.red.darker();
+    private Color highlightColor = new Color(0, 0, 204);
+    private Color amColor = new Color(148, 188, 246);
+    private Color pmColor = new Color(0, 0, 78);
+
+    //
+    // Constructors
+    //
+
+    /**
+     * Constructs an {@code AnalogClock} for the given model.
+     */
+    public AnalogClock(TimeModel model) {
+	setTimeModel(model);
+	setTimeSelectionModel(new SimpleTimeSelectionModel());
+	setOpaque(false);
+	setPreferredDiameter(_DIAMETER);
+    }
+
+    /**
+     * Constructs an {@code AnalogClock} with a zero {@link
+     * TimeModel#getTimeOffset offset} from the system clock.
+     */
+    public AnalogClock() {
+	this(new SimpleTimeModel());
+    }
+
+    //
+    // MouseListener methods
+    //
+
+    @Override
+    public void mouseClicked(MouseEvent e) {
+	int selectedField = selModel.getSelectedField();
+	if (selectedField == Calendar.AM_PM && GUIUtil.isUnmodifiedClick(e, 1))
+	{
+	    amPmAnimOffset = (float)Math.PI;
+
+	    amPmAnimFactor = model.getCalendar().get(
+		selectedField) == Calendar.AM ? 1 : -1;
+
+	    // 43200000 = 1000 * 60 * 60 * 12 = milliseconds in 12-hour change
+	    long delta = amPmAnimFactor * 43200000;
+	    model.setTimeOffset(model.getTimeOffset() + delta);
+
+	    amPmAnimator.start();
+	}
+    }
+
+    @Override
+    public void mouseEntered(MouseEvent e) {
+    }
+
+    @Override
+    public void mouseExited(MouseEvent e) {
+	mousePoint = null;
+	repaint();
+    }
+
+    @Override
+    public void mousePressed(MouseEvent e) {
+	mousePoint = null;
+	repaint();
+    }
+
+    @Override
+    public void mouseReleased(MouseEvent e) {
+	mouseMoved(e);
+    }
+
+    //
+    // MouseMotionListener methods
+    //
+
+    @Override
+    public void mouseDragged(MouseEvent e) {
+	int selectedField = selModel.getSelectedField();
+	if (selectedField != Calendar.HOUR &&
+	    selectedField != Calendar.MINUTE &&
+	    selectedField != Calendar.SECOND) {
+	    return;
+	}
+
+	Point centerPoint = this.centerPoint;
+	Point dragPoint = e.getPoint();
+	int dY = centerPoint.y - dragPoint.y;
+	int dX = dragPoint.x - centerPoint.x;
+	float angle = (float)Math.atan2(dY, dX);
+
+	int newVal;
+	int period;
+
+	if (selectedField == Calendar.HOUR) {
+	    newVal = (int)radiansToHour(angle);
+	    period = 12;
+	} else {
+	    newVal = (int)radiansToMin(angle);
+	    period = 60;
+	}
+
+	Calendar cal = model.getCalendar();
+	int curVal = cal.get(selectedField);
+	int factor = (newVal - curVal + period) % period;
+	if (factor >= period / 2) {
+	    factor -= period;
+	}
+
+        long timeOffset = model.getTimeOffset() + factor *
+	    timeFieldToMillis(selectedField);
+	model.setTimeOffset(timeOffset);
+    }
+
+    @Override
+    public void mouseMoved(MouseEvent e) {
+	mousePoint = e.getPoint();
+	repaint();
+    }
+
+    //
+    // TimingTarget methods
+    //
+
+    @Override
+    public void begin() {
+    }
+
+    @Override
+    public void end() {
+    }
+
+    @Override
+    public void repeat() {
+    }
+
+    @Override
+    public void timingEvent(float fraction) {
+	amPmAnimOffset = amPmAnimFactor * (float)Math.PI * (1 - fraction);
+	timeImage = null;
+	repaint();
+    }
+
+    //
+    // JComponent methods
+    //
+
+    @Override
+    protected void paintComponent(Graphics g1) {
+	super.paintComponent(g1);
+
+	Graphics2D g = (Graphics2D)g1;
+	g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+	    RenderingHints.VALUE_ANTIALIAS_ON);
+
+	Insets insets = getInsets();
+	int width = getWidth() - insets.left - insets.right;
+	int height = getHeight() - insets.top - insets.bottom;
+
+	if (width <= 0 || height <= 0) {
+	    return;
+	}
+
+	// TimeModel is square
+	int diameter = Math.min(width, height);
+
+	int diff = height - width;
+	int hDiff = diff / 2;
+	if (diff > 0) {
+	    diameter = width;
+	    insets.top += hDiff;
+	    insets.bottom += diff - hDiff;
+	} else if (diff < 0) {
+	    diameter = height;
+	    insets.left -= hDiff;
+	    insets.right -= diff - hDiff;
+	}
+
+	g.translate(insets.left, insets.top);
+
+	createBaseImage(diameter);
+	g.drawImage(baseImage, 0, 0, null);
+
+	g.translate(frameWidth, frameWidth);
+
+	diameter -= 2 * frameWidth;
+	int radius = Math.round(diameter/ 2f);
+
+	createTimeImage(diameter);
+	g.drawImage(timeImage, 0, 0, null);
+
+	Point centerPoint = new Point(radius, radius);
+	Point mousePoint = this.mousePoint;
+
+	if (mousePoint != null) {
+	    int selectedField = -1;
+	    mousePoint = (Point)mousePoint.clone();
+	    mousePoint.translate(-frameWidth - insets.left,
+		-frameWidth - insets.top);
+
+	    Line2D line = new Line2D.Float();
+
+	    line.setLine(centerPoint, hourPoint);
+	    double hourDist = line.ptSegDist(mousePoint);
+
+	    line.setLine(centerPoint, minPoint);
+	    double minDist = line.ptSegDist(mousePoint);
+
+	    line.setLine(centerPoint, secPoint);
+	    double secDist = line.ptSegDist(mousePoint);
+
+	    // Is the mouse close enough to a hand to call it selected?
+	    double closest = Math.min(hourDist, Math.min(minDist, secDist));
+	    if (closest / diameter <= .02f) {
+		if (hourDist == closest) {
+		    selectedField = Calendar.HOUR;
+		    drawHourHand(g, diameter, centerPoint, hourPoint);
+		} else if (minDist == closest) {
+		    selectedField = Calendar.MINUTE;
+		    drawMinuteHand(g, diameter, centerPoint, minPoint);
+		} else {
+		    selectedField = Calendar.SECOND;
+		    drawSecondHand(g, diameter, centerPoint, secPoint);
+		}
+	    } else if (amPm.contains(mousePoint)) {
+		selectedField = Calendar.AM_PM;
+	    }
+
+	    setSelectedField(selectedField);
+	}
+
+	createCenterImage(diameter);
+	g.drawImage(centerImage, 0, 0, null);
+
+	centerPoint.translate(frameWidth + insets.left,
+	    frameWidth + insets.top);
+
+	this.centerPoint = centerPoint;
+
+	g.translate(-frameWidth - insets.left, -frameWidth - insets.top);
+    }
+
+    //
+    // AnalogClock methods
+    //
+
+    protected float amPmToRadians() {
+	Calendar cal = model.getCalendar();
+	int hour = cal.get(Calendar.HOUR);
+	int minute = cal.get(Calendar.MINUTE);
+	int second = cal.get(Calendar.SECOND);
+	int amPm = cal.get(Calendar.AM_PM);
+	return amPmToRadians(amPm, hour, minute, second);
+    }
+
+    protected Shape drawAmPm(Graphics2D g, int diameter) {
+	// Create shapes
+
+	float outerMargin = diameter * .17f;
+	float innerMargin = diameter * .105f;
+
+	diameter -= Math.round(2 * outerMargin);
+	float radius = diameter / 2f;
+
+	int offset = Math.round(outerMargin);
+	g.translate(offset, offset);
+
+	Arc2D.Float outer = new Arc2D.Float(0, 0,
+	    diameter, diameter, 0, 180, Arc2D.OPEN);
+
+	int innerDiam = Math.round(2 * innerMargin);
+	Arc2D.Float inner = new Arc2D.Float(radius - innerMargin,
+	    radius - innerMargin, innerDiam, innerDiam, 180, -180, Arc2D.OPEN);
+
+	Line2D.Float[] lines = {
+	    new Line2D.Float(outer.getEndPoint(), inner.getStartPoint()),
+	    new Line2D.Float(inner.getEndPoint(), outer.getStartPoint()),
+	};
+
+	GeneralPath window = new GeneralPath(outer);
+	window.append(lines[0], true);
+	window.append(inner, true);
+	window.append(lines[1], true);
+	window.closePath();
+
+	// Fill background
+
+	float angle = (float)Math.PI - amPmToRadians() - amPmAnimOffset;
+	float cos = (float)Math.cos(angle);
+	float sin = (float)Math.sin(angle);
+	float xDelta = radius * cos;
+	float yDelta = radius * sin;
+
+	g.setPaint(new GradientPaint(
+	    Math.round(radius + xDelta), Math.round(radius + yDelta), amColor,
+	    Math.round(radius - xDelta), Math.round(radius - yDelta), pmColor));
+	g.fill(window);
+
+	// Paint icons
+
+	Shape clip = g.getClip();
+	g.setClip(window);
+
+	float arcWidth = radius - innerMargin;
+	float midPointToCtr = innerMargin + arcWidth * .5f;
+	int sunIconDiam = (int)(arcWidth * .7f);
+	float sunIconRad = sunIconDiam / 2f;
+	xDelta = midPointToCtr * cos;
+	yDelta = midPointToCtr * sin;
+
+	sunIcon.setDiameter(sunIconDiam);
+	sunIcon.paintIcon(this, g, Math.round(radius + xDelta - sunIconRad),
+	    Math.round(radius + yDelta - sunIconRad));
+
+	int moonIconDiam = (int)(sunIconDiam * sunIcon.getOrbSizePercentage());
+	float moonIconRad = moonIconDiam / 2f;
+
+	moonIcon.setDiameter(moonIconDiam);
+	moonIcon.paintIcon(this, g, Math.round(radius - xDelta - moonIconRad),
+	    Math.round(radius - yDelta - moonIconRad));
+
+	int starIconDiam = (int)(moonIconDiam * .4f);
+	float starIconRad = starIconDiam / 2f;
+
+	starIcon.setDiameter(starIconDiam);
+
+	// Angle (degree) offset from moon, percent distance between arcs
+	float[] starData = {
+	    -48f, .55f,
+	    -30f, .22f,
+	    -25f, .7f,
+	    5f, .85f,
+	    20f, .25f,
+	    25f, .65f,
+	    45f, .35f,
+	};
+
+	for (int i = 0; i < starData.length; i += 2) {
+	    float starAngle = (float)Math.toRadians(starData[i]);
+	    float starDist = innerMargin + arcWidth * starData[i + 1];
+
+	    int x = Math.round(radius - starDist *
+		(float)Math.cos(angle + starAngle) - starIconRad);
+
+	    int y = Math.round(radius - starDist *
+		(float)Math.sin(angle + starAngle) - starIconRad);
+
+	    starIcon.paintIcon(this, g, x, y);
+	}
+
+	g.setClip(clip);
+
+	// Paint inset border
+
+	Stroke stroke = g.getStroke();
+	g.setStroke(new BasicStroke(diameter / 80));
+
+	cos = (float)Math.cos(lightAngle);
+	sin = (float)Math.sin(lightAngle);
+	xDelta = radius * cos;
+	yDelta = radius * sin;
+
+	Color color = Color.gray;
+	Color shadow = ColorUtil.darker(color, .25f);
+	Color light = ColorUtil.darker(Color.white, .25f);
+
+	g.setPaint(new GradientPaint(radius + xDelta, radius - yDelta, shadow,
+	    radius - xDelta, radius + yDelta, light, false));
+	g.draw(outer);
+
+	xDelta = innerMargin * cos;
+	yDelta = innerMargin * sin;
+
+	g.setPaint(new GradientPaint(radius + xDelta, radius - yDelta, light,
+	    radius - xDelta, radius + yDelta, shadow, false));
+	g.draw(inner);
+
+	float theta = lightAngle - (float)Math.PI / 2f;
+	float pct = (1 - (float)Math.cos(theta)) / 2f;
+
+	g.setColor(ColorUtil.blend(light, shadow, pct));
+	g.draw(lines[0]);
+	g.draw(lines[1]);
+
+	g.setStroke(stroke);
+	g.translate(-offset, -offset);
+
+	Shape shape = AffineTransform.getTranslateInstance(offset, offset).
+	    createTransformedShape(window);
+
+	return shape;
+    }
+
+    protected void drawCenterDot(Graphics2D g, int diameter) {
+	float radius = diameter / 2f;
+	int dotDiam = Math.round(diameter * .06f);
+	float dotRadius = dotDiam / 2f;
+	float pctFromCtr = .75f;
+	float distFromCtr = dotRadius * pctFromCtr;
+	float xCenter = radius + distFromCtr * (float)Math.cos(lightAngle);
+	float yCenter = radius - distFromCtr * (float)Math.sin(lightAngle);
+
+	Color focusColor = ColorUtil.lighter(centerColor, .65f);
+	float[] fractions = {0.0f, 1.0f};
+	Color[] colors = {focusColor, centerColor};
+	RadialGradientPaint paint = new RadialGradientPaint(
+	    xCenter, yCenter, dotRadius, fractions, colors,
+	    MultipleGradientPaint.CycleMethod.NO_CYCLE);
+	g.setPaint(paint);
+	int coord = Math.round(radius - dotRadius);
+	g.fillOval(coord, coord, dotDiam, dotDiam);
+    }
+
+    protected void drawFace(Graphics2D g, int diameter) {
+	g.setColor(faceColor);
+	g.fillOval(0, 0, diameter, diameter);
+    }
+
+    protected int drawFrame(Graphics2D g, int diameter) {
+	// Draw outline
+	int frameWidth = 0;
+	float radius = diameter / 2f;
+
+	float cos = (float)Math.cos(lightAngle);
+	float sin = (float)Math.sin(lightAngle);
+	float pctFromEdge = .67f;
+	float xStart = radius * (1 + cos);
+	float yStart = radius * (1 - sin);
+	float xEnd = radius * (1 + pctFromEdge * cos);
+	float yEnd = radius * (1 - pctFromEdge * sin);
+
+	g.setPaint(new GradientPaint(xStart, yStart,
+	    ColorUtil.darker(Color.white, .25f), xEnd, yEnd,
+	    ColorUtil.darker(frameColor, .25f), false));
+	g.fillOval(frameWidth, frameWidth, diameter, diameter);
+
+	// Draw outer edge
+	int gap = Math.round(diameter * .006f);
+	frameWidth += gap;
+	diameter -= 2 * gap;
+	radius = diameter / 2f;
+
+	xStart = radius * (1 + cos);
+	yStart = radius * (1 - sin);
+	xEnd = radius * (1 + pctFromEdge * cos);
+	yEnd = radius * (1 - pctFromEdge * sin);
+
+	g.setPaint(new GradientPaint(xStart, yStart, Color.white, xEnd, yEnd,
+	    frameColor, false));
+	g.fillOval(frameWidth, frameWidth, diameter, diameter);
+
+	// Draw inner edge
+	gap = Math.round(diameter * .025f);
+	frameWidth += gap;
+	diameter -= 2 * gap;
+	radius = diameter / 2f;
+
+	cos = (float)Math.cos(lightAngle + Math.PI);
+	sin = (float)Math.sin(lightAngle + Math.PI);
+	pctFromEdge = .5f;
+	xStart = radius * (1 + cos);
+	yStart = radius * (1 - sin);
+	xEnd = radius * (1 + pctFromEdge * cos);
+	yEnd = radius * (1 - pctFromEdge * sin);
+
+	g.setPaint(new GradientPaint(xStart, yStart,
+	    ColorUtil.lighter(frameColor, .55f), xEnd, yEnd, frameColor,
+	    false));
+	g.fillOval(frameWidth, frameWidth, diameter, diameter);
+
+	gap = Math.round(diameter * .011f);
+	frameWidth += gap;
+
+	return frameWidth;
+    }
+
+    protected void drawHand(Graphics2D g, Color color, float handWidth,
+	Point p1, Point p2) {
+
+	Stroke stroke = g.getStroke();
+	g.setStroke(new BasicStroke(handWidth, BasicStroke.CAP_ROUND,
+	    BasicStroke.JOIN_MITER));
+
+	g.setColor(color);
+	g.drawLine(p1.x, p1.y, p2.x, p2.y);
+
+	g.setStroke(stroke);
+    }
+
+    protected Point drawHand(Graphics2D g, Color color, float handWidth,
+	float handLength, float angle, int diameter) {
+
+	float radius = diameter / 2f;
+	float cos = (float)Math.cos(angle);
+	float sin = (float)Math.sin(angle);
+	int x = Math.round(radius + handLength * cos);
+	int y = Math.round(radius - handLength * sin);
+
+	// Shadow
+	float offset = diameter * .02f;
+	cos = (float)Math.cos(lightAngle + Math.PI);
+	sin = (float)Math.sin(lightAngle + Math.PI);
+	int xOffset = Math.round(offset * cos);
+	int yOffset = -Math.round(offset * sin);
+
+	drawHand(g, ColorUtil.alpha(Color.black, .2f), handWidth,
+	    new Point((int)radius + xOffset, (int)radius + yOffset),
+	    new Point(x + xOffset, y + yOffset));
+
+	// Hand
+	Point p = new Point(x, y);
+	drawHand(g, color, handWidth, new Point((int)radius, (int)radius), p);
+
+	return p;
+    }
+
+    protected void drawHourHand(Graphics2D g, int diameter, Point p1,
+	Point p2) {
+	drawHand(g, highlightColor, diameter * .02f, p1, p2);
+    }
+
+    protected Point drawHourHand(Graphics2D g, int diameter) {
+	return drawHand(g, hourHandColor, diameter * .02f, diameter * .275f,
+	    hourToRadians(), diameter);
+    }
+
+    protected void drawHourMarks(Graphics2D g, int diameter) {
+	float markWidth = diameter * .015f;
+	float markLength = diameter * .04f;
+	float pctFromCtr = .9f;
+	float radius = diameter / 2f;
+
+	Stroke stroke = g.getStroke();
+	g.setStroke(new BasicStroke(markWidth, BasicStroke.CAP_ROUND,
+	    BasicStroke.JOIN_MITER));
+	g.setColor(hourMarkColor);
+
+	float angleConst = (float)Math.PI / 6f;
+	float distFromCtrStart = radius * pctFromCtr;
+	float distFromCtrEnd = distFromCtrStart - markLength;
+
+	for (int i = 0; i < 12; i++) {
+	    float angle = i * angleConst;
+	    float cos = (float)Math.cos(angle);
+	    float sin = (float)Math.sin(angle);
+
+	    int xStart = Math.round(radius + distFromCtrStart * cos);
+	    int yStart = Math.round(radius - distFromCtrStart * sin);
+
+	    int xEnd = Math.round(radius + distFromCtrEnd * cos);
+	    int yEnd = Math.round(radius - distFromCtrEnd * sin);
+
+	    g.drawLine(xStart, yStart, xEnd, yEnd);
+	}
+
+	g.setStroke(stroke);
+    }
+
+    protected void drawMinuteHand(Graphics2D g, int diameter, Point p1,
+	Point p2) {
+	drawHand(g, highlightColor, diameter * .02f, p1, p2);
+    }
+
+    protected Point drawMinuteHand(Graphics2D g, int diameter) {
+	return drawHand(g, minuteHandColor, diameter * .02f, diameter * .405f,
+	    minToRadians(), diameter);
+    }
+
+    protected void drawMinuteMarks(Graphics2D g, int diameter) {
+	float markWidth = diameter * .009f;
+	float markLength = diameter * .015f;
+	float pctFromCtr = .9f;
+	float radius = diameter / 2f;
+
+	Stroke stroke = g.getStroke();
+	g.setStroke(new BasicStroke(markWidth, BasicStroke.CAP_ROUND,
+	    BasicStroke.JOIN_MITER));
+	g.setColor(minuteMarkColor);
+
+	float angleConst = (float)Math.PI / 30f;
+	float distFromCtrStart = radius * pctFromCtr;
+	float distFromCtrEnd = distFromCtrStart - markLength;
+
+	for (int i = 0; i < 60; i++) {
+	    if (i % 5 != 0) {
+		float angle = i * angleConst;
+		float cos = (float)Math.cos(angle);
+		float sin = (float)Math.sin(angle);
+
+		int xStart = Math.round(radius + distFromCtrStart * cos);
+		int yStart = Math.round(radius - distFromCtrStart * sin);
+
+		int xEnd = Math.round(radius + distFromCtrEnd * cos);
+		int yEnd = Math.round(radius - distFromCtrEnd * sin);
+
+		g.drawLine(xStart, yStart, xEnd, yEnd);
+	    }
+	}
+
+	g.setStroke(stroke);
+    }
+
+    protected void drawSecondHand(Graphics2D g, int diameter, Point p1,
+	Point p2) {
+	drawHand(g, highlightColor, diameter * .011f, p1, p2);
+    }
+
+    protected Point drawSecondHand(Graphics2D g, int diameter) {
+	return drawHand(g, secondHandColor, diameter * .011f,
+	    diameter * .405f, secToRadians(), diameter);
+    }
+
+    protected void drawShadow(Graphics2D g, int diameter) {
+	// This puts the shadow around the entire face, which also looks nice
+	// float offset = 0f;
+	// float[] fractions = {0f, .95f, 1f};
+
+	float radius = diameter / 2f;
+	Color shadow = Color.black;
+	Color focus = ColorUtil.alpha(Color.black, 0f);
+	float[] fractions = {0f, .94f, 1f};
+	Color[] colors = {focus, focus, shadow};
+
+	float cos = (float)Math.cos(lightAngle + Math.PI);
+	float sin = (float)Math.sin(lightAngle + Math.PI);
+	float offset = radius * .02f;
+	float xOffset = offset * cos;
+	float yOffset = -offset * sin;
+	RadialGradientPaint paint = new RadialGradientPaint(radius + xOffset,
+	    radius + yOffset, radius + offset, fractions, colors,
+	    MultipleGradientPaint.CycleMethod.NO_CYCLE);
+
+	g.setPaint(paint);
+	g.fillOval(0, 0, diameter, diameter);
+    }
+
+    /**
+     * Gets the color at the AM end of the AM-PM gradient.
+     */
+    public Color getAmColor() {
+	return amColor;
+    }
+
+    /**
+     * Gets the {@code Animator} the controls the animation of the interactive
+     * change from AM to PM and back.
+     */
+    public Animator getAmPmAnimator() {
+	return amPmAnimator;
+    }
+
+    /**
+     * Gets the color of the center dial of the clock.
+     */
+    public Color getCenterColor() {
+	return centerColor;
+    }
+
+    /**
+     * Gets the color of the face of the clock.
+     */
+    public Color getFaceColor() {
+	return faceColor;
+    }
+
+    /**
+     * Gets the color of the outer frame of the clock.
+     */
+    public Color getFrameColor() {
+	return frameColor;
+    }
+
+    /**
+     * Gets the color to use when highlighting a hand.
+     */
+    public Color getHighlightColor() {
+	return highlightColor;
+    }
+
+    public Color getHourHandColor() {
+	return hourHandColor;
+    }
+
+    /**
+     * Gets the color of the hour mark on the clock face.
+     */
+    public Color getHourMarkColor() {
+	return hourMarkColor;
+    }
+
+    /**
+     * Gets the angle of the light, in radians, with 0 at three o'clock, shining
+     * onto the clock.
+     */
+    public float getLightAngle() {
+	return lightAngle;
+    }
+
+    public Color getMinuteHandColor() {
+	return minuteHandColor;
+    }
+
+    /**
+     * Gets the color of the minute mark on the clock face.
+     */
+    public Color getMinuteMarkColor() {
+	return minuteMarkColor;
+    }
+
+    /**
+     * Gets the color at the PM end of the AM-PM gradient.
+     */
+    public Color getPmColor() {
+	return pmColor;
+    }
+
+    public Color getSecondHandColor() {
+	return secondHandColor;
+    }
+
+    public TimeModel getTimeModel() {
+	return model;
+    }
+
+    public TimeSelectionModel getTimeSelectionModel() {
+	return selModel;
+    }
+
+    /**
+     * Gets whether to use instructional tooltips over the interactive portions
+     * of the clock.
+     */
+    public boolean getUseToolTips() {
+	return useToolTips;
+    }
+
+    protected float hourToRadians() {
+	Calendar cal = model.getCalendar();
+	int hour = cal.get(Calendar.HOUR);
+	int minute = cal.get(Calendar.MINUTE);
+	int second = cal.get(Calendar.SECOND);
+	return hourToRadians(hour, minute, second);
+    }
+
+    /**
+     * Gets whether this {@code AnalogClock}'s time can be set by dragging the
+     * hands.
+     */
+    public boolean isInteractive() {
+	return interactive;
+    }
+
+    protected float minToRadians() {
+	Calendar cal = model.getCalendar();
+	int minute = cal.get(Calendar.MINUTE);
+	int second = cal.get(Calendar.SECOND);
+	return minToRadians(minute, second);
+    }
+
+    protected float secToRadians() {
+	Calendar cal = model.getCalendar();
+	int second = cal.get(Calendar.SECOND);
+	return secToRadians(second);
+    }
+
+    /**
+     * Sets the color at the AM end of the AM-PM gradient.
+     */
+    public void setAmColor(Color amColor) {
+	this.amColor = amColor;
+	repaint();
+    }
+
+    /**
+     * Sets the color of the center dial of the clock.
+     */
+    public void setCenterColor(Color centerColor) {
+	this.centerColor = centerColor;
+	repaint();
+    }
+
+    /**
+     * Sets the preferred widht and height of this {@code AnalogClock} to {@code
+     * diameter}.
+     */
+    public void setPreferredDiameter(int diameter) {
+	setPreferredSize(new Dimension(diameter, diameter));
+    }
+
+    /**
+     * Sets the color of the face of the clock.
+     */
+    public void setFaceColor(Color faceColor) {
+	this.faceColor = faceColor;
+	baseImage = null;
+	repaint();
+    }
+
+    /**
+     * Sets the color of the outer frame of the clock.
+     */
+    public void setFrameColor(Color frameColor) {
+	this.frameColor = frameColor;
+	baseImage = null;
+	repaint();
+    }
+
+    /**
+     * Sets the color to use when highlighting a hand.
+     */
+    public void setHighlightColor(Color highlightColor) {
+	this.highlightColor = highlightColor;
+    }
+
+    public void setHourHandColor(Color hourHandColor) {
+	this.hourHandColor = hourHandColor;
+	repaint();
+    }
+
+    /**
+     * Sets the color of the hour mark on the clock face.
+     */
+    public void setHourMarkColor(Color hourMarkColor) {
+	this.hourMarkColor = hourMarkColor;
+	baseImage = null;
+	repaint();
+    }
+
+    /**
+     * Sets whether this {@code AnalogClock}'s time can be set by dragging the
+     * hands.
+     */
+    public void setInteractive(boolean interactive) {
+	if (this.interactive != interactive) {
+	    this.interactive = interactive;
+	    if (interactive) {
+		addMouseListener(this);
+		addMouseMotionListener(this);
+	    } else {
+		removeMouseListener(this);
+		removeMouseMotionListener(this);
+		mousePoint = null;
+	    }
+	}
+    }
+
+    /**
+     * Sets the angle of the light, in radians, with 0 at three o'clock, shining
+     * onto the clock.
+     */
+    public void setLightAngle(float lightAngle) {
+	this.lightAngle = lightAngle;
+	baseImage = null;
+	timeImage = null;
+	centerImage = null;
+	repaint();
+    }
+
+    public void setMinuteHandColor(Color minuteHandColor) {
+	this.minuteHandColor = minuteHandColor;
+	repaint();
+    }
+
+    /**
+     * Sets the color of the minute mark on the clock face.
+     */
+    public void setMinuteMarkColor(Color minuteMarkColor) {
+	this.minuteMarkColor = minuteMarkColor;
+	baseImage = null;
+	repaint();
+    }
+
+    /**
+     * Sets the color at the PM end of the AM-PM gradient.
+     */
+    public void setPmColor(Color pmColor) {
+	this.pmColor = pmColor;
+	repaint();
+    }
+
+    public void setSecondHandColor(Color secondHandColor) {
+	this.secondHandColor = secondHandColor;
+	repaint();
+    }
+
+    public void setTimeModel(TimeModel model) {
+	if (this.model != model) {
+	    if (this.model != null) {
+		this.model.removePropertyChangeListener(timeListener);
+	    }
+
+	    model.addPropertyChangeListener(TimeModel.PROPERTY_TIME,
+		timeListener);
+
+	    this.model = model;
+	    timeChanged();
+	}
+    }
+
+    public void setTimeSelectionModel(TimeSelectionModel selModel) {
+	this.selModel = selModel;
+    }
+
+    /**
+     * Sets whether to use instructional tooltips over the interactive portions
+     * of the clock.
+     */
+    public void setUseToolTips(boolean useToolTips) {
+	if (this.useToolTips != useToolTips) {
+	    this.useToolTips = useToolTips;
+	    setToolTipText();
+	}
+    }
+
+    //
+    // Private methods
+    //
+
+    private void createBaseImage(int diameter) {
+	if (baseImage == null || baseImage.getWidth() != diameter) {
+	    baseImage = new BufferedImage(diameter, diameter,
+		BufferedImage.TYPE_INT_ARGB);
+
+	    Graphics2D g = baseImage.createGraphics();
+	    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+		RenderingHints.VALUE_ANTIALIAS_ON);
+
+	    frameWidth = drawFrame(g, diameter);
+	    diameter -= 2 * frameWidth;
+
+	    g.translate(frameWidth, frameWidth);
+
+	    drawFace(g, diameter);
+	    drawHourMarks(g, diameter);
+	    drawMinuteMarks(g, diameter);
+	    drawShadow(g, diameter);
+
+	    g.translate(-frameWidth, -frameWidth);
+	}
+    }
+
+    private void createCenterImage(int diameter) {
+	if (centerImage == null || centerImage.getWidth() != diameter) {
+	    centerImage = new BufferedImage(diameter, diameter,
+		BufferedImage.TYPE_INT_ARGB);
+
+	    Graphics2D g = centerImage.createGraphics();
+	    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+		RenderingHints.VALUE_ANTIALIAS_ON);
+
+	    drawCenterDot(g, diameter);
+	}
+    }
+
+    private void createTimeImage(int diameter) {
+	if (timeImage == null || timeImage.getWidth() != diameter) {
+	    timeImage = new BufferedImage(diameter, diameter,
+		BufferedImage.TYPE_INT_ARGB);
+
+	    Graphics2D g = timeImage.createGraphics();
+	    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+		RenderingHints.VALUE_ANTIALIAS_ON);
+
+	    amPm = drawAmPm(g, diameter);
+	    hourPoint = drawHourHand(g, diameter);
+	    minPoint = drawMinuteHand(g, diameter);
+	    secPoint = drawSecondHand(g, diameter);
+	}
+    }
+
+    private void setSelectedField(int selectedField) {
+	setCursor(selectedField == -1 ? null :
+	    Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+	selModel.setSelectedField(selectedField);
+
+	setToolTipText();
+    }
+
+    private void setToolTipText() {
+	String resource = null;
+
+	if (useToolTips) {
+	    switch (selModel.getSelectedField()) {
+		case Calendar.AM_PM:
+		    resource = "time.tooltip.ampm";
+		    break;
+
+		case Calendar.HOUR:
+		    resource = "time.tooltip.hour";
+		    break;
+
+		case Calendar.MINUTE:
+		    resource = "time.tooltip.minute";
+		    break;
+
+		case Calendar.SECOND:
+		    resource = "time.tooltip.second";
+		    break;
+	    }
+	}
+
+	setToolTipText(resource == null ? null : Finder.getString(resource));
+    }
+
+    private void timeChanged() {
+	timeImage = null;
+	repaint();
+    }
+
+    //
+    // Static methods
+    //
+
+    public static float amPmToRadians(int amPm, int hour, int minute,
+	int second) {
+
+	hour = (hour % 12) + (amPm == Calendar.PM ? 12 : 0);
+	return (6 - (hour + minute / 60f + second / 3600f)) * (float)Math.PI /
+	    12f;
+    }
+
+    public static float hourToRadians(int hour, int minute, int second) {
+	return (15 - (hour + minute / 60f + second / 3600f)) * (float)Math.PI /
+	    6f;
+    }
+
+    public static float minToRadians(int minute, int second) {
+	return (75 - (minute + second / 60f)) * (float)Math.PI / 30f;
+    }
+
+    public static float radiansToHour(float radians) {
+	float value = (-6 * radians / (float)Math.PI) + 3;
+	if (value < 1) {
+	    value += 12;
+	}
+	return value;
+    }
+
+    public static float radiansToMin(float radians) {
+	float value = (-30 * radians / (float)Math.PI) + 15;
+	if (value < 0) {
+	    value += 60;
+	}
+	return value;
+    }
+
+    public static float secToRadians(int second) {
+	return (75 - second) * (float)Math.PI / 30f;
+    }
+
+    /**
+     * Converts {@code Calendar} time fields to milliseconds.
+     *
+     * @param	    calendarField
+     *		    {@code Calendar.MILLISECOND},
+     *		    {@code Calendar.SECOND},
+     *		    {@code Calendar.MINUTE},
+     *		    {@code Calendar.HOUR},
+     *		    {@code Calendar.HOUR_OF_DAY}, or
+     *		    {@code Calendar.AM_PM},
+     *
+     * @return	    the number of milliseconds in a unit of the given field, or
+     *		    zero if {@code calendarField} is not any of the allowed
+     *		    fields
+     */
+    public static long timeFieldToMillis(int calendarField) {
+	long millis = 0;
+	switch (calendarField) {
+	    case Calendar.MILLISECOND:
+		millis = 1;
+		break;
+
+	    case Calendar.SECOND:
+		millis = 1000;
+		break;
+
+	    case Calendar.MINUTE:
+		millis = 60000;
+		break;
+
+	    case Calendar.HOUR:
+	    case Calendar.HOUR_OF_DAY:
+		millis = 3600000;
+		break;
+
+	    case Calendar.AM_PM:
+		millis = 43200000;
+		break;
+	}
+
+	return millis;
+    }
+
+    public static void main(String args[]) {
+	AnalogClock clock = new AnalogClock();
+	clock.setInteractive(true);
+	clock.setPreferredDiameter(300);
+
+	TimeModelUpdater updater = new TimeModelUpdater(clock.getTimeModel());
+	updater.start();
+
+	JFrame frame = new JFrame();
+	Container c = frame.getContentPane();
+	c.setLayout(new BorderLayout());
+	c.add(clock, BorderLayout.CENTER);
+	frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+	frame.pack();
+	frame.setVisible(true);
+    }
+}