/*
* @(#)ColorFormatter.java
*
* Copyright (c) 2009-2010 The authors and contributors of JHotDraw.
*
* You may not use, copy or modify this file, except in compliance with the
* accompanying license terms.
*/
package org.jhotdraw.text;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.awt.Color;
import java.awt.color.ColorSpace;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JFormattedTextField.AbstractFormatterFactory;
import javax.swing.text.DefaultFormatter;
import javax.swing.text.DefaultFormatterFactory;
import org.jhotdraw.color.ColorUtil;
import org.jhotdraw.color.HSBColorSpace;
import org.jhotdraw.util.prefs.PreferencesUtil;
/**
* {@code ColorFormatter} is used to format colors into a textual representation
* which can be edited in an entry field.
* <p>
* The following formats are supported:
* <ul>
* <li><b>Format.RGB_HEX</b> - {@code "#"rrggbb} or {@code "#"rgb}.<br>
* If 6 digits are entered, each pair of hexadecimal digits, in the range 0
* to F, represents one sRGB color component in the order red, green and blue.
* The digits A to F may be given in either uppercase or lowercase.<br>
* If only 3 digits are entered, they are expanded to 6 digits by
* replicating each digit.<br>
* This syntactical form can represent 16777216 colors.
* Examples: {@code #9400D3} (i.e. a dark violet), {@code #FFD700}
* (i.e. a golden color), {@code #000} (i.e. black) {@code #fff} (i.e. white).
* </li>
* <li><b>Format.RGB_INTEGER_SHORT</b> - {@code red green blue},
* or {@code red green blue} optionally separated by commas.<br>
* Each value represents one sRGB color component.
* Each value is in the range 0 to 255.
* This syntactical form can represent 16777216 colors.
* Examples: {@code 233 150 122} (i.e. a salmon pink), {@code 255 165 0}
* (i.e. an orange).
* </li>
* <li><b>Format.RGB_INTEGER</b> - {@code "rgb" red green blue},
* or {@code red green blue} optionally separated by commas.<br>
* Each value represents one sRGB color component.
* Each value is in the range 0 to 255.
* This syntactical form can represent 16777216 colors.
* Examples: {@code rgb 233 150 122} (i.e. a salmon pink), {@code rgb 255 165 0}
* (i.e. an orange).
* </li>
* <li><b>Format.RGB_PERCENTAGE</b> - {@code "rgb%" red green blue},
* or {@code red"%" green"%" blue"%"} optionally separated by commas.<br>
* Each value represents one sRGB color component.
* Each value is in the range 0.0 to 100.0.
* This syntactical form can represent 10^9 colors.
* </li>
* <li><b>Format.GRAY_PERCENTAGE</b> - {@code "gray" brightness}.<br>
* The value represents the brightness in the range from 0.0 to 100.0.
* </li>
* <li><b>Format.HSB_PERCENTAGE</b> - {@code "hsb" hue saturation brightness}.<br>
* Each integer represents one HSV component in the order hue, saturation and
* value, separated by a comma and
* optionally by white space. Hue is in the range from 0.0 to 359.0, saturation
* and brightness in the range from 0.0 to 100.0.
* </li>
* </ul>
* <p>
* By default, the formatter is adaptive, meaning that the format depends
* on the {@code ColorSpace} of the current {@code Color} value.
*
* <p>
* FIXME - This class does too much work. It should be split up into
* individual classes for each of the supported formats.
*
* @author Werner Randelshofer
* @version $Id$
*/
public class ColorFormatter extends DefaultFormatter {
private static final long serialVersionUID = 1L;
/**
* Specifies the formats supported by ColorFormatter.
*/
public enum Format {
RGB_HEX,
RGB_INTEGER_SHORT,
RGB_INTEGER,
RGB_PERCENTAGE,
HSB_PERCENTAGE,
GRAY_PERCENTAGE,
CMYK_PERCENTAGE;
};
/**
* Specifies the preferred output format.
*/
protected Format outputFormat = Format.RGB_INTEGER;
/**
* Specifies the last used input format.
*/
@Nullable protected Format lastUsedInputFormat = null;
/**
* This regular expression is used for parsing the RGB_HEX format.
*/
protected static final Pattern rgbHexPattern = Pattern.compile("^\\s*(?:[rR][gG][bB]\\s*#|#)\\s*([0-9a-fA-F]{3,6})\\s*$");
/**
* This regular expression is used for parsing the RGB_INTEGER format.
*/
protected static final Pattern rgbIntegerShortPattern = Pattern.compile("^\\s*([0-9]{1,3})(?:\\s*,\\s*|\\s+)([0-9]{1,3})(?:\\s*,\\s*|\\s+)([0-9]{1,3})\\s*$");
/**
* This regular expression is used for parsing the RGB_INTEGER format.
*/
protected static final Pattern rgbIntegerPattern = Pattern.compile("^\\s*(?:[rR][gG][bB])?\\s*([0-9]{1,3})(?:\\s*,\\s*|\\s+)([0-9]{1,3})(?:\\s*,\\s*|\\s+)([0-9]{1,3})\\s*$");
/**
* This regular expression is used for parsing the RGB_PERCENTAGE format.
*/
protected static final Pattern rgbPercentagePattern = Pattern.compile("^\\s*(?:[rR][gG][bB][%])?\\s*([0-9]{1,3}(?:\\.[0-9]+)?)(?:\\s*,\\s*|\\s+)([0-9]{1,3}(?:\\.[0-9]+)?)(?:\\s*,\\s*|\\s+)([0-9]{1,3}(?:\\.[0-9]+)?)\\s*$");
/**
* This regular expression is used for parsing the HSB_PERCENTAGE format.
* This format is recognized when the degree sign is present.
*/
protected static final Pattern hsbPercentagePattern = Pattern.compile("^\\s*(?:[hH][sS][bB])?\\s*([0-9]{1,3}(?:\\.[0-9]+)?)(?:\\s*,\\s*|\\s+)([0-9]{1,3}(?:\\.[0-9]+)?)(?:\\s*,\\s*|\\s+)([0-9]{1,3}(?:\\.[0-9]+)?)\\s*$");
/**
* This regular expression is used for parsing the GRAY_PERCENTAGE format.
* This format is recognized when the degree sign is present.
*/
protected static final Pattern grayPercentagePattern = Pattern.compile("^\\s*(?:[gG][rR][aA][yY])?\\s*([0-9]{1,3}(?:\\.[0-9]+)?)\\s*$");
/**
* Specifies whether the formatter allows null values.
*/
protected boolean allowsNullValue = true;
/**
* Specifies whether the formatter should adaptively change its output
* format depending on the last input format used by the user.
*/
protected boolean isAdaptive = true;
/**
* Preferences used for storing the last used input format.
*/
protected Preferences prefs;
protected DecimalFormat numberFormat;
public ColorFormatter() {
this(Format.RGB_INTEGER, true, true);
}
public ColorFormatter(Format outputFormat, boolean allowsNullValue, boolean isAdaptive) {
this.outputFormat = outputFormat;
this.allowsNullValue = allowsNullValue;
this.isAdaptive = isAdaptive;
numberFormat = new DecimalFormat("#.#");
numberFormat.setDecimalSeparatorAlwaysShown(false);
numberFormat.setMaximumFractionDigits(1);
DecimalFormatSymbols dfs = new DecimalFormatSymbols();
dfs.setDecimalSeparator('.');
numberFormat.setDecimalFormatSymbols(dfs);
// Retrieve last used input format from preferences
prefs = PreferencesUtil.userNodeForPackage(getClass());
try {
lastUsedInputFormat = Format.valueOf(prefs.get("ColorFormatter.lastUsedInputFormat", Format.RGB_HEX.name()));
} catch (IllegalArgumentException e) {
// leave lastUsedInputFormat as null
}
if (isAdaptive && lastUsedInputFormat != null) {
this.outputFormat = lastUsedInputFormat;
}
setOverwriteMode(false);
}
/**
* Sets the output format.
* @param newValue
*/
public void setOutputFormat(Format newValue) {
if (newValue == null) {
throw new NullPointerException("outputFormat may not be null");
}
outputFormat = newValue;
}
/**
* Gets the output format.
*/
public Format getOutputFormat() {
return outputFormat;
}
/**
* Gets the last used input format.
*/
public Format getLastUsedInputFormat() {
return lastUsedInputFormat;
}
/**
* Sets whether a null value is allowed.
* @param newValue
*/
public void setAllowsNullValue(boolean newValue) {
allowsNullValue = newValue;
}
/**
* Returns true, if null value is allowed.
*/
public boolean getAllowsNullValue() {
return allowsNullValue;
}
/**
* Sets whether the color formatter adaptively selects its output
* format depending on the last input format used by the user.
*
* @param newValue
*/
public void setAdaptive(boolean newValue) {
isAdaptive = newValue;
if (newValue && lastUsedInputFormat != null) {
outputFormat = lastUsedInputFormat;
}
}
/**
* Returns true, if the color formatter is adaptive.
*/
public boolean isAdaptive() {
return isAdaptive;
}
private void setLastUsedInputFormat(Format newValue) {
lastUsedInputFormat = newValue;
if (isAdaptive) {
outputFormat = lastUsedInputFormat;
}
prefs.put("ColorFormatter.lastUsedInputFormat", newValue.name());
}
@Override
public Object stringToValue(String str) throws ParseException {
// Handle null and empty case
if (str == null || str.trim().length() == 0) {
if (allowsNullValue) {
return null;
} else {
throw new ParseException("Null value is not allowed.", 0);
}
}
// Format RGB_HEX
Matcher matcher = rgbHexPattern.matcher(str);
if (matcher.matches()) {
setLastUsedInputFormat(Format.RGB_HEX);
try {
String group1 = matcher.group(1);
if (group1.length() == 3) {
return new Color(Integer.parseInt(
"" + group1.charAt(0) + group1.charAt(0) + //
group1.charAt(1) + group1.charAt(1) + //
group1.charAt(2) + group1.charAt(2), //
16));
} else if (group1.length() == 6) {
return new Color(Integer.parseInt(group1, 16));
} else {
throw new ParseException("Hex color must have 3 or 6 digits.", 1);
}
} catch (NumberFormatException nfe) {
ParseException pe = new ParseException(str, 0);
pe.initCause(nfe);
throw pe;
}
}
// Format RGB_INTEGER_SHORT and RGB_INTEGER
matcher = rgbIntegerShortPattern.matcher(str);
if (matcher.matches()) {
setLastUsedInputFormat(Format.RGB_INTEGER_SHORT);
} else {
matcher = rgbIntegerPattern.matcher(str);
if (matcher.matches()) {
setLastUsedInputFormat(Format.RGB_INTEGER);
}
}
if (matcher.matches()) {
try {
return new Color(//
Integer.parseInt(matcher.group(1)), //
Integer.parseInt(matcher.group(2)), //
Integer.parseInt(matcher.group(3)));
} catch (NumberFormatException nfe) {
ParseException pe = new ParseException(str, 0);
pe.initCause(nfe);
throw pe;
} catch (IllegalArgumentException iae) {
ParseException pe = new ParseException(str, 0);
pe.initCause(iae);
throw pe;
}
}
// Format RGB_PERCENTAGE
matcher = rgbPercentagePattern.matcher(str);
if (matcher.matches()) {
setLastUsedInputFormat(Format.RGB_PERCENTAGE);
try {
return new Color(//
numberFormat.parse(matcher.group(1)).floatValue() / 100f, //
numberFormat.parse(matcher.group(2)).floatValue() / 100f, //
numberFormat.parse(matcher.group(3)).floatValue() / 100f);
} catch (NumberFormatException nfe) {
ParseException pe = new ParseException(str, 0);
pe.initCause(nfe);
throw pe;
} catch (IllegalArgumentException iae) {
ParseException pe = new ParseException(str, 0);
pe.initCause(iae);
throw pe;
}
}
// Format HSB_PERCENTAGE
matcher = hsbPercentagePattern.matcher(str);
if (matcher.matches()) {
setLastUsedInputFormat(Format.HSB_PERCENTAGE);
try {
return new Color(HSBColorSpace.getInstance(),
new float[]{//
matcher.group(1) == null ? 0f : numberFormat.parse(matcher.group(1)).floatValue() / 360f, //
matcher.group(2) == null ? 1f : numberFormat.parse(matcher.group(2)).floatValue() / 100f, //
matcher.group(3) == null ? 1f : numberFormat.parse(matcher.group(3)).floatValue() / 100f},//
1f);
} catch (NumberFormatException nfe) {
ParseException pe = new ParseException(str, 0);
pe.initCause(nfe);
throw pe;
} catch (IllegalArgumentException iae) {
ParseException pe = new ParseException(str, 0);
pe.initCause(iae);
throw pe;
}
}
// Format GRAY_PERCENTAGE
matcher = grayPercentagePattern.matcher(str);
if (matcher.matches()) {
setLastUsedInputFormat(Format.GRAY_PERCENTAGE);
try {
return ColorUtil.toColor(ColorSpace.getInstance(ColorSpace.CS_GRAY),
new float[]{//
matcher.group(1) == null ? 0f : numberFormat.parse(matcher.group(1)).floatValue() / 100f}//
);
} catch (NumberFormatException nfe) {
ParseException pe = new ParseException(str, 0);
pe.initCause(nfe);
throw pe;
} catch (IllegalArgumentException iae) {
ParseException pe = new ParseException(str, 0);
pe.initCause(iae);
throw pe;
}
}
throw new ParseException(str, 0);
}
@Override
public String valueToString(Object value) throws ParseException {
String str = null;
if (value == null) {
if (allowsNullValue) {
str = "";
} else {
throw new ParseException("Null value is not allowed.", 0);
}
} else {
if (!(value instanceof Color)) {
throw new ParseException("Value is not a color " + value, 0);
}
Color c = (Color) value;
Format f = outputFormat;
if (isAdaptive) {
switch (c.getColorSpace().getType()) {
case ColorSpace.TYPE_HSV:
f = Format.HSB_PERCENTAGE;
break;
case ColorSpace.TYPE_GRAY:
f = Format.GRAY_PERCENTAGE;
break;
case ColorSpace.TYPE_RGB:
default:
f = Format.RGB_INTEGER_SHORT;
}
}
switch (f) {
case RGB_HEX:
str = "000000" + Integer.toHexString(c.getRGB() & 0xffffff);
str = "#" + str.substring(str.length() - 6);
break;
case RGB_INTEGER_SHORT:
str = c.getRed() + " " + c.getGreen() + " " + c.getBlue();
break;
case RGB_INTEGER:
str = "rgb " + c.getRed() + " " + c.getGreen() + " " + c.getBlue();
break;
case RGB_PERCENTAGE:
str = "rgb% " + numberFormat.format(c.getRed() / 255f) + " " + numberFormat.format(c.getGreen() / 255f) + " " + numberFormat.format(c.getBlue() / 255f) + "";
break;
case HSB_PERCENTAGE: {
float[] components;
if (c.getColorSpace().getType()==ColorSpace.TYPE_HSV) {
components = c.getComponents(null);
} else {
components = Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), new float[3]);
}
str = "hsb " + numberFormat.format(components[0] * 360) + " "//
+ numberFormat.format(components[1] * 100) + " " //
+ numberFormat.format(components[2] * 100) + "";
break;
}
case GRAY_PERCENTAGE: {
float[] components;
if (c.getColorSpace().getType()==ColorSpace.TYPE_GRAY) {
components = c.getComponents(null);
} else {
components = c.getColorComponents(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);
}
str = "gray " + numberFormat.format(components[0] * 100) + "";
break;
}
}
}
return str;
}
/**
* Convenience method for creating a formatter factory with a
* {@code ColorFormatter}.
* Uses the RGB_INTEGER_SHORT format, allows null values and is adaptive.
*/
public static AbstractFormatterFactory createFormatterFactory() {
return createFormatterFactory(Format.RGB_INTEGER_SHORT, true, true);
}
/**
* Convenience method for creating a formatter factory with a
* 8@code ColorFormatter}.
*/
public static AbstractFormatterFactory createFormatterFactory(Format outputFormat, boolean allowsNullValue, boolean isAdaptive) {
return new DefaultFormatterFactory(new ColorFormatter(outputFormat, allowsNullValue, isAdaptive));
}
}