/* * Licensed Materials - Property of Perforce Software, Inc. * © Copyright Perforce Software, Inc. 2014, 2021 * © 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); } } } }