/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2006-14 Ben Fry and Casey Reas
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.
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, write to the Free Software Foundation,
Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.app.ui;
import processing.app.Language;
import processing.app.Platform;
import processing.core.*;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.text.*;
/**
* Generic color selector frame, pulled from the Tool object. API not really
* worked out here (what should the constructor be? how flexible?) So use with
* caution and be ready for it to break in future releases.
*/
public class ColorChooser { //extends JFrame implements DocumentListener {
int hue, saturation, brightness; // range 360, 100, 100
int red, green, blue; // range 256, 256, 256
ColorRange range;
ColorSlider slider;
JTextField hueField, saturationField, brightnessField;
JTextField redField, greenField, blueField;
JTextField hexField;
JPanel colorPanel;
DocumentListener colorListener;
JDialog window;
// public String getMenuTitle() {
// return "Color Selector";
// }
public ColorChooser(Frame owner, boolean modal, Color initialColor,
String buttonName, ActionListener buttonListener) {
//super("Color Selector");
window = new JDialog(owner, Language.text("color_chooser"), modal);
window.getContentPane().setLayout(new BorderLayout());
Box box = Box.createHorizontalBox();
box.setBorder(new EmptyBorder(12, 12, 12, 12));
range = new ColorRange();
Box rangeBox = new Box(BoxLayout.Y_AXIS);
rangeBox.setAlignmentY(0);
rangeBox.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
rangeBox.add(range);
box.add(rangeBox);
box.add(Box.createHorizontalStrut(10));
slider = new ColorSlider();
Box sliderBox = new Box(BoxLayout.Y_AXIS);
sliderBox.setAlignmentY(0);
sliderBox.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
sliderBox.add(slider);
box.add(sliderBox);
box.add(Box.createHorizontalStrut(10));
box.add(createColorFields(buttonName, buttonListener));
// System.out.println("1: " + hexField.getInsets());
box.add(Box.createHorizontalStrut(10));
// System.out.println("2: " + hexField.getInsets());
window.getContentPane().add(box, BorderLayout.CENTER);
// System.out.println(hexField);
// System.out.println("3: " + hexField.getInsets());
// colorPanel.setInsets(hexField.getInsets());
window.pack();
window.setResizable(false);
// Dimension size = getSize();
// Dimension screen = Toolkit.getScreenSize();
// setLocation((screen.width - size.width) / 2,
// (screen.height - size.height) / 2);
window.setLocationRelativeTo(null);
window.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
window.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
hide();
}
});
Toolkit.registerWindowCloseKeys(window.getRootPane(), new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
hide();
}
});
Toolkit.setIcon(window);
colorListener = new ColorListener();
hueField.getDocument().addDocumentListener(colorListener);
saturationField.getDocument().addDocumentListener(colorListener);
brightnessField.getDocument().addDocumentListener(colorListener);
redField.getDocument().addDocumentListener(colorListener);
greenField.getDocument().addDocumentListener(colorListener);
blueField.getDocument().addDocumentListener(colorListener);
hexField.getDocument().addDocumentListener(colorListener);
setColor(initialColor);
// System.out.println("4: " + hexField.getInsets());
}
//hexField.setText("#FFFFFF");
public void show() {
window.setVisible(true);
}
public void hide() {
window.setVisible(false);
}
public Color getColor() {
return new Color(red, green, blue);
}
public void setColor(Color color) {
updateRGB(color.getRGB());
}
public String getHexColor() {
return "#" + PApplet.hex(red, 2) + PApplet.hex(green, 2) + PApplet.hex(blue, 2);
}
public class ColorListener implements DocumentListener {
public void changedUpdate(DocumentEvent e) {
//System.out.println("changed");
}
public void removeUpdate(DocumentEvent e) {
//System.out.println("remove");
}
boolean updating;
public void insertUpdate(DocumentEvent e) {
if (updating) return; // don't update forever recursively
updating = true;
Document doc = e.getDocument();
if (doc == hueField.getDocument()) {
hue = bounded(hue, hueField, 359);
updateRGB();
updateHex();
} else if (doc == saturationField.getDocument()) {
saturation = bounded(saturation, saturationField, 99);
updateRGB();
updateHex();
} else if (doc == brightnessField.getDocument()) {
brightness = bounded(brightness, brightnessField, 99);
updateRGB();
updateHex();
} else if (doc == redField.getDocument()) {
red = bounded(red, redField, 255);
updateHSB();
updateHex();
} else if (doc == greenField.getDocument()) {
green = bounded(green, greenField, 255);
updateHSB();
updateHex();
} else if (doc == blueField.getDocument()) {
blue = bounded(blue, blueField, 255);
updateHSB();
updateHex();
} else if (doc == hexField.getDocument()) {
String str = hexField.getText();
if (str.startsWith("#")) {
str = str.substring(1);
}
while (str.length() < 6) {
str += "0";
}
if (str.length() > 6) {
str = str.substring(0, 6);
}
updateRGB(Integer.parseInt(str, 16));
updateHSB();
}
range.repaint();
slider.repaint();
//colorPanel.setBackground(new Color(red, green, blue));
colorPanel.repaint();
updating = false;
}
}
/**
* Set the RGB values based on the current HSB values.
*/
protected void updateRGB() {
updateRGB(Color.HSBtoRGB(hue / 359f,
saturation / 99f,
brightness / 99f));
}
/**
* Set the RGB values based on a calculated ARGB int.
* Used by both updateRGB() to set the color from the HSB values,
* and by updateHex(), to unpack the hex colors and assign them.
*/
protected void updateRGB(int rgb) {
red = (rgb >> 16) & 0xff;
green = (rgb >> 8) & 0xff;
blue = rgb & 0xff;
redField.setText(String.valueOf(red));
greenField.setText(String.valueOf(green));
blueField.setText(String.valueOf(blue));
}
/**
* Set the HSB values based on the current RGB values.
*/
protected void updateHSB() {
float hsb[] = new float[3];
Color.RGBtoHSB(red, green, blue, hsb);
hue = (int) (hsb[0] * 359.0f);
saturation = (int) (hsb[1] * 99.0f);
brightness = (int) (hsb[2] * 99.0f);
hueField.setText(String.valueOf(hue));
saturationField.setText(String.valueOf(saturation));
brightnessField.setText(String.valueOf(brightness));
}
protected void updateHex() {
hexField.setText(getHexColor());
}
/**
* Get the bounded value for a specific range. If the value is outside
* the max, you can't edit right away, so just act as if it's already
* been bounded and return the bounded value, then fire an event to set
* it to the value that was just returned.
*/
protected int bounded(int current, final JTextField field, final int max) {
String text = field.getText();
if (text.length() == 0) {
return 0;
}
try {
int value = Integer.parseInt(text);
if (value > max) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
field.setText(String.valueOf(max));
}
});
return max;
}
return value;
} catch (NumberFormatException e) {
return current; // should not be reachable
}
}
protected Container createColorFields(String buttonName, ActionListener buttonListener) {
Box box = Box.createVerticalBox();
box.setAlignmentY(0);
final int GAP = Platform.isWindows() ? 5 : 0;
final int BETWEEN = Platform.isWindows() ? 8 : 6; //10;
Box row;
row = Box.createHorizontalBox();
if (Platform.isMacOS()) {
row.add(Box.createHorizontalStrut(17));
} else {
row.add(createFixedLabel(""));
}
// Can't just set the bg color of the panel because it also tints the bevel
// (on OS X), which looks odd. So instead we override paintComponent().
colorPanel = new JPanel() {
public void paintComponent(Graphics g) {
g.setColor(new Color(red, green, blue));
Dimension size = getSize();
g.fillRect(0, 0, size.width, size.height);
}
};
colorPanel.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
Dimension dim = new Dimension(70, 25);
colorPanel.setMinimumSize(dim);
colorPanel.setMaximumSize(dim);
colorPanel.setPreferredSize(dim);
row.add(colorPanel);
row.add(Box.createHorizontalGlue());
box.add(row);
box.add(Box.createVerticalStrut(BETWEEN));
// if (Base.isMacOS()) { // need a little extra
// box.add(Box.createVerticalStrut(BETWEEN));
// }
row = Box.createHorizontalBox();
row.add(createFixedLabel("H"));
row.add(hueField = new NumberField(4, false));
row.add(new JLabel(" \u00B0")); // degree symbol
row.add(Box.createHorizontalGlue());
box.add(row);
box.add(Box.createVerticalStrut(GAP));
row = Box.createHorizontalBox();
row.add(createFixedLabel("S"));
row.add(saturationField = new NumberField(4, false));
row.add(new JLabel(" %"));
row.add(Box.createHorizontalGlue());
box.add(row);
box.add(Box.createVerticalStrut(GAP));
row = Box.createHorizontalBox();
row.add(createFixedLabel("B"));
row.add(brightnessField = new NumberField(4, false));
row.add(new JLabel(" %"));
row.add(Box.createHorizontalGlue());
box.add(row);
box.add(Box.createVerticalStrut(BETWEEN));
//
row = Box.createHorizontalBox();
row.add(createFixedLabel("R"));
row.add(redField = new NumberField(4, false));
row.add(Box.createHorizontalGlue());
box.add(row);
box.add(Box.createVerticalStrut(GAP));
row = Box.createHorizontalBox();
row.add(createFixedLabel("G"));
row.add(greenField = new NumberField(4, false));
row.add(Box.createHorizontalGlue());
box.add(row);
box.add(Box.createVerticalStrut(GAP));
row = Box.createHorizontalBox();
row.add(createFixedLabel("B"));
row.add(blueField = new NumberField(4, false));
row.add(Box.createHorizontalGlue());
box.add(row);
box.add(Box.createVerticalStrut(BETWEEN));
//
row = Box.createHorizontalBox();
row.add(createFixedLabel(""));
// Windows needs extra space, OS X and Linux do not
// Mac OS X needs 6 because #CCCCCC is quite wide
final int hexCount = Platform.isWindows() ? 7 : 6;
row.add(hexField = new NumberField(hexCount, true));
row.add(Box.createHorizontalGlue());
box.add(row);
box.add(Box.createVerticalStrut(GAP));
//
// // Not great, because the insets make things weird anyway
// //Dimension dim = new Dimension(hexField.getPreferredSize());
// Dimension dim = new Dimension(70, 20);
// colorPanel.setMinimumSize(dim);
// colorPanel.setMaximumSize(dim);
// colorPanel.setPreferredSize(dim);
//// colorPanel.setBorder(new EmptyBorder(hexField.getInsets()));
//
row = Box.createHorizontalBox();
if (Platform.isMacOS()) {
row.add(Box.createHorizontalStrut(11));
} else {
row.add(createFixedLabel(""));
}
JButton button = new JButton(buttonName);
button.addActionListener(buttonListener);
//System.out.println("button: " + button.getInsets());
row.add(button);
row.add(Box.createHorizontalGlue());
box.add(row);
row = Box.createHorizontalBox();
if (Platform.isMacOS()) {
row.add(Box.createHorizontalStrut(11));
} else {
row.add(createFixedLabel(""));
}
button = new JButton(Language.text("prompt.cancel"));
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
ColorChooser.this.hide();
}
});
row.add(button);
row.add(Box.createHorizontalGlue());
box.add(row);
//
box.add(Box.createVerticalGlue());
return box;
}
int labelH;
/**
* return a label of a fixed width
*/
protected JLabel createFixedLabel(String title) {
JLabel label = new JLabel(title);
if (labelH == 0) {
labelH = label.getPreferredSize().height;
}
Dimension dim = new Dimension(15, labelH);
label.setPreferredSize(dim);
label.setMinimumSize(dim);
label.setMaximumSize(dim);
return label;
}
public class ColorRange extends JComponent {
static final int WIDE = 256;
static final int HIGH = 256;
private int lastX, lastY;
public ColorRange() {
setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
updateMouse(e);
}
});
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
updateMouse(e);
}
});
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
super.keyPressed(e);
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
ColorChooser.this.hide();
}
}
});
}
private void updateMouse(MouseEvent e) {
int mouseX = e.getX();
int mouseY = e.getY();
if ((mouseX >= 0) && (mouseX < WIDE) &&
(mouseY >= 0) && (mouseY < HIGH)) {
int nsaturation = (int) (100 * (mouseX / 255.0f));
int nbrightness = 100 - ((int) (100 * (mouseY / 255.0f)));
saturationField.setText(String.valueOf(nsaturation));
brightnessField.setText(String.valueOf(nbrightness));
lastX = mouseX;
lastY = mouseY;
}
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
for (int j = 0; j < WIDE; j++) {
for (int i = 0; i < HIGH; i++) {
g.setColor(Color.getHSBColor(hue / 360f, i / 256f, (255 - j) / 256f));
g.fillRect(i, j, 1, 1);
}
}
g.setColor((brightness > 50) ? Color.BLACK : Color.WHITE);
g.drawRect(lastX - 5, lastY - 5, 10, 10);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(WIDE, HIGH);
}
@Override
public Dimension getMinimumSize() {
return getPreferredSize();
}
@Override
public Dimension getMaximumSize() {
return getPreferredSize();
}
}
public class ColorSlider extends JComponent {
static final int WIDE = 20;
static final int HIGH = 256;
public ColorSlider() {
setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
updateMouse(e);
}
});
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
updateMouse(e);
}
});
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
super.keyPressed(e);
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
ColorChooser.this.hide();
}
}
});
}
private void updateMouse(MouseEvent e) {
int mouseX = e.getX();
int mouseY = e.getY();
if ((mouseX >= 0) && (mouseX < WIDE) &&
(mouseY >= 0) && (mouseY < HIGH)) {
int nhue = 359 - (int) (359 * (mouseY / 255.0f));
hueField.setText(String.valueOf(nhue));
}
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
int sel = 255 - (int) (255 * (hue / 359.0));
for (int j = 0; j < HIGH; j++) {
Color color = Color.getHSBColor((255 - j) / 256f, 1, 1);
if (j == sel) {
color = Color.BLACK;
}
g.setColor(color);
g.drawRect(0, j, WIDE, 1);
}
}
public Dimension getPreferredSize() {
return new Dimension(WIDE, HIGH);
}
public Dimension getMinimumSize() {
return getPreferredSize();
}
public Dimension getMaximumSize() {
return getPreferredSize();
}
}
/**
* Extension of JTextField that only allows numbers
*/
static class NumberField extends JTextField {
public boolean allowHex;
public NumberField(int cols, boolean allowHex) {
super(cols);
this.allowHex = allowHex;
}
protected Document createDefaultModel() {
return new NumberDocument(this);
}
public Dimension getPreferredSize() {
if (!allowHex) {
return new Dimension(45, super.getPreferredSize().height);
}
return super.getPreferredSize();
}
public Dimension getMinimumSize() {
return getPreferredSize();
}
public Dimension getMaximumSize() {
return getPreferredSize();
}
}
/**
* Document model to go with JTextField that only allows numbers.
*/
static class NumberDocument extends PlainDocument {
NumberField parentField;
public NumberDocument(NumberField parentField) {
this.parentField = parentField;
//System.out.println("setting parent to " + parentSelector);
}
public void insertString(int offs, String str, AttributeSet a)
throws BadLocationException {
if (str == null) return;
char chars[] = str.toCharArray();
int charCount = 0;
// remove any non-digit chars
for (int i = 0; i < chars.length; i++) {
boolean ok = Character.isDigit(chars[i]);
if (parentField.allowHex) {
if ((chars[i] >= 'A') && (chars[i] <= 'F')) ok = true;
if ((chars[i] >= 'a') && (chars[i] <= 'f')) ok = true;
if ((offs == 0) && (i == 0) && (chars[i] == '#')) ok = true;
}
if (ok) {
if (charCount != i) { // shift if necessary
chars[charCount] = chars[i];
}
charCount++;
}
}
super.insertString(offs, new String(chars, 0, charCount), a);
// can't call any sort of methods on the enclosing class here
// seems to have something to do with how Document objects are set up
}
}
// static public void main(String[] args) {
// ColorSelector cs = new ColorSelector();
// cs.init(null);
// EventQueue.invokeLater(cs);
// }
}