/*
* Licensed Materials - Property of Rogue Wave Software, Inc.
* © Copyright Rogue Wave Software, Inc. 2014, 2017
* © Copyright IBM Corp. 2009, 2014
* © Copyright ILOG 1996, 2009
* All Rights Reserved.
*
* Note to U.S. Government Users Restricted Rights:
* The Software and Documentation were developed at private expense and
* are "Commercial Items" as that term is defined at 48 CFR 2.101,
* consisting of "Commercial Computer Software" and
* "Commercial Computer Software Documentation", as such terms are
* used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4,
* as applicable.
*/
import java.awt.AWTEvent;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import javax.swing.JLabel;
import javax.swing.JPanel;
/**
* A dial ring that allows to select an angle (goniometer).
* The center of the dial ring has space to display another component,
* whose size is controlled by the dial ring parameters.
* @proofread
*/
public class JDial extends JPanel
{
// the inner panel of the dial that can be used for further contents
JPanel contents;
// the thickness of the dial
int thickness = 25;
// whether we fill the outer shape of the dial
boolean drawOuter = false;
// the base color, should be rather dark
Color color = Color.gray;
// the angle color
Color angleColor = new Color(200, 0, 0);
// the start angle
int startAngle = 0;
// the current angle
int angle = 0;
// show the angle color at 4 sides.
boolean showAngleColorAt4Sides;
// when drawing the outer shape, we have space for 4 labels
JLabel label1;
JLabel label2;
JLabel label3;
JLabel label4;
// inner and outer ellipse
transient Shape outerEllipse = new Ellipse2D.Double(0, 0, 1, 1);
transient Shape innerEllipse = new Ellipse2D.Double(1, 1, 2, 2);
transient Shape innerClipEllipse = new Ellipse2D.Double(1, 1, 2, 2);
/**
* Creates a new dial.
* @param drawOuter Whether the dial fills the outer rectangular area.
* It should be false for inner dials when dials are nested.
*/
public JDial(boolean drawOuter)
{
this.drawOuter = drawOuter;
contents = new JPanel() {
// allow only the inner circle to be active for events
Override
public boolean contains(int x, int y) {
x += thickness;
y += thickness;
return innerEllipse.contains(x, y);
}
};
contents.setLayout(new BorderLayout());
contents.setForeground(Color.pink);
contents.setBackground(Color.pink);
setLayout(null);
add(contents);
if (drawOuter) {
label1 = new JLabel("");
label2 = new JLabel("");
label3 = new JLabel("");
label4 = new JLabel("");
super.add(label1);
super.add(label2);
super.add(label3);
super.add(label4);
}
enableEvents(AWTEvent.KEY_EVENT_MASK |
AWTEvent.MOUSE_WHEEL_EVENT_MASK |
AWTEvent.MOUSE_EVENT_MASK |
AWTEvent.MOUSE_MOTION_EVENT_MASK);
}
/**
* Sets the thickness of the dial.
*/
public void setThickness(int t)
{
thickness = t;
}
/**
* Returns the thickness of the dial.
*/
public int getThickness()
{
return thickness;
}
/**
* Sets the color of the dial.
* Since due the highlighting, the dial appears rather light, darker colors
* are preferred.
* The default color is gray.
*/
public void setColor(Color c)
{
color = c;
}
/**
* Returns the color of the dial.
*/
public Color getColor()
{
return color;
}
/**
* Sets the color of the angle area of the dial.
* The default color is a light red.
*/
public void setAngleColor(Color c)
{
angleColor = c;
}
/**
* Returns the color of the angle area of the dial.
*/
public Color getAngleColor()
{
return angleColor;
}
/**
* Sets whether the angle color is not only shown from startAngle to
* angle, but also from startAngle to -angle, and from startAngle+180
* to -/- angle.
* This makes only sense if the angle range is 0..90.
*/
public void setShowAngleColorAt4Sides(boolean b)
{
showAngleColorAt4Sides = b;
}
/**
* Sets the start angle of the dial in degree.
* 0 degree is displayed on the left side of the dial.
*/
public void setStartAngle(int a)
{
startAngle = a;
repaintOnInteraction();
}
/**
* Returns the start angle of the dial.
*/
public int getStartAngle()
{
return startAngle;
}
/**
* Sets the current angle of the dial in degree.
*/
public void setAngle(int a)
{
angle = a;
repaintOnInteraction();
}
/**
* Returns the current angle of the dial in degree.
*/
public int getAngle()
{
return angle;
}
/**
* Returns the top left label.
* If the outer rectangular area is not drawn, it returns null.
*/
public JLabel getTopLeftLabel()
{
return label1;
}
/**
* Returns the top right label.
* If the outer rectangular area is not drawn, it returns null.
*/
public JLabel getTopRightLabel()
{
return label2;
}
/**
* Returns the bottom left label.
* If the outer rectangular area is not drawn, it returns null.
*/
public JLabel getBottomLeftLabel()
{
return label3;
}
/**
* Returns the bottom right label.
* If the outer rectangular area is not drawn, it returns null.
*/
public JLabel getBottomRightLabel()
{
return label4;
}
/**
* Returns the inner content of the dial.
*/
public Component getContents()
{
if (contents.getComponentCount() > 0)
return contents.getComponent(0);
return null;
}
/**
* Add a component displayed in the inner of the dial.
*/
Override
public Component add(Component c)
{
if (c == contents)
super.add(c);
else {
contents.removeAll();
contents.add(c, BorderLayout.CENTER);
}
return c;
}
/**
* Moves and resizes the component.
* @see Component#setBounds
*/
Override
public void setBounds(int x, int y, int w, int h)
{
if (w != h) {
w = h = Math.min(w, h);
}
if (w < 2*thickness + 10) w = 2*thickness + 10;
if (h < 2*thickness + 10) h = 2*thickness + 10;
super.setBounds(x, y, w, h);
int margin = ((contents instanceof JDial) ? 2 : 0);
int t = getThickness();
contents.setBounds(t + margin,
t + margin,
w - 2 * (t + margin),
h - 2 * (t + margin));
outerEllipse = new Ellipse2D.Double(0, 0, w, h);
innerEllipse = new Ellipse2D.Double(t, t, w-2*t, h-2*t);
innerClipEllipse = new Ellipse2D.Double(0, 0, w-2*t, h-2*t);
if (drawOuter) {
Dimension s = label1.getPreferredSize();
label1.setBounds(15, 15, s.width, s.height);
s = label2.getPreferredSize();
label2.setBounds(w-15-s.width, 15, s.width, s.height);
s = label3.getPreferredSize();
label3.setBounds(15, h-15-s.height, s.width, s.height);
s = label4.getPreferredSize();
label4.setBounds(w-15-s.width, h-15-s.height, s.width, s.height);
}
}
/**
* Paint the dial.
*/
Override
public void paint(Graphics g) {
Graphics2D dst = (Graphics2D)g;
Object old = dst.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
dst.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
try {
super.paint(g);
} finally {
dst.setRenderingHint(RenderingHints.KEY_ANTIALIASING, old);
}
}
/**
* Paint the children of the component.
*/
Override
public void paintChildren(Graphics g) {
Graphics2D dst = (Graphics2D)g;
Object old = dst.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
dst.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
Area shape = new Area(innerEllipse);
Shape oldClip = dst.getClip();
dst.setClip(shape);
try {
super.paintChildren(g);
} finally {
dst.setClip(oldClip);
dst.setRenderingHint(RenderingHints.KEY_ANTIALIASING, old);
}
}
/**
* Paint the border of the component.
*/
Override
public void paintBorder(Graphics g) {
Graphics2D dst = (Graphics2D)g;
Object old = dst.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
dst.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
try {
paintBorderImpl(dst);
} finally {
dst.setRenderingHint(RenderingHints.KEY_ANTIALIASING, old);
}
}
private void paintBorderImpl(Graphics2D dst) {
super.paintBorder(dst);
// technically, the dial is part of the border, so that it always overlaps
// the content
int t = getThickness();
Rectangle r = getBounds();
dst.setColor(Color.black);
dst.fillRect(0,0,r.width,r.height);
Area shape = new Area(outerEllipse);
shape.subtract(new Area(innerEllipse));
Color c = color;
dst.setColor(c);
dst.fill(shape);
int i = 0;
int now = r.width - 7;
int niw = r.width - 2 * t + 7;
int noh = r.height - 7;
int nih = r.height - 2 * t + 7;
// draw the shading
for (i = 0; i < 4; i++) {
now = r.width - 7 - 2 * i;
niw = r.width - 2 * t + 7 + 2 * i;
noh = r.height - 7 - 2 * i;
nih = r.height - 2 * t + 7 + 2 * i;
if (now <= niw) break;
if (noh <= nih) break;
shape = new Area(new Ellipse2D.Double(2+i, 2+i, now, noh));
shape.subtract(new Area(new Ellipse2D.Double(t-5-i, t-5-i, niw, nih)));
c = new Color(Math.min(255, c.getRed() + 25),
Math.min(255, c.getGreen() + 25),
Math.min(255, c.getBlue() + 25));
dst.setColor(c);
dst.fill(shape);
}
// draw the highlight
shape = new Area(new Ellipse2D.Double(2+i, 2+i, now, noh));
shape.subtract(new Area(new Ellipse2D.Double(4+i, 4+i, now, noh)));
dst.setColor(Color.white);
dst.fill(shape);
shape = new Area(new Ellipse2D.Double(t-2-i, t-2-i, niw, nih));
shape.subtract(new Area(new Ellipse2D.Double(t-4-i, t-4-i, niw, nih)));
dst.fill(shape);
// draw the handle
c = new Color(c.getRed()-25, c.getGreen()-25, c.getBlue()-25, 100);
for (i = 3; i >= 0; i--) {
if (i == 0)
c = new Color(0,0,0);
else
c = new Color(Math.max(0, c.getRed() - 40),
Math.max(0, c.getGreen() - 40),
Math.max(0, c.getBlue() - 40), 100);
shape = new Area(new Ellipse2D.Double(0, 0, r.width, r.height));
shape.subtract(new Area(new Ellipse2D.Double(t, t,
r.width - 2 * t, r.height - 2 * t)));
shape.intersect(new Area(getArrowPointFromAngle(r, 1+i)));
dst.setColor(c);
dst.fill(shape);
}
if (angleColor != null)
drawDelta(dst, r);
drawScale(dst, r);
if (drawOuter)
drawOuterArea(dst, r);
}
/**
* Calculates the points of the indicator arrow.
*/
private Shape getArrowPointFromAngle(Rectangle bounds, int size)
{
int radius = Math.max(bounds.width / 2, bounds.height / 2) + 1;
double a = (startAngle + angle) * 2 * Math.PI / 360;
double x0 = bounds.width / 2;
double y0 = bounds.height / 2;
double x1 = bounds.width / 2 - Math.cos(a) * radius;
double y1 = bounds.height / 2 - Math.sin(a) * radius;
double dx = x1 - x0;
double dy = y1 - y0;
double d = Math.sqrt(dx * dx + dy * dy);
double odx = size * dy / d;
double ody = -size * dx / d;
GeneralPath gp = new GeneralPath();
gp.moveTo((x0 + odx), (y0 + ody));
gp.lineTo((x0 - odx), (y0 - ody));
gp.lineTo((x1 - odx), (y1 - ody));
gp.lineTo((x1 + odx), (y1 + ody));
gp.closePath();
return gp;
}
/**
* Draws the delta area between start angle and end angle,
*/
private void drawDelta(Graphics2D dst, Rectangle r)
{
double s = 180 - startAngle;
double d = -angle;
Area shape = new Area(outerEllipse);
shape.subtract(new Area(innerEllipse));
shape.intersect(new Area(new Arc2D.Double(0,0,r.width,r.height,s,d,Arc2D.PIE)));
dst.setColor(new Color(angleColor.getRed(),
angleColor.getGreen(),
angleColor.getBlue(),
100));
dst.fill(shape);
if (showAngleColorAt4Sides) {
d = angle;
shape = new Area(outerEllipse);
shape.subtract(new Area(innerEllipse));
shape.intersect(new Area(new Arc2D.Double(0,0,r.width,r.height,s,d,Arc2D.PIE)));
dst.setColor(new Color(angleColor.getRed(),
angleColor.getGreen(),
angleColor.getBlue(),
30));
dst.fill(shape);
s = -startAngle;
shape = new Area(outerEllipse);
shape.subtract(new Area(innerEllipse));
shape.intersect(new Area(new Arc2D.Double(0,0,r.width,r.height,s,d,Arc2D.PIE)));
dst.setColor(new Color(angleColor.getRed(),
angleColor.getGreen(),
angleColor.getBlue(),
30));
dst.fill(shape);
d = -angle;
shape = new Area(outerEllipse);
shape.subtract(new Area(innerEllipse));
shape.intersect(new Area(new Arc2D.Double(0,0,r.width,r.height,s,d,Arc2D.PIE)));
dst.setColor(new Color(angleColor.getRed(),
angleColor.getGreen(),
angleColor.getBlue(),
30));
dst.fill(shape);
}
}
/**
* Draws the scale.
*/
private void drawScale(Graphics2D dst, Rectangle r)
{
int radius = Math.max(r.width / 2, r.height / 2) + 1;
dst.setColor(Color.black);
dst.setStroke(new BasicStroke(1));
int t = getThickness();
Area clip = new Area(new Ellipse2D.Double(t/2, t/2, r.width-t, r.height-t));
clip.subtract(new Area(innerEllipse));
Shape oldClip = dst.getClip();
dst.setClip(clip);
try {
for (int i = 0; i < 4; i++) {
double a = i * 90 * 2 * Math.PI / 360;
double x0 = r.width / 2;
double y0 = r.height / 2;
double x1 = r.width / 2 - Math.cos(a) * radius;
double y1 = r.height / 2 - Math.sin(a) * radius;
dst.drawLine((int)x0, (int)y0, (int)x1, (int)y1);
}
clip = new Area(new Ellipse2D.Double(3*t/4, 3*t/4, r.width-3*t/2, r.height-3*t/2));
clip.subtract(new Area(innerEllipse));
dst.setClip(clip);
for (int i = 0; i < 36; i++) {
if (i == 0 || i == 9 || i == 18 || i == 27) continue;
double a = i * 10 * 2 * Math.PI / 360;
double x0 = r.width / 2;
double y0 = r.height / 2;
double x1 = r.width / 2 - Math.cos(a) * radius;
double y1 = r.height / 2 - Math.sin(a) * radius;
dst.drawLine((int)x0, (int)y0, (int)x1, (int)y1);
}
} finally {
dst.setClip(oldClip);
}
}
/**
* Draws the outer area of the dial.
*/
private void drawOuterArea(Graphics2D dst, Rectangle r)
{
Area shape = new Area(new Rectangle2D.Double(0, 0, r.width, r.height));
shape.subtract(new Area(new Ellipse2D.Double(0, 0, r.width, r.height)));
Color c = color;
dst.setColor(c);
dst.fill(shape);
for (int i = 0; i < 4; i++) {
int now = r.width - 7 - 2 * i;
int noh = r.height - 7 - 2 * i;
shape = new Area(new Rectangle2D.Double(2+i, 2+i, now, noh));
shape.subtract(new Area(new Ellipse2D.Double(0, 0, r.width, r.height)));
c = new Color(Math.min(255, c.getRed() + 25),
Math.min(255, c.getGreen() + 25),
Math.min(255, c.getBlue() + 25));
dst.setColor(c);
dst.fill(shape);
}
AffineTransform oldt = dst.getTransform();
Rectangle lr = label1.getBounds();
dst.transform(new AffineTransform(1,0,0,1,lr.x,lr.y));
label1.paint(dst);
dst.setTransform(oldt);
lr = label2.getBounds();
dst.transform(new AffineTransform(1,0,0,1,lr.x,lr.y));
label2.paint(dst);
dst.setTransform(oldt);
lr = label3.getBounds();
dst.transform(new AffineTransform(1,0,0,1,lr.x,lr.y));
label3.paint(dst);
dst.setTransform(oldt);
lr = label4.getBounds();
dst.transform(new AffineTransform(1,0,0,1,lr.x,lr.y));
label4.paint(dst);
dst.setTransform(oldt);
}
/**
* Returns true if this contains the input point.
*/
Override
public boolean contains(int x, int y) {
if (drawOuter)
return super.contains(x, y);
return outerEllipse.contains(x, y);
}
// ------------------------------------------------------------------------
// Interaction
private boolean dragging;
private int initialDragAngle;
private int startDragAngle;
/**
* Called when the mouse was clicked.
*/
Override
protected void processMouseEvent(MouseEvent e) {
if (outerEllipse.contains(e.getX(), e.getY()) &&
!innerEllipse.contains(e.getX(), e.getY())) {
int id = e.getID();
switch (id) {
case MouseEvent.MOUSE_PRESSED:
dragging = true;
initialDragAngle = angle;
startDragAngle = getAngle(e.getX(), e.getY());
repaintOnInteraction();
break;
case MouseEvent.MOUSE_RELEASED:
if (dragging) {
int stopDragAngle = getAngle(e.getX(), e.getY());
angle = initialDragAngle + (stopDragAngle - startDragAngle);
fireActionEvent("angle");
repaintOnInteraction();
dragging = false;
}
break;
case MouseEvent.MOUSE_CLICKED:
dragging = false;
if (e.getButton() == MouseEvent.BUTTON1)
angle++;
else
angle--;
fireActionEvent("angle");
repaintOnInteraction();
break;
}
}
super.processMouseEvent(e);
}
/**
* Called when the mouse was moved or dragged.
*/
Override
protected void processMouseMotionEvent(MouseEvent e) {
// don't need to check inner/outerElipse since we do only something when
// dragging is true
int id = e.getID();
switch (id) {
case MouseEvent.MOUSE_DRAGGED:
if (dragging) {
int stopDragAngle = getAngle(e.getX(), e.getY());
angle = initialDragAngle + (stopDragAngle - startDragAngle);
fireActionEvent("angle");
repaintOnInteraction();
}
break;
}
super.processMouseEvent(e);
}
/**
* Called when the mouse wheel was used.
*/
Override
protected void processMouseWheelEvent(MouseWheelEvent e) {
if (outerEllipse.contains(e.getX(), e.getY()) &&
!innerEllipse.contains(e.getX(), e.getY())) {
int id = e.getID();
switch (id) {
case MouseEvent.MOUSE_WHEEL:
int click = e.getWheelRotation();
angle -= click;
fireActionEvent("angle");
repaintOnInteraction();
break;
}
}
}
/**
* Repaints this component when an interaction happened.
*/
void repaintOnInteraction()
{
// normalize the angle
while (angle >= 360) angle -= 360;
while (angle < 0) angle += 360;
// repaint
Component c = getParent();
if (c instanceof JPanel)
c = ((JPanel)c).getParent();
if (c instanceof JDial)
((JDial)c).repaintOnInteraction();
else
repaint();
}
/**
* Returns the angle from a mouse position.
*/
private int getAngle(int x, int y)
{
Rectangle r = getBounds();
double x0 = r.width / 2;
double y0 = r.height / 2;
double dx = (x - x0);
double dy = (y - y0);
double a = Math.atan2(dy, dx);
return (int)(a / (2 * Math.PI) * 360);
}
/**
* Adds an action listener.
* Action listeners are notified whenever the angle of this dial changed.
*/
public void addActionListener(ActionListener l)
{
listenerList.add(ActionListener.class, l);
}
/**
* Removes an action listener.
*/
public void removeActionListener(ActionListener l)
{
listenerList.remove(ActionListener.class, l);
}
/**
* Fires the action event of this control.
*/
protected void fireActionEvent(String actionCommand)
{
// normalize the angle
while (angle >= 360) angle -= 360;
while (angle < 0) angle += 360;
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
ActionEvent e = null;
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length-2; i>=0; i-=2) {
if (listeners[i]==ActionListener.class) {
// Lazily create the event:
if (e == null) {
e = new ActionEvent(this,
ActionEvent.ACTION_PERFORMED,
actionCommand);
}
((ActionListener)listeners[i+1]).actionPerformed(e);
}
}
}
}