import java.awt.*; import java.awt.event.*; import java.beans.*; import utilities.*; import utilities.gfx.*; public class NumericDial extends Dial implements MouseListener, MouseMotionListener { protected double value; // the value of the NumericDial protected double min, max; // the possible range of values protected double valueRange; // max - min protected double minAngle, maxAngle, angleRange; // dial marks protected double valueToAngleFactor; protected String dialName; protected boolean clockwise; // how do values grow ? protected double pushIncrement; // interactive inc/dec amount protected int lastX, lastY; // mouse drag variables protected Circle circle; protected PropertyChangeSupport butler; //------------------------------------------------------------------- // NumericDial Constructor //------------------------------------------------------------------- public NumericDial() { this("", 100); } public NumericDial(String dialName, int size) { this(dialName, size, true); } public NumericDial(String dialName, int size, boolean clockwise) { this(dialName, size, 0.0, 100.0, clockwise); } public NumericDial(String dialName, int size, double min, double max) { this(dialName, size, min, max, -45.0, 180.0 +45.0, true); } public NumericDial(String dialName, int size, double min, double max, boolean clockwise) { this(dialName, size, min, max, -45.0, 180.0 +45.0, clockwise); } //------------------------------------------------------------------- public NumericDial(String dialName, int size, double min, double max, double minAngle, double maxAngle, boolean clockwise) { super(size); // Dial constructor if ( min >= max ) { throw new IllegalArgumentException( "Illegal NumericDial value range (min="+ min +" max="+ max +")"); } circle = new Circle(size/2,size/2,size/2); circle.useDegrees(true); this.value = min; this.min = min; this.max = max; this.minAngle = minAngle; this.maxAngle = maxAngle; this.clockwise = clockwise; this.dialName = dialName; valueRange = max - min; angleRange = maxAngle - minAngle; valueToAngleFactor = Math.abs(angleRange/valueRange); mouseReleased(null); // lastX = lastY = Integer.MIN_VALUE pushIncrement = valueRange/dialWidth; headingVisible = true; mapValueToHeading(); addMouseListener(this); addMouseMotionListener(this); setFont(new Font("Courier", Font.PLAIN, 10)); butler = new PropertyChangeSupport(this); } //------------------------------------------------------------------- // set()/get() for 'value' property //------------------------------------------------------------------- public void setValue(double value) { if ( this.value != value ) { this.value = value; mapValueToHeading(); reflectChanges(); butler.firePropertyChange(dialName, null, new Double(value)); } } public double getValue() { return value; } //------------------------------------------------------------------- // set()/get() for 'clockwise' property //------------------------------------------------------------------- public void setclockwise(boolean value) { if ( clockwise != value) { this.clockwise = value; mapValueToHeading(); reflectChanges(); } } public boolean getclockwise() { return clockwise; } //------------------------------------------------------------------- // Convert current value to heading angle //------------------------------------------------------------------- protected void mapValueToHeading() { double heading; heading = value - min; // abs -> relative heading *= valueToAngleFactor; // scale if ( clockwise ) { heading = maxAngle - heading; } else { heading = minAngle + heading; } this.heading = heading; // set heading } //------------------------------------------------------------------- // MouseListener interface methods //------------------------------------------------------------------- public void mousePressed(MouseEvent mouseEvent) { lastX = mouseEvent.getX(); lastY = mouseEvent.getY(); } public void mouseReleased(MouseEvent mouseEvent) { lastX = lastY = Integer.MIN_VALUE; } public void mouseClicked(MouseEvent mouseEvent) {} public void mouseEntered(MouseEvent mouseEvent) {} public void mouseExited(MouseEvent mouseEvent) {} //------------------------------------------------------------------- // MouseMotionListener interface methods //------------------------------------------------------------------- public void mouseDragged(MouseEvent mouseEvent) { int x,y, dx, dy, distance; double newValue; x = mouseEvent.getX(); y = mouseEvent.getY(); DebugSupport._assert(lastX != Integer.MIN_VALUE, "Impossible: lastX not valid"); dx = x - lastX; // dragging to the right is + dy = lastY - y; // dragging up is also + // pick largest distance moved, preserving sign if ( Math.abs(dx) > Math.abs(dy) ) { distance = dx; } else { distance = dy; } // adjust value accordingly newValue = value + distance*pushIncrement; // but make sure we do not exceed min or max if ( distance > 0 ) { newValue = Math.min(max, newValue); // clamp down } else { newValue = Math.max(min, newValue); // clamp up } setValue(newValue); // change value, broadcast to listeners lastX = x; lastY = y; } //------------------------------------------------------------------- public void mouseMoved(MouseEvent mouseEvent) {} //------------------------------------------------------------------- // Render value that dial is indicating in middle of dial // Render scale. //------------------------------------------------------------------- protected void renderCustomFeatures(Graphics g) { FontMetrics fm; fm = getFontMetrics(getFont()); renderMinMax (g); renderValue (g, fm); renderDialName(g, fm); } //------------------------------------------------------------------- // Display the value that this NumericDial represents in middle of dial. //------------------------------------------------------------------- protected void renderValue(Graphics g, FontMetrics fm) { String valueStr; valueStr = fp2CleanString(value, 7); GfxKit.drawCenteredString(g, valueStr, getSize().width, getSize().height - extraHeight(), fm); } //------------------------------------------------------------------- // Convert a double value to a "clean" String representation. // Numbers like // 23.0000000002 are converted to 23.0 // 56.7999999998 are converted to 56.9 //------------------------------------------------------------------- public static String fp2CleanString(double value, int maxStrlen) { String numStr; int infSeqIndex; // infinite sequence start numStr = Double.toString(value); if ( numStr.length() <= maxStrlen ) { return numStr; } infSeqIndex = -1; do { // do-while simply for block context if ( (infSeqIndex = numStr.indexOf("0000000")) != -1) break; if ( (infSeqIndex = numStr.indexOf("1111111")) != -1) break; if ( (infSeqIndex = numStr.indexOf("2222222")) != -1) break; if ( (infSeqIndex = numStr.indexOf("3333333")) != -1) break; if ( (infSeqIndex = numStr.indexOf("4444444")) != -1) break; if ( (infSeqIndex = numStr.indexOf("5555555")) != -1) break; if ( (infSeqIndex = numStr.indexOf("6666666")) != -1) break; if ( (infSeqIndex = numStr.indexOf("7777777")) != -1) break; if ( (infSeqIndex = numStr.indexOf("8888888")) != -1) break; if ( (infSeqIndex = numStr.indexOf("9999999")) != -1) break; } while (false); // end of do-while if ( infSeqIndex != -1) { numStr = numStr.substring(0, infSeqIndex+1); } else { numStr = numStr.substring(0, maxStrlen); } return numStr; } //------------------------------------------------------------------- // Display the Scale markers to show min/max range //------------------------------------------------------------------- protected void renderMinMax(Graphics g) { circle.setRadialSize(0.5, 0.9); circle.drawRadial(g, minAngle); circle.drawRadial(g, maxAngle); } //------------------------------------------------------------------- // Display the Dial Name below the dial itself. //------------------------------------------------------------------- protected void renderDialName(Graphics g, FontMetrics fm) { Dimension nameArea; int nameAreaOffset; nameArea = getSize(); nameAreaOffset = nameArea.height - extraHeight(); // translate down to name label area g.translate(0, nameAreaOffset); nameArea.height = extraHeight(); GfxKit.drawCenteredString(g, dialName, nameArea, fm); // translate back g.translate(0, -nameAreaOffset); } protected final int extraHeight() { return 16; } //------------------------------------------------------------------- // PropertyChangeListener management methods //------------------------------------------------------------------- public void addPropertyChangeListener(PropertyChangeListener listener) { butler.addPropertyChangeListener(listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { butler.removePropertyChangeListener(listener); } //------------------------------------------------------------------- // Overridden Object.toString() //------------------------------------------------------------------- public String toString() { StringBuffer sb = new StringBuffer(); sb.append("NumericDial "+ dialName + " (val = " + value + ", "); sb.append("min/max = (" + min +"," + max + "), "); sb.append((clockwise ? "Clockwise" : "Anti-Clockwise") + " incrementing)"); return sb.toString(); } //------------------------------------------------------------------- // Self-Test code //------------------------------------------------------------------- public static void main (String[] args) { Frame window = new Frame("NumericDial Test"); NumericDial aNumericDial = new NumericDial("Temperature", 100, -50, 50); NumericDial bNumericDial = new NumericDial("Depth" , 60, -400, -200, false); NumericDial cNumericDial = new NumericDial("Bottles/s" , 160, 10, 15); window.setLayout(new FlowLayout()); window.add(aNumericDial); window.add(bNumericDial); window.add(cNumericDial); window.pack(); window.setVisible(true); } } // End of Class NumericDial