/* For License see bottom */
/*
* DKnob.java
* (c) 2000 by Joakim Eriksson
*
* DKnob is a component similar to JSlider but with
* round "user interface", a knob.
*/
package com.dreamfabric;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.Arc2D;
import java.util.ArrayList;
import javax.swing.JComponent;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import jonkoshare.util.VersionInformation;
/**
* Round Slider interface.
* Basic source code derived from Joakim Eriksson. Improved by
* Alexander Methke (font size adjustment, tick adjustment,
* rendering of boolean properties like enabled/disabled or
* resizing)
* <a href="http://www.dreamfabric.com/java/knob/knob.html">
* http://www.dreamfabric.com/java/knob/knob.html
* </a>
*
* @author Joakim Eriksson
* @author Alexander Methke
*/
@VersionInformation (
lastChanged="$LastChangedDate: 2009-07-25 04:59:33 -0500 (Sat, 25 Jul 2009) $",
authors={"Joakim Eriksson", "Alexander Methke"},
revision="$LastChangedRevision: 11 $",
lastEditor="$LastChangedBy: onkobu $",
id="$Id"
)
public class DKnob extends JComponent {
/**
* Default starting angle.
*/
private final static double DEFAULT_START = 225;
/**
* Default length in degree.
*/
private final static double DEFAULT_LENGTH = 270;
/**
* Angle in radians.
*/
private final static double DEFAULT_START_ANGLE = (DEFAULT_START/360)*Math.PI*2;
/**
* Length in radians.
*/
private final static double DEFAULT_LENGTH_ANGLE = (DEFAULT_LENGTH/360)*Math.PI*2;
/**
* Multiplier for radian conversion.
*/
private final static double MULTIP = 180 / Math.PI;
/**
* Focus color.
*/
private final static Color DEFAULT_FOCUS_COLOR = new Color(0x8080ff);
/**
* Drag increment.
*/
private final static double DEFAULT_DRAG_INCREMENT=0.01;
/**
* Click increment.
*/
private final static double DEFAULT_CLICK_INCREMENT=0.01;
/**
* Default minimum value.
*/
private final static int DEFAULT_MIN_VALUE=0;
/**
* Default maximum value.
*/
private final static int DEFAULT_MAX_VALUE=100;
/**
* Default starting value.
*/
private final static int DEFAULT_INT_VALUE=50;
private int SHADOWX = 1;
private int SHADOWY = 1;
private double dragIncrement;
private double clickIncrement;
private int size;
private int middle;
/**
* Describes possible DragTypes.
*/
public enum DragType {
/**
* Simple Drag-Pattern with mainly
* vertical movement.
*/
SIMPLE,
/**
* Round Drag-Pattern where user
* needs to go round in circles.
* This is the default value.
*/
ROUND
}
private DragType dragType = DragType.ROUND;
private final static Dimension DEFAULT_MIN_SIZE = new Dimension(40, 40);
private final static Dimension DEFAULT_PREF_SIZE = new Dimension(80, 80);
// Set the antialiasing to get the right look!
private final static RenderingHints AALIAS =
new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
private ChangeEvent changeEvent = null;
private ArrayList<ChangeListener> listenerList = new ArrayList<ChangeListener>();
private Arc2D hitArc = new Arc2D.Float(Arc2D.PIE);
private double angle = (double) DEFAULT_START_ANGLE;
private double value;
private int dragpos = -1;
private double startVal;
private Color focusColor;
private double lastAng;
public DKnob() {
setMinimumSize(DEFAULT_MIN_SIZE);
setPreferredSize(DEFAULT_PREF_SIZE);
setMinimumValue(DEFAULT_MIN_VALUE);
setMaximumValue(DEFAULT_MAX_VALUE);
setIntValue(DEFAULT_INT_VALUE);
dragIncrement=DEFAULT_DRAG_INCREMENT;
clickIncrement=DEFAULT_CLICK_INCREMENT;
SHADOWX = 1;
SHADOWY = 1;
focusColor = DEFAULT_FOCUS_COLOR;
hitArc.setAngleStart(235); // Degrees ??? Radians???
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent me) {
if (!isEnabled()) {
return;
}
dragpos = me.getX() + me.getY();
startVal = value;
// Fix last angle
int xpos = middle - me.getX();
int ypos = middle - me.getY();
lastAng = Math.atan2(xpos, ypos);
requestFocus();
}
@Override
public void mouseClicked(MouseEvent me) {
if (!isEnabled()) {
return;
}
hitArc.setAngleExtent(-(DEFAULT_LENGTH + 20));
if (hitArc.contains(me.getX(), me.getY())) {
hitArc.setAngleExtent(MULTIP * (angle-DEFAULT_START_ANGLE)-10);
if (hitArc.contains(me.getX(), me.getY())) {
decValue();
} else incValue();
}
}
});
// Let the user control the knob with the mouse
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent me) {
if (!isEnabled()) {
return;
}
if ( dragType == DragType.SIMPLE) {
double f = dragIncrement * (double)
((me.getX() + me.getY()) - dragpos);
setValue(startVal + f);
fireChangeEvent();
} else if ( dragType == DragType.ROUND) {
// Measure relative the middle of the button!
int xpos = middle - me.getX();
int ypos = middle - me.getY();
double angle = Math.atan2(xpos, ypos);
double diff = lastAng - angle;
setValue((double) (getValue() + (diff / DEFAULT_LENGTH_ANGLE)));
lastAng = angle;
fireChangeEvent();
}
}
@Override
public void mouseMoved(MouseEvent me) {
}
});
// Let the user control the knob with the keyboard
addKeyListener(new KeyListener() {
public void keyTyped(KeyEvent e) {}
public void keyReleased(KeyEvent e) {}
public void keyPressed(KeyEvent e) {
if (!isEnabled()) {
return;
}
int k = e.getKeyCode();
if (k == KeyEvent.VK_RIGHT) {
incValue();
fireChangeEvent();
} else if (k == KeyEvent.VK_LEFT) {
decValue();
fireChangeEvent();
}
}
});
// Handle focus so that the knob gets the correct focus highlighting.
addFocusListener(new FocusListener() {
public void focusGained(FocusEvent e) {
if (!isEnabled()) {
return;
}
repaint();
}
public void focusLost(FocusEvent e) {
if (!isEnabled()) {
return;
}
repaint();
}
});
}
/**
* Sets the component's drag type.
*
* @param type
*/
public void setDragType(DragType type) {
dragType = type;
}
/**
* Returns component's drag type.
*
* @return
*/
public DragType getDragType() {
return dragType;
}
/**
* Increments by one tick.
*
* @see #getClickIncrement
*/
public void incValue() {
setValue(value + clickIncrement);
}
/**
* Decrements by one tick.
*
* @see #getClickIncrement
*/
public void decValue() {
setValue(value - clickIncrement);
}
/**
* Returns set value. Values are
* between 0 and 1.
*
* @return
*/
public double getValue() {
return value;
}
/**
* Sets value. Beware, this causes
* a change event. Values must be
* between 0 and 1. Greater values
* will be set to 1, smaller values
* to 0.
*
* @param value
*/
public void setValue(double value) {
if (value < 0) {
value = 0;
}
if (value > 1) {
value = 1;
}
this.value = value;
angle = DEFAULT_START_ANGLE - (double) DEFAULT_LENGTH_ANGLE * value;
repaint();
fireChangeEvent();
}
/**
* Returns value as rounded int.
*
* @return
*/
public int getIntValue() {
return (int)(Math.round(getValue()*maximumValue))+minimumValue;
}
/**
* Sets value as int.
*
* @param val
*/
public void setIntValue(int val) {
if (val<minimumValue ||
val>maximumValue) {
throw new IllegalArgumentException("value must be between "+minimumValue+" and "+maximumValue+" therefore "+val+" is invalid");
}
if (val<=minimumValue) {
setValue(0);
} else if (val>=maximumValue) {
setValue(1.0);
} else {
setValue((double)val/(double)maximumValue);
}
}
/**
* Adds a {@link ChangeListener} to notify it
* of changes to value. Multiple registration
* is not possible.
*
* @param cl
*/
public void addChangeListener(ChangeListener cl) {
if (listenerList.contains(cl)) {
return;
}
listenerList.add(cl);
}
/**
* Removes given Listener.
*
* @param cl
*/
public void removeChangeListener(ChangeListener cl) {
listenerList.remove(cl);
}
/**
* Call this method if you need to notify
* about changes.
*
*/
protected void fireChangeEvent() {
// Process the listeners last to first, notifying
// those that are interested in this event
for (ChangeListener cl:listenerList) {
// Lazily create the event:
if (changeEvent == null) {
changeEvent = new ChangeEvent(this);
}
cl.stateChanged(changeEvent);
}
}
/**
* Determines if markes (ticks 'round
* circle) will be painted.
*
* @param f
*/
public void setPaintMarkers(boolean f) {
paintMarkers=f;
}
/**
* Checks if markers should be painted.
*
* @return
*/
public boolean isPaintMarkers() {
return paintMarkers;
}
// Paint the DKnob
@Override
public void paint(Graphics g) {
int width = getWidth();
int height = getHeight();
size = Math.min(width, height) - 22;
middle = 10 + size/2;
if (g instanceof Graphics2D) {
Graphics2D g2d = (Graphics2D) g;
g2d.setBackground(getParent().getBackground());
g2d.addRenderingHints(AALIAS);
// For the size of the "mouse click" area
hitArc.setFrame(4, 4, size+12, size+12);
}
int msCorr=0;
if (size<20) {
g.setFont(g.getFont().deriveFont(8.0F));
msCorr=2;
} else if (size<30) {
g.setFont(g.getFont().deriveFont(9.0F));
msCorr=1;
}
// Paint the "markers"
if (isPaintMarkers()) {
int ms=(size>>1);
for (double a2 = DEFAULT_START_ANGLE;
a2 >= DEFAULT_START_ANGLE - DEFAULT_LENGTH_ANGLE;
a2=a2 -(double) (DEFAULT_LENGTH_ANGLE/10.01)) {
int x = 10 + ms + (int)((6+ms-msCorr) * Math.cos(a2));
int y = 10 + ms - (int)((6+ms-msCorr) * Math.sin(a2));
g.drawLine(10 + ms, 10 + ms, x, y);
}
}
// Set the position of the Zero
g.drawString("0", 1, size + 10+msCorr);
// Paint focus if in focus
if (hasFocus()) {
g.setColor(focusColor);
} else {
g.setColor(Color.white);
}
g.fillOval(10, 10, size, size);
g.setColor(Color.gray);
g.fillOval(14 + SHADOWX, 14 + SHADOWY, size-8, size-8);
if (isEnabled()) {
g.setColor(getCenterColor());
} else {
g.setColor(Color.gray);
}
g.drawArc(10, 10, size, size, 315, 270);
g.fillOval(14, 14, size-8, size-8);
if (isEnabled()) {
g.setColor(Color.white);
} else {
g.setColor(Color.lightGray);
}
int x = 10 + size/2 + (int)(size/2 * Math.cos(angle));
int y = 10 + size/2 - (int)(size/2 * Math.sin(angle));
g.drawLine(10 + size/2, 10 + size/2, x, y);
g.setColor(Color.gray);
int s2 = (int) Math.max(size / 6, 6);
g.drawOval(10 + s2, 10 + s2, size - s2*2, size - s2*2);
int dx = (int)(2 * Math.sin(angle));
int dy = (int)(2 * Math.cos(angle));
g.drawLine(10 + dx + size/2, 10 + dy + size/2, x, y);
g.drawLine(10-dx + size/2, 10-dy + size/2, x, y);
}
/**
* Sets Minimum value. This is the value
* in leftmost position.
*
* @param val
*/
public void setMinimumValue(int val) {
minimumValue=val;
}
/**
* Sets Maximum value. This is the value
* in rightmost position.
*
* @param val
*/
public void setMaximumValue(int val) {
maximumValue=val;
}
/**
* Returns minimum value.
*
* @see #setMinimumValue
* @return
*/
public int getMinimumValue() {
return minimumValue;
}
/**
* Returns maximum value.
*
* @see #setMaximumValue
* @return
*/
public int getMaximumValue() {
return maximumValue;
}
/**
* Getter for property centerColor.
* @return Value of property centerColor.
*/
public Color getCenterColor() {
return this.centerColor==null?getForeground():this.centerColor;
}
/**
* Setter for property centerColor.
* @param centerColor New value of property centerColor.
*/
public void setCenterColor(Color centerColor) {
this.centerColor = centerColor;
}
private int minimumValue;
private int maximumValue;
private boolean paintMarkers;
/**
* Holds value of property centerColor.
*/
private Color centerColor;
}
/*
Copyright (C) 2008 Alexander Methke
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program (gplv3.txt).
*/