/*
* 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 ();
}
}