/* * Get Organized - Organize your schedule, course assignments, and grades * Copyright © 2012 Alex Laird * getorganized@alexlaird.com * alexlaird.com * * 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. If not, see <http://www.gnu.org/licenses/>. */ /* * @(#)ColorPicker.java 1.0 2008-03-01 * * Copyright (c) 2008 Jeremy Wood * E-mail: mickleness@gmail.com * All rights reserved. * * The copyright of this software is owned by Jeremy Wood. * You may not use, copy or modify this software, except in * accordance with the license agreement you entered into with * Jeremy Wood. For details see accompanying license terms. */ package adl.go.gui.colorpicker; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.geom.Ellipse2D; import java.awt.image.BufferedImage; import java.util.ArrayList; import javax.swing.JPanel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.MouseInputAdapter; import javax.swing.event.MouseInputListener; /** * This is the large graphic element in the * <code>ColorPicker</code> that depicts a wide range of colors. <P>This panel * can operate in 6 different modes. In each mode a different property is held * constant: hue, saturation, brightness, red, green, or blue. (Each property is * identified with a constant in the * <code>ColorPicker</code> class, such as: * <code>ColorPicker.HUE</code> or * <code>ColorPicker.GREEN</code>.) <P>In saturation and brightness mode, a * wheel is used. Although it doesn't use as many pixels as a square does: it is * a very aesthetic model since the hue can wrap around in a complete circle. * (Also, on top of looks, this is how most people learn to think the color * spectrum, so it has that advantage, too). In all other modes a square is * used. <P>The user can click in this panel to select a new color. The selected * color is highlighted with a circle drawn around it. Also once this component * has the keyboard focus, the user can use the arrow keys to traverse the * available colors. <P>Note this component is public and exists independently * of the * <code>ColorPicker</code> class. The only way this class is dependent on the * <code>ColorPicker</code> class is when the constants for the modes are used. * <P>The graphic in this panel will be based on either the width or the height * of this component: depending on which is smaller. * * @version 1.0 * @author Jeremy Wood */ public final class ColorPickerPanel extends JPanel { private static final long serialVersionUID = 1L; /** * The maximum size the graphic will be. No matter how big the panel * becomes, the graphic will not exceed this length. <P>(This is enforced * because only 1 BufferedImage is used to render the graphic. This image is * created once at a fixed size and is never replaced.) */ public static int MAX_SIZE = 325; private int mode = ColorPicker.BRI; private Point point = new Point (0, 0); private ArrayList<ChangeListener> changeListeners; /* Floats from [0,1]. They must be kept distinct, because * when you convert them to RGB coordinates HSB(0,0,0) and HSB (.5,0,0) * and then convert them back to HSB coordinates, the hue always shifts back to zero. */ float hue = -1, sat = -1, bri = -1; int red = -1, green = -1, blue = -1; MouseInputListener mouseListener = new MouseInputAdapter () { @Override public void mousePressed(MouseEvent e) { requestFocus (); Point p = e.getPoint (); int size = Math.min (MAX_SIZE, Math.min (getWidth () - imagePadding.left - imagePadding.right, getHeight () - imagePadding.top - imagePadding.bottom)); p.translate (-(getWidth () / 2 - size / 2), -(getHeight () / 2 - size / 2)); if (mode == ColorPicker.BRI || mode == ColorPicker.SAT) { //the two circular views: double radius = ((double) size) / 2.0; double x = p.getX () - size / 2.0; double y = p.getY () - size / 2.0; double r = Math.sqrt (x * x + y * y) / radius; double theta = Math.atan2 (y, x) / (Math.PI * 2.0); if (r > 1) { r = 1; } if (mode == ColorPicker.BRI) { setHSB ((float) (theta + .25f), (float) (r), bri); } else { setHSB ((float) (theta + .25f), sat, (float) (r)); } } else if (mode == ColorPicker.HUE) { float s = ((float) p.x) / ((float) size); float b = ((float) p.y) / ((float) size); if (s < 0) { s = 0; } if (s > 1) { s = 1; } if (b < 0) { b = 0; } if (b > 1) { b = 1; } setHSB (hue, s, b); } else { int x2 = p.x * 255 / size; int y2 = p.y * 255 / size; if (x2 < 0) { x2 = 0; } if (x2 > 255) { x2 = 255; } if (y2 < 0) { y2 = 0; } if (y2 > 255) { y2 = 255; } if (mode == ColorPicker.RED) { setRGB (red, x2, y2); } else if (mode == ColorPicker.GREEN) { setRGB (x2, green, y2); } else { setRGB (x2, y2, blue); } } } @Override public void mouseDragged(MouseEvent e) { mousePressed (e); } }; KeyListener keyListener = new KeyAdapter () { @Override public void keyPressed(KeyEvent e) { int dx = 0; int dy = 0; if (e.getKeyCode () == KeyEvent.VK_LEFT) { dx = -1; } else if (e.getKeyCode () == KeyEvent.VK_RIGHT) { dx = 1; } else if (e.getKeyCode () == KeyEvent.VK_UP) { dy = -1; } else if (e.getKeyCode () == KeyEvent.VK_DOWN) { dy = 1; } int multiplier = 1; if (e.isShiftDown () && e.isAltDown ()) { multiplier = 10; } else if (e.isShiftDown () || e.isAltDown ()) { multiplier = 5; } if (dx != 0 || dy != 0) { int size = Math.min (MAX_SIZE, Math.min (getWidth () - imagePadding.left - imagePadding.right, getHeight () - imagePadding.top - imagePadding.bottom)); int offsetX = getWidth () / 2 - size / 2; int offsetY = getHeight () / 2 - size / 2; mouseListener.mousePressed (new MouseEvent (ColorPickerPanel.this, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis (), 0, point.x + multiplier * dx + offsetX, point.y + multiplier * dy + offsetY, 1, false)); } } }; FocusListener focusListener = new FocusListener () { @Override public void focusGained(FocusEvent e) { repaint (); } @Override public void focusLost(FocusEvent e) { repaint (); } }; ComponentListener componentListener = new ComponentAdapter () { @Override public void componentResized(ComponentEvent e) { regeneratePoint (); regenerateImage (); } }; BufferedImage image = new BufferedImage (MAX_SIZE, MAX_SIZE, BufferedImage.TYPE_INT_ARGB); /** * Creates a new * <code>ColorPickerPanel</code> */ public ColorPickerPanel() { setMaximumSize (new Dimension (MAX_SIZE + imagePadding.left + imagePadding.right, MAX_SIZE + imagePadding.top + imagePadding.bottom)); setPreferredSize (new Dimension ((int) (MAX_SIZE * .75), (int) (MAX_SIZE * .75))); setRGB (0, 0, 0); addMouseListener (mouseListener); addMouseMotionListener (mouseListener); setFocusable (true); addKeyListener (keyListener); addFocusListener (focusListener); setCursor (Cursor.getPredefinedCursor (Cursor.CROSSHAIR_CURSOR)); addComponentListener (componentListener); } /** * This listener will be notified when the current HSB or RGB values change, * depending on what mode the user is in. */ public void addChangeListener(ChangeListener l) { if (changeListeners == null) { changeListeners = new ArrayList<ChangeListener> (); } if (changeListeners.contains (l)) { return; } changeListeners.add (l); } /** * Remove a * <code>ChangeListener</code> so it is no longer notified when the selected * color changes. */ public void removeChangeListener(ChangeListener l) { if (changeListeners == null) { return; } changeListeners.remove (l); } protected void fireChangeListeners() { if (changeListeners == null) { return; } for (int a = 0; a < changeListeners.size (); a++) { ChangeListener l = (ChangeListener) changeListeners.get (a); try { l.stateChanged (new ChangeEvent (this)); } catch (RuntimeException e) { } } } Insets imagePadding = new Insets (6, 6, 6, 6); @Override public void paint(Graphics g) { super.paint (g); Graphics2D g2 = (Graphics2D) g; int size = Math.min (MAX_SIZE, Math.min (getWidth () - imagePadding.left - imagePadding.right, getHeight () - imagePadding.top - imagePadding.bottom)); g2.translate (getWidth () / 2 - size / 2, getHeight () / 2 - size / 2); g2.setRenderingHint (RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); Shape shape; if (mode == ColorPicker.SAT || mode == ColorPicker.BRI) { shape = new Ellipse2D.Float (0, 0, size, size); } else { Rectangle r = new Rectangle (0, 0, size, size); shape = r; } if (hasFocus ()) { PaintUtils.paintFocus (g2, shape, 5); } if (!(shape instanceof Rectangle)) { //paint a circular shadow g2.translate (2, 2); g2.setColor (new Color (0, 0, 0, 20)); g2.fill (new Ellipse2D.Float (-2, -2, size + 4, size + 4)); g2.setColor (new Color (0, 0, 0, 40)); g2.fill (new Ellipse2D.Float (-1, -1, size + 2, size + 2)); g2.setColor (new Color (0, 0, 0, 80)); g2.fill (new Ellipse2D.Float (0, 0, size, size)); g2.translate (-2, -2); } g2.drawImage (image, 0, 0, size, size, 0, 0, size, size, null); if (shape instanceof Rectangle) { Rectangle r = (Rectangle) shape; PaintUtils.drawBevel (g2, r); } else { g2.setColor (new Color (0, 0, 0, 120)); g2.draw (shape); } g2.setColor (Color.white); g2.setStroke (new BasicStroke (1)); g2.draw (new Ellipse2D.Float (point.x - 3, point.y - 3, 6, 6)); g2.setColor (Color.black); g2.draw (new Ellipse2D.Float (point.x - 4, point.y - 4, 8, 8)); g.translate (-imagePadding.left, -imagePadding.top); } /** * Set the mode of this panel. * * @param mode This must be one of the following constants from * the <code>ColorPicker</code> class: * * * <code>HUE</code>, <code>SAT</code>, <code>BRI</code>, <code>RED</code>, <code>GREEN</code>, * or <code>BLUE</code> */ public void setMode(int mode) { if (!(mode == ColorPicker.HUE || mode == ColorPicker.SAT || mode == ColorPicker.BRI || mode == ColorPicker.RED || mode == ColorPicker.GREEN || mode == ColorPicker.BLUE)) { throw new IllegalArgumentException ("The mode must be HUE, SAT, BRI, RED, GREEN, or BLUE."); } if (this.mode == mode) { return; } this.mode = mode; regenerateImage (); regeneratePoint (); } /** * Sets the selected color of this panel. <P>If this panel is in HUE, SAT, * or BRI mode, then this method converts these values to HSB coordinates * and calls * <code>setHSB</code>. <P>This method may regenerate the graphic if * necessary. * * @param r the red value of the selected color. * @param g the green value of the selected color. * @param b the blue value of the selected color. */ public void setRGB(int r, int g, int b) { if (r < 0 || r > 255) { throw new IllegalArgumentException ("The red value (" + r + ") must be between [0,255]."); } if (g < 0 || g > 255) { throw new IllegalArgumentException ("The green value (" + g + ") must be between [0,255]."); } if (b < 0 || b > 255) { throw new IllegalArgumentException ("The blue value (" + b + ") must be between [0,255]."); } if (red != r || green != g || blue != b) { if (mode == ColorPicker.RED || mode == ColorPicker.GREEN || mode == ColorPicker.BLUE) { int lastR = red; int lastG = green; int lastB = blue; red = r; green = g; blue = b; if (mode == ColorPicker.RED) { if (lastR != r) { regenerateImage (); } } else if (mode == ColorPicker.GREEN) { if (lastG != g) { regenerateImage (); } } else if (mode == ColorPicker.BLUE) { if (lastB != b) { regenerateImage (); } } } else { float[] hsb = new float[3]; Color.RGBtoHSB (r, g, b, hsb); setHSB (hsb[0], hsb[1], hsb[2]); return; } regeneratePoint (); repaint (); fireChangeListeners (); } } /** * @return the HSB values of the selected color. Each value is between * [0,1]. */ public float[] getHSB() { return new float[] { hue, sat, bri }; } /** * @return the RGB values of the selected color. Each value is between * [0,255]. */ public int[] getRGB() { return new int[] { red, green, blue }; } /** * Sets the selected color of this panel. <P>If this panel is in RED, GREEN, * or BLUE mode, then this method converts these values to RGB coordinates * and calls * <code>setRGB</code>. <P>This method may regenerate the graphic if * necessary. * * @param h the hue value of the selected color. * @param s the saturation value of the selected color. * @param b the brightness value of the selected color. */ public void setHSB(float h, float s, float b) { if (Float.isInfinite (h) || Float.isNaN (h)) { throw new IllegalArgumentException ("The hue value (" + h + ") is not a valid number."); } //hue is cyclic, so it can be any value: while (h < 0) { h++; } while (h > 1) { h--; } if (s < 0 || s > 1) { throw new IllegalArgumentException ("The saturation value (" + s + ") must be between [0,1]"); } if (b < 0 || b > 1) { throw new IllegalArgumentException ("The brightness value (" + b + ") must be between [0,1]"); } if (hue != h || sat != s || bri != b) { if (mode == ColorPicker.HUE || mode == ColorPicker.BRI || mode == ColorPicker.SAT) { float lastHue = hue; float lastBri = bri; float lastSat = sat; hue = h; sat = s; bri = b; if (mode == ColorPicker.HUE) { if (lastHue != hue) { regenerateImage (); } } else if (mode == ColorPicker.SAT) { if (lastSat != sat) { regenerateImage (); } } else if (mode == ColorPicker.BRI) { if (lastBri != bri) { regenerateImage (); } } } else { Color c = new Color (Color.HSBtoRGB (h, s, b)); setRGB (c.getRed (), c.getGreen (), c.getBlue ()); return; } Color c = new Color (Color.HSBtoRGB (hue, sat, bri)); red = c.getRed (); green = c.getGreen (); blue = c.getBlue (); regeneratePoint (); repaint (); fireChangeListeners (); } } /** * Recalculates the (x,y) point used to indicate the selected color. */ private void regeneratePoint() { int size = Math.min (MAX_SIZE, Math.min (getWidth () - imagePadding.left - imagePadding.right, getHeight () - imagePadding.top - imagePadding.bottom)); if (mode == ColorPicker.HUE || mode == ColorPicker.SAT || mode == ColorPicker.BRI) { if (mode == ColorPicker.HUE) { point = new Point ((int) (sat * size), (int) (bri * size)); } else if (mode == ColorPicker.SAT) { double theta = hue * 2 * Math.PI - Math.PI / 2; if (theta < 0) { theta += 2 * Math.PI; } double r = bri * size / 2; point = new Point ((int) (r * Math.cos (theta) + .5 + size / 2.0), (int) (r * Math.sin (theta) + .5 + size / 2.0)); } else if (mode == ColorPicker.BRI) { double theta = hue * 2 * Math.PI - Math.PI / 2; if (theta < 0) { theta += 2 * Math.PI; } double r = sat * size / 2; point = new Point ((int) (r * Math.cos (theta) + .5 + size / 2.0), (int) (r * Math.sin (theta) + .5 + size / 2.0)); } } else if (mode == ColorPicker.RED) { point = new Point ((int) (green * size / 255f + .49f), (int) (blue * size / 255f + .49f)); } else if (mode == ColorPicker.GREEN) { point = new Point ((int) (red * size / 255f + .49f), (int) (blue * size / 255f + .49f)); } else if (mode == ColorPicker.BLUE) { point = new Point ((int) (red * size / 255f + .49f), (int) (green * size / 255f + .49f)); } } /** * A row of pixel data we recycle every time we regenerate this image. */ private int[] row = new int[MAX_SIZE]; /** * Regenerates the image. */ private synchronized void regenerateImage() { int size = Math.min (MAX_SIZE, Math.min (getWidth () - imagePadding.left - imagePadding.right, getHeight () - imagePadding.top - imagePadding.bottom)); if (mode == ColorPicker.BRI || mode == ColorPicker.SAT) { float bri2 = this.bri; float sat2 = this.sat; float radius = ((float) size) / 2f; float hue2; float k = 1.2f; //the number of pixels to antialias for (int y = 0; y < size; y++) { float y2 = (y - size / 2f); for (int x = 0; x < size; x++) { float x2 = (x - size / 2f); double theta = Math.atan2 (y2, x2) - 3 * Math.PI / 2.0; if (theta < 0) { theta += 2 * Math.PI; } double r = Math.sqrt (x2 * x2 + y2 * y2); if (r <= radius) { if (mode == ColorPicker.BRI) { hue2 = (float) (theta / (2 * Math.PI)); sat2 = (float) (r / radius); } else { //SAT hue2 = (float) (theta / (2 * Math.PI)); bri2 = (float) (r / radius); } row[x] = Color.HSBtoRGB (hue2, sat2, bri2); if (r > radius - k) { int alpha = (int) (255 - 255 * (r - radius + k) / k); if (alpha < 0) { alpha = 0; } if (alpha > 255) { alpha = 255; } row[x] = row[x] & 0xffffff + (alpha << 24); } } else { row[x] = 0x00000000; } } image.getRaster ().setDataElements (0, y, size, 1, row); } } else if (mode == ColorPicker.HUE) { float hue2 = this.hue; for (int y = 0; y < size; y++) { float y2 = ((float) y) / ((float) size); for (int x = 0; x < size; x++) { float x2 = ((float) x) / ((float) size); row[x] = Color.HSBtoRGB (hue2, x2, y2); } image.getRaster ().setDataElements (0, y, image.getWidth (), 1, row); } } else { //mode is RED, GREEN, or BLUE int red2 = red; int green2 = green; int blue2 = blue; for (int y = 0; y < size; y++) { float y2 = ((float) y) / ((float) size); for (int x = 0; x < size; x++) { float x2 = ((float) x) / ((float) size); if (mode == ColorPicker.RED) { green2 = (int) (x2 * 255 + .49); blue2 = (int) (y2 * 255 + .49); } else if (mode == ColorPicker.GREEN) { red2 = (int) (x2 * 255 + .49); blue2 = (int) (y2 * 255 + .49); } else { red2 = (int) (x2 * 255 + .49); green2 = (int) (y2 * 255 + .49); } row[x] = 0xFF000000 + (red2 << 16) + (green2 << 8) + blue2; } image.getRaster ().setDataElements (0, y, size, 1, row); } } repaint (); } }