diff options
Diffstat (limited to 'src/main/java/com/jsyn/swing/RotaryController.java')
-rw-r--r-- | src/main/java/com/jsyn/swing/RotaryController.java | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/src/main/java/com/jsyn/swing/RotaryController.java b/src/main/java/com/jsyn/swing/RotaryController.java new file mode 100644 index 0000000..c26c37f --- /dev/null +++ b/src/main/java/com/jsyn/swing/RotaryController.java @@ -0,0 +1,335 @@ +/* + * Copyright 2010 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; + +import javax.swing.BoundedRangeModel; +import javax.swing.JPanel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * Rotary controller looks like a knob on a synthesizer. You control this knob by clicking on it and + * dragging <b>up</b> or <b>down</b>. If you move the mouse to the <b>left</b> of the knob then you + * will have <b>coarse</b> control. If you move the mouse to the <b>right</b> of the knob then you + * will have <b>fine</b> control. + * <P> + * + * @author (C) 2010 Phil Burk, Mobileer Inc + * @version 16.1 + */ +public class RotaryController extends JPanel { + private static final long serialVersionUID = 6681532871556659546L; + private static final double SENSITIVITY = 0.01; + private final BoundedRangeModel model; + + private final double minAngle = 1.4 * Math.PI; + private final double maxAngle = -0.4 * Math.PI; + private final double unitIncrement = 0.01; + private int lastY; + private int startX; + private Color knobColor = Color.LIGHT_GRAY; + private Color lineColor = Color.RED; + private double baseValue; + + public enum Style { + LINE, LINEDOT, ARROW, ARC + }; + + private Style style = Style.ARC; + + public RotaryController(BoundedRangeModel model) { + this.model = model; + setMinimumSize(new Dimension(50, 50)); + setPreferredSize(new Dimension(50, 50)); + addMouseListener(new MouseHandler()); + addMouseMotionListener(new MouseMotionHandler()); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + safeRepaint(); + } + + }); + } + + // This can be overridden in subclasses to workaround OpenJDK bugs. + public void safeRepaint() { + repaint(); + } + + public BoundedRangeModel getModel() { + return model; + } + + private class MouseHandler extends MouseAdapter { + + @Override + public void mousePressed(MouseEvent e) { + lastY = e.getY(); + startX = e.getX(); + } + + @Override + public void mouseReleased(MouseEvent e) { + if (isEnabled()) { + setKnobByXY(e.getX(), e.getY()); + } + } + } + + private class MouseMotionHandler extends MouseMotionAdapter { + @Override + public void mouseDragged(MouseEvent e) { + if (isEnabled()) { + setKnobByXY(e.getX(), e.getY()); + } + } + } + + private int getModelRange() { + return (((model.getMaximum() - model.getExtent()) - model.getMinimum())); + } + + /** + * A fractional value is useful for drawing. + * + * @return model value as a normalized fraction between 0.0 and 1.0 + */ + public double getFractionFromModel() { + double value = model.getValue(); + return convertValueToFraction(value); + } + + private double convertValueToFraction(double value) { + return (value - model.getMinimum()) / getModelRange(); + } + + private void setKnobByXY(int x, int y) { + // Scale increment by X position. + int xdiff = startX - x; // More to left causes bigger increments. + double power = xdiff * SENSITIVITY; + double perPixel = unitIncrement * Math.pow(2.0, power); + + int ydiff = lastY - y; + double fractionalDelta = ydiff * perPixel; + // Only update the model if we actually change values. + // This is needed in case the range is small. + int valueDelta = (int) Math.round(fractionalDelta * getModelRange()); + if (valueDelta != 0) { + model.setValue(model.getValue() + valueDelta); + lastY = y; + } + } + + private double fractionToAngle(double fraction) { + return (fraction * (maxAngle - minAngle)) + minAngle; + } + + private void drawLineIndicator(Graphics g, int x, int y, int radius, double angle, + boolean drawDot) { + double arrowSize = radius * 0.95; + int arrowX = (int) (arrowSize * Math.sin(angle)); + int arrowY = (int) (arrowSize * Math.cos(angle)); + g.setColor(lineColor); + g.drawLine(x, y, x + arrowX, y - arrowY); + if (drawDot) { + // draw little dot at end + double dotScale = 0.1; + int dotRadius = (int) (dotScale * arrowSize); + if (dotRadius > 1) { + int dotX = x + (int) ((0.99 - dotScale) * arrowX) - dotRadius; + int dotY = y - (int) ((0.99 - dotScale) * arrowY) - dotRadius; + g.fillOval(dotX, dotY, dotRadius * 2, dotRadius * 2); + } + } + } + + private void drawArrowIndicator(Graphics g, int x0, int y0, int radius, double angle) { + int arrowSize = (int) (radius * 0.95); + int arrowWidth = (int) (radius * 0.2); + int xp[] = { + 0, arrowWidth, 0, -arrowWidth + }; + int yp[] = { + arrowSize, -arrowSize / 2, 0, -arrowSize / 2 + }; + double sa = Math.sin(angle); + double ca = Math.cos(angle); + for (int i = 0; i < xp.length; i++) { + int x = xp[i]; + int y = yp[i]; + xp[i] = x0 - (int) ((x * ca) - (y * sa)); + yp[i] = y0 - (int) ((x * sa) + (y * ca)); + } + g.fillPolygon(xp, yp, xp.length); + } + + private void drawArcIndicator(Graphics g, int x, int y, int radius, double angle) { + final double DEGREES_PER_RADIAN = 180.0 / Math.PI; + final int minAngleDegrees = (int) (minAngle * DEGREES_PER_RADIAN); + final int maxAngleDegrees = (int) (maxAngle * DEGREES_PER_RADIAN); + + int zeroAngleDegrees = (int) (fractionToAngle(baseValue) * DEGREES_PER_RADIAN); + + double arrowSize = radius * 0.95; + int arcX = x - radius; + int arcY = y - radius; + int arcAngle = (int) (angle * DEGREES_PER_RADIAN); + int arrowX = (int) (arrowSize * Math.cos(angle)); + int arrowY = (int) (arrowSize * Math.sin(angle)); + + g.setColor(knobColor.darker().darker()); + g.fillArc(arcX, arcY, 2 * radius, 2 * radius, minAngleDegrees, maxAngleDegrees + - minAngleDegrees); + g.setColor(Color.ORANGE); + g.fillArc(arcX, arcY, 2 * radius, 2 * radius, zeroAngleDegrees, arcAngle - zeroAngleDegrees); + + // fill in middle + int arcWidth = radius / 4; + int diameter = ((radius - arcWidth) * 2); + g.setColor(knobColor); + g.fillOval(arcWidth + x - radius, arcWidth + y - radius, diameter, diameter); + + g.setColor(lineColor); + g.drawLine(x, y, x + arrowX, y - arrowY); + + } + + /** + * Override this method if you want to draw your own line or dot on the knob. + */ + public void drawIndicator(Graphics g, int x, int y, int radius, double angle) { + g.setColor(isEnabled() ? lineColor : lineColor.darker()); + switch (style) { + case LINE: + drawLineIndicator(g, x, y, radius, angle, false); + break; + case LINEDOT: + drawLineIndicator(g, x, y, radius, angle, true); + break; + case ARROW: + drawArrowIndicator(g, x, y, radius, angle); + break; + case ARC: + drawArcIndicator(g, x, y, radius, angle); + break; + } + } + + /** + * Override this method if you want to draw your own knob. + * + * @param g graphics context + * @param x position of center of knob + * @param y position of center of knob + * @param radius of knob in pixels + * @param angle in radians. Zero is straight up. + */ + public void drawKnob(Graphics g, int x, int y, int radius, double angle) { + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + int diameter = radius * 2; + // Draw shaded side. + g.setColor(knobColor.darker()); + g.fillOval(x - radius + 2, y - radius + 2, diameter, diameter); + g.setColor(knobColor); + g.fillOval(x - radius, y - radius, diameter, diameter); + + // Draw line or other indicator of knob position. + drawIndicator(g, x, y, radius, angle); + } + + // Draw the round knob based on the current size and model value. + // This used to have a bug where the scope would draw in this components background. + // Then I changed it from overriding paint() to overriding paintComponent() and it worked. + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + + int width = getWidth(); + int height = getHeight(); + int x = width / 2; + int y = height / 2; + + // Calculate radius from size of component. + int diameter = (width < height) ? width : height; + diameter -= 4; + int radius = diameter / 2; + + double angle = fractionToAngle(getFractionFromModel()); + drawKnob(g, x, y, radius, angle); + } + + public Color getKnobColor() { + return knobColor; + } + + /** + * @param knobColor color of body of knob + */ + public void setKnobColor(Color knobColor) { + this.knobColor = knobColor; + } + + public Color getLineColor() { + return lineColor; + } + + /** + * @param lineColor color of indicator on knob like a line or arrow + */ + public void setLineColor(Color lineColor) { + this.lineColor = lineColor; + } + + public void setStyle(Style style) { + this.style = style; + } + + public Style getStyle() { + return style; + } + + public double getBaseValue() { + return baseValue; + } + + /* + * Specify where the orange arc originates. For example a pan knob with a centered arc would + * have a baseValue of 0.5. + * @param baseValue a fraction between 0.0 and 1.0. + */ + public void setBaseValue(double baseValue) { + if (baseValue < 0.0) { + baseValue = 0.0; + } else if (baseValue > 1.0) { + baseValue = 1.0; + } + this.baseValue = baseValue; + } + +} |