/*
* This code is part of Project Waterloo from King's College London
*
*
* Copyright King's College London 2011-
*
* @author Malcolm Lidierth, King's College London [Contact]
*
* Project Waterloo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Project Waterloo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*
*/
package kcl.waterloo.widget;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Ellipse2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.NumberFormat;
import javax.swing.DefaultBoundedRangeModel;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JSlider;
import javax.swing.SpringLayout;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.SliderUI;
/**
*
* GJDial is a subclass of the standard swing
* JSlider.
*
*
* GJDial provides a rotating dial instead of a slider to change
* the settings of the widget. By default the dial covers the range between the
* minimum and maximum in a single turn - equivalent to the sweep length of the
* JSlider. However, the number of turns available can be set programmatically
* using the setAvailableTurns method, e.g. with
* setAvailableTurns(10) the each rotation would cover 1/10th of
* the range of the GJDial.
*
*
* Dials are circular and will be drawn at the center of the GJDial's bounds
* with a diameter determined from the lesser of the width and the height - so
* it makes sense generally to specify square sizes.
*
*
* The following methods are specific to GJDial or have been overridden.
*
*
* getTurnCount - returns the number of turns made
*
* getValue - returns the value calculated from the current dial
* settings.
*
*
* The following also have suitable getters.
*
* setAvailableTurns(int) - sets the number of turns for the
* dial
*
* setDisplayValue(true/false) provides a text display of the value
* in the center of the dial
* setLineColor(Color/Paint) - sets the line color
*
* setFill(Color/Paint) - sets the fill color for the dial
*
* setMajorTickSpacing(int n) - displays n major ticks around the
* dial
*
* setMinorTickSpacing(int n) - displays n minor ticks around the
* dial
*
* setText(String) provides a JLabel with the specified string in
* the center of the dial
*
* setValue(int) - positions the dial (and internal settings as
* appropriate)
*
*
* Other JSlider methods, e.g. setMinimum and
* setMaximum act as you would expect or are ignored e.g.
* setExtent so a GJDial should work as a drop-in replacement for a
* JSlider.
*
* @author Malcolm Lidierth, King's College London [Contact]
*/
public class GJDial extends JSlider implements MouseListener, MouseMotionListener, PropertyChangeListener {
private static final String uiClassID = "BasicGJDialUI";
private Paint BackgroundFill = null;
private Paint Fill = java.awt.Color.white;
private Color LineColor = java.awt.Color.gray;
private JLabel label = new JLabel();
private boolean displayValue = false;
private int mouseX;
private int mouseY;
private double Theta = 0;
private double ThetaAtDragStart;
private double MouseThetaAtDragStart;
private static final float DialLineWidth = 2.5f;
private static final BasicStroke pen1 = new BasicStroke(DialLineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL);
private static final BasicStroke pen2 = new BasicStroke(DialLineWidth / 2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL);
private static NumberFormat nf = NumberFormat.getInstance();
private GJRoundedBoundedDataModel model = new GJRoundedBoundedDataModel();
private ActionListener actionListener = null;
/**
* Default constructor returns a 100x100 pixel single turn GJDial with no
* display of the value and no JLabel text
*/
public GJDial() {
this(100, 1, false);
}
/**
* Constructor returns a GjDial with no display of the value and no JLabel
* text
*
* @param diameter diameter of the GJDial in pixels
* @param n number of available turns
*/
public GJDial(int diameter, int n) {
this(diameter, n, false);
}
/**
* Constructor
*
* @param diameter diameter of the GJDial in pixels
* @param n number of available turns
* @param flag true to display the values as text
*/
public GJDial(int diameter, int n, boolean flag) {
setup();
model.setAvailableTurns(n);
displayValue = flag;
setBorder(new EmptyBorder(new Insets(10, 10, 10, 10)));
super.setModel(model);
super.setOpaque(false);
setPreferredSize(new Dimension(diameter, diameter));
updateUI();
}
private void setup() {
SpringLayout layout = new SpringLayout();
setLayout(layout);
layout.putConstraint(SpringLayout.HORIZONTAL_CENTER, label, 0, SpringLayout.HORIZONTAL_CENTER, this);
layout.putConstraint(SpringLayout.VERTICAL_CENTER, label, 0, SpringLayout.VERTICAL_CENTER, this);
label.setHorizontalAlignment(JLabel.CENTER);
add(label);
revalidate();
setMajorTickSpacing(20);
addMouseListener(this);
addMouseMotionListener(this);
setValue(getMinimum());
}
public void addActionListener(ActionListener listener) {
actionListener = listener;
}
/**
* {@inheritDoc}
*
* @param val
*/
@Override
public void setMinimum(int val) {
super.setMinimum(val);
repaint();
}
/**
* {@inheritDoc}
*
* @param val
*/
@Override
public void setMaximum(int val) {
super.setMaximum(val);
repaint();
}
/**
* {@inheritDoc}
*
* @return the current value of the dial.
*/
@Override
public int getValue() {
return getValue(getTheta());
}
private int getValue(double theta) {
double perTurn = (getMaximum() - getMinimum()) / model.getAvailableTurns();
double val = getMinimum() + (model.getTurnCount() * perTurn) + getFraction(theta);
return (int) val;
}
/**
* {@inheritDoc}
*
* @param val
*/
@Override
public void setValue(int val) {
int old = getValue();
double perTurn = (getMaximum() - getMinimum()) / model.getAvailableTurns();
val = val - getMinimum();
model.setTurnCount((int) Math.max(0, Math.floor(val / perTurn)));
setTheta((2 * Math.PI) - (val % perTurn) / perTurn * Math.PI * 2);
firePropertyChange("value", old, getValue());
}
private double getFraction(double theta) {
double perTurn = (getMaximum() - getMinimum()) / model.getAvailableTurns();
return perTurn - (perTurn * theta / Math.PI / 2);
}
/**
* {@inheritDoc}
*
* @return
*/
@Override
public BasicGJDialUI getUI() {
return (BasicGJDialUI) super.getUI();
}
/**
* {@inheritDoc}
*/
@Override
public final void updateUI() {
setUI(BasicGJDialUI.createUI(this));
invalidate();
}
/**
* {@inheritDoc}
*
* @return
*/
@Override
public String getUIClassID() {
return uiClassID;
}
/**
* Sets the fill for the dial
*
* @return the Paint object
*/
public Paint getFill() {
return Fill;
}
/**
* Returns the fill for the dial
*
* @param p the Paint object e.g. a Color
*/
public void setFill(Paint p) {
Fill = p;
}
public double getTheta() {
return Theta;
}
private void setTheta(double theta) {
Theta = theta;
fireStateChanged();
if (actionListener != null) {
actionListener.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "theta"));
}
repaint();
}
// public double getThetaOffset() {
// return ThetaOffset;
// }
//
// public void setThetaOffset(double theta) {
// ThetaOffset = theta;
// }
private double getThetaAtDragStart() {
return ThetaAtDragStart;
}
private void setThetaAtDragStart(double theta) {
ThetaAtDragStart = theta;
}
private double getMouseThetaAtDragStart() {
return MouseThetaAtDragStart;
}
private void setMouseThetaAtDragStart(double theta) {
MouseThetaAtDragStart = theta;
}
/**
* Returns the number of turns made - by mouse or programmatically via
* setValue
*
* @return
*/
public int getTurnCount() {
return model.getTurnCount();
}
private void setTurnCount(int val) {
model.setTurnCount(val);
}
/**
* Sets the text for a JLabel displayed at the center of the dial. Note that
* this will overlap displayed values if isDisplayValuetrue,/code>.
*
* @param str
*/
public void setText(String str) {
label.setText(str);
}
/**
* Returns the JLabel's text
*
* @return
*/
public String getText() {
return label.getText();
}
/**
* Displays the current value at the center of the dial
*
* @param flag
*/
public void setDisplayValue(boolean flag) {
displayValue = flag;
}
/**
* Determines if values will be displayed at the center of the dial
*
* @return
*/
public boolean isDisplayValue() {
return displayValue;
}
private void paintBackground(Graphics2D g2d) {
if (isOpaque()) {
if (BackgroundFill == null) {
this.getUI().update(g2d, this);
} else {
g2d.setPaint(BackgroundFill);
g2d.fill(new java.awt.Rectangle(0, 0, getWidth(), getHeight()));
}
}
}
private void paintTicks(Graphics2D g2d) {
if (getPaintTicks()) {
Graphics2D g2d2 = (Graphics2D) g2d.create();
float radius = Math.min(getWidth(), getHeight()) / 2;
radius = radius - 0.1f * radius;
int xoffset = getWidth() / 2;
int yoffset = getHeight() / 2;
g2d2.setColor(getLineColor());
g2d2.setStroke(pen1);
for (int k = 0; k <= getMajorTickSpacing(); k++) {
g2d2.drawLine(xoffset, yoffset, (int) (xoffset + radius + Math.max(5, radius / 15)), yoffset);
g2d2.rotate(2 * Math.PI / getMajorTickSpacing(), xoffset, yoffset);
}
g2d2.setStroke(pen2);
for (int k = 0; k <= getMinorTickSpacing(); k++) {
g2d2.drawLine(xoffset, yoffset, (int) (xoffset + radius + Math.max(2.5, radius / 20)), yoffset);
g2d2.rotate(2 * Math.PI / getMinorTickSpacing(), xoffset, yoffset);
}
g2d2.dispose();
}
}
/**
* {@inheritDoc}
*
* @param g
*/
@Override
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
// Background
paintBackground(g2d);
paintTicks(g2d);
//Dial
g2d.setColor(getLineColor());
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setStroke(pen1);
// Set up
float diam = Math.min(getWidth(), getHeight());
diam = diam - (0.1f * diam);
float xoffset = (getWidth() - diam) / 2;
float yoffset = (getHeight() - diam) / 2;
Ellipse2D.Float circle = new Ellipse2D.Float(xoffset, yoffset, diam, diam);
g2d.setColor(getLineColor());
g2d.draw(circle);
g2d.setPaint(Fill);
g2d.fill(circle);
g2d.setColor(getLineColor());
if (displayValue) {
FontMetrics metrics = g2d.getFontMetrics();
String str = nf.format(getValue());
g2d.drawString(str,
getWidth() / 2 - metrics.stringWidth(str) / 2,
getHeight() / 2 + metrics.getHeight() / 2);
}
//Notch
Ellipse2D.Float circle2 = new Ellipse2D.Float(xoffset + diam - diam / 5f, yoffset + diam / 2 - diam / 15, diam / 7.5f, diam / 7.5f);
g2d.rotate(-Theta, xoffset + diam / 2, yoffset + diam / 2);
g2d.draw(circle2);
g2d.dispose();
}
@Override
public void mouseClicked(MouseEvent me) {
mouseX = me.getX();
mouseY = me.getY();
setThetaAtDragStart(getTheta());
setMouseThetaAtDragStart(getTheta());
mouseDragged(me);
}
@Override
public void mousePressed(MouseEvent me) {
mouseX = me.getX();
mouseY = me.getY();
double theta = mouseTheta(mouseX, mouseY);
setMouseThetaAtDragStart(theta);
setThetaAtDragStart(getTheta());
}
@Override
public void mouseReleased(MouseEvent me) {
}
@Override
public void mouseEntered(MouseEvent me) {
}
@Override
public void mouseExited(MouseEvent me) {
}
@Override
public void mouseDragged(MouseEvent me) {
double oldTheta = getTheta();
mouseX = me.getX();
mouseY = me.getY();
double thisTheta = mouseTheta(mouseX, mouseY);
if (oldTheta == thisTheta) {
return;
}
// This prevents theta jumping to the initial mouse position at drag
// initiation via the mousePressed callback.
// The size of the arc for this drag
double thisArc = getMouseThetaAtDragStart() - thisTheta;
// The new dial position
double newTheta = arclength(getThetaAtDragStart(), thisArc);
// calculate normallyClockWise - this will be true for clockwise rotations
// over most of the unit circle BUT the logic will invert when crossing 0
// and 2PI
double sz = oldTheta - newTheta;
boolean normallyClockWise = (sz > 0);
int zeroCrossing = 0;
if (normallyClockWise
&& oldTheta > 2 * Math.PI - Math.PI / 2
&& newTheta < Math.PI / 2) {
// Therefore anti-clockwise crossing
zeroCrossing = -1;
} else if (!normallyClockWise
&& oldTheta < Math.PI / 2
&& newTheta > 2 * Math.PI - Math.PI / 2) {
// Therefore clockwise crossing
zeroCrossing = 1;
}
if (zeroCrossing == -1) {
if (getTurnCount() > 0) {
setTurnCount(getTurnCount() - 1);
} else {
return;
}
} else if (zeroCrossing == 1) {
if (getTurnCount() + 1 < model.getAvailableTurns()) {
setTurnCount(getTurnCount() + 1);
} else {
return;
}
}
// Tidy up any rounding errors that could affect subsequent sums
// around the crossing point
if (newTheta < 0) {
newTheta = 0;
}
if (newTheta > 2 * Math.PI) {
newTheta = 2 * Math.PI;
}
setTheta(newTheta);
}
@Override
public void mouseMoved(MouseEvent me) {
}
private static double arclength(double theta1, double theta2) {
double val = theta1 - theta2;
if (val > 2 * Math.PI || val < 0) {
val = val % (2 * Math.PI);
}
return val;
}
private double mouseTheta(double x, double y) {
x = x - getWidth() / 2;
y = getHeight() - y - getHeight() / 2;
return getTheta2PI(x, y);
}
private static double getTheta2PI(double x, double y) {
double sintheta = y / Math.hypot(x, y);
double theta = Math.asin(sintheta);
if (x <= 0 && y >= 0) {
theta = Math.PI - theta;
} else if (x <= 0 && y <= 0) {
theta = Math.PI - theta;
} else if (x >= 0 && y <= 0) {
theta = 2 * Math.PI + theta;
}
return theta;
}
/**
* @return the LineColor
*/
public Color getLineColor() {
return LineColor;
}
/**
* @param LineColor the LineColor to set
*/
public void setLineColor(Color LineColor) {
this.LineColor = LineColor;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
}
private class GJRoundedBoundedDataModel extends DefaultBoundedRangeModel {
private int availableTurns = 1;
private int turnCount = 0;
public GJRoundedBoundedDataModel() {
}
public int getAvailableTurns() {
return availableTurns;
}
public void setAvailableTurns(int t) {
availableTurns = t;
}
public int getTurnCount() {
return turnCount;
}
public void setTurnCount(int val) {
turnCount = val;
}
@Override
public String toString() {
String str = super.toString();
str = str.substring(0, str.length() - 2);
str = str + String.format(", turns=%d]\n", availableTurns);
return str;
}
}
public static class BasicGJDialUI extends SliderUI {
public static ComponentUI createUI(JComponent c) {
return new BasicGJDialUI();
}
}
}
Ads help maintain this website.