blob: 2abbfd4b63513c07e744683a43010371f7795972 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pivot.wtk;
import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.LinearGradientPaint;
import java.awt.Paint;
import java.awt.RadialGradientPaint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.TextHitInfo;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.text.AttributedCharacterIterator;
import java.util.Locale;
import org.apache.pivot.collections.Dictionary;
import org.apache.pivot.collections.List;
import org.apache.pivot.json.JSON;
import org.apache.pivot.json.JSONSerializer;
import org.apache.pivot.serialization.SerializationException;
import org.apache.pivot.util.Utils;
/**
* Contains utility methods dealing with the Java2D API.
*/
public final class GraphicsUtilities {
/**
* Enumeration representing a paint type.
*/
public enum PaintType {
/** A solid color value. */
SOLID_COLOR,
/** A gradient that proceeds smoothly from the starting (X,Y) to the ending (X,Y) positions. */
GRADIENT,
/** A gradient that proceeds smoothly from color to color along the line from start to end position. */
LINEAR_GRADIENT,
/** A gradient that proceeds from color to color starting from a central position out to a given radius. */
RADIAL_GRADIENT
}
public static final String PAINT_TYPE_KEY = "paintType";
public static final String COLOR_KEY = "color";
public static final String START_X_KEY = "startX";
public static final String START_Y_KEY = "startY";
public static final String END_X_KEY = "endX";
public static final String END_Y_KEY = "endY";
public static final String START_COLOR_KEY = "startColor";
public static final String END_COLOR_KEY = "endColor";
public static final String CENTER_X_KEY = "centerX";
public static final String CENTER_Y_KEY = "centerY";
public static final String RADIUS_KEY = "radius";
public static final String STOPS_KEY = "stops";
public static final String OFFSET_KEY = "offset";
/** A bit mask for 8 bits (max size of a color value). */
private static final int EIGHT_BITS = 0xff;
/** A scale factor used to divide an 8-bit color value by to get a fraction from 0.0 to 1.0. */
private static final float EIGHT_BIT_FLOAT = 255f;
/** The default thickness of lines and borders. */
private static final int DEFAULT_THICKNESS = 1;
/** Number of digits in a hex RGB color value. */
private static final int RGB_DIGIT_LENGTH = 6;
/** Number of digits in a hex full color value (including alpha). */
private static final int FULL_DIGIT_LENGTH = RGB_DIGIT_LENGTH + 2;
/** Radix for hex digit values. */
private static final int HEX_RADIX = 16;
/** Shift value for 16 bits (or two color values). */
private static final int TWO_BYTES = 16;
/** Shift value for 8 bits (or one color value). */
private static final int ONE_BYTE = 8;
/** Utility classes should not have public constructors. */
private GraphicsUtilities() {
}
/**
* Set anti-aliasing on for the given graphics context.
*
* @param graphics The 2D graphics context to set the attribute for.
*/
public static void setAntialiasingOn(final Graphics2D graphics) {
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
}
/**
* Set anti-aliasing of for the given graphics context.
*
* @param graphics The 2D graphics context to clear the attribute for.
*/
public static void setAntialiasingOff(final Graphics2D graphics) {
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF);
}
/**
* Draw a straight line in the given graphics context from a start position for a given
* length along the given horizontal or vertical direction with default thickness.
*
* @param graphics The graphics context to draw in.
* @param x Starting X position.
* @param y Starting Y position.
* @param length Length along the desired direction.
* @param orientation Whether the line is vertical or horizontal for the given length.
* @see #drawLine(Graphics2D, int, int, int, Orientation, int)
*/
public static void drawLine(final Graphics2D graphics, final int x, final int y,
final int length, final Orientation orientation) {
drawLine(graphics, x, y, length, orientation, DEFAULT_THICKNESS);
}
/**
* Draw a straight line in the given graphics context from a start position for a given
* length along the given horizontal or vertical direction with given thickness.
*
* @param graphics The graphics context to draw in.
* @param x Starting X position.
* @param y Starting Y position.
* @param length Length along the desired direction.
* @param orientation Whether the line is vertical or horizontal for the given length.
* @param thickness The pixel thickness of the line.
* @see #drawLine(Graphics2D, int, int, int, Orientation)
*/
public static void drawLine(final Graphics2D graphics, final int x, final int y,
final int length, final Orientation orientation, final int thickness) {
if (length > 0 && thickness > 0) {
switch (orientation) {
case HORIZONTAL:
graphics.fillRect(x, y, length, thickness);
break;
case VERTICAL:
graphics.fillRect(x, y, thickness, length);
break;
default:
throw new UnsupportedOperationException("Unknown orientation " + orientation);
}
}
}
/**
* Draws a rectangle with a thickness of one pixel at the specified
* coordinates whose <u>outer border</u> is the specified width and height.
* In other words, the distance from the left edge of the leftmost pixel to
* the left edge of the rightmost pixel is <tt>width - 1</tt>. <p> This
* method provides more reliable pixel rounding behavior than
* <tt>java.awt.Graphics#drawRect</tt> when scaling is applied because this
* method does not stroke the shape but instead explicitly fills the desired
* pixels with the graphics context's paint. For this reason, and because
* Pivot supports scaling the display host, it is recommended that skins use
* this method over <tt>java.awt.Graphics#drawRect</tt>.
*
* @param graphics The graphics context that will be used to perform the operation.
* @param x The x-coordinate of the upper-left corner of the rectangle.
* @param y The y-coordinate of the upper-left corner of the rectangle.
* @param width The <i>outer width</i> of the rectangle.
* @param height The <i>outer height</i> of the rectangle.
* @see #drawRect(Graphics2D, int, int, int, int, int)
*/
public static void drawRect(final Graphics2D graphics, final int x, final int y,
final int width, final int height) {
drawRect(graphics, x, y, width, height, DEFAULT_THICKNESS);
}
/**
* Draws a rectangle with the specified thickness at the specified
* coordinates whose <u>outer border</u> is the specified width and height.
* In other words, the distance from the left edge of the leftmost pixel to
* the left edge of the rightmost pixel is <tt>width - thickness</tt>. <p>
* This method provides more reliable pixel rounding behavior than
* <tt>java.awt.Graphics#drawRect</tt> when scaling is applied because this
* method does not stroke the shape but instead explicitly fills the desired
* pixels with the graphics context's paint. For this reason, and because
* Pivot supports scaling the display host, it is recommended that skins use
* this method over <tt>java.awt.Graphics#drawRect</tt>.
*
* @param graphics The graphics context that will be used to perform the operation.
* @param x The x-coordinate of the upper-left corner of the rectangle.
* @param y The y-coordinate of the upper-left corner of the rectangle.
* @param width The <i>outer width</i> of the rectangle.
* @param height The <i>outer height</i> of the rectangle.
* @param thickness The thickness of each edge.
* @see #drawRect(Graphics2D, int, int, int, int)
*/
public static void drawRect(final Graphics2D graphics, final int x, final int y,
final int width, final int height, final int thickness) {
Graphics2D rectGraphics = graphics;
if ((graphics.getTransform().getType() & AffineTransform.TYPE_MASK_SCALE) != 0) {
rectGraphics = (Graphics2D) graphics.create();
setAntialiasingOn(rectGraphics);
}
if (width > 0 && height > 0 && thickness > 0) {
drawLine(rectGraphics, x, y, width, Orientation.HORIZONTAL, thickness);
drawLine(rectGraphics, x + width - thickness, y, height, Orientation.VERTICAL, thickness);
drawLine(rectGraphics, x, y + height - thickness, width, Orientation.HORIZONTAL, thickness);
drawLine(rectGraphics, x, y, height, Orientation.VERTICAL, thickness);
}
if (rectGraphics != graphics) {
rectGraphics.dispose();
}
}
/**
* Interprets a string as a color value.
*
* @param value One of the following forms:
* <ul>
* <li>0xdddddddd - 8 hexadecimal digits, specifying 8 bits each of red,
* green, and blue, followed by 8 bits of alpha.</li>
* <li>#dddddd - 6 hexadecimal digits, specifying 8 bits each of red,
* green, and blue.</li>
* <li>Any of the names of the static colors in the Java {@link Color} class.</li>
* <li>Any of the CSS3/X11 color names from here:
* <a href="http://www.w3.org/TR/css3-color/">http://www.w3.org/TR/css3-color/</a>
* (except the Java color values will be used for the standard Java names).</li>
* <li>null - case-insensitive</li>
* </ul>
* @param argument A name for this color value (for the exception if it can't be decoded).
* @return A {@link Color} on successful decoding, which could be {@code null} for an input
* of {@code "null"}.
* @throws NumberFormatException if the value in the first two cases
* contains illegal hexadecimal digits.
* @throws IllegalArgumentException if the value is not in one of the
* formats listed above.
* @see CSSColor
*/
public static Color decodeColor(final String value, final String argument) throws NumberFormatException {
Utils.checkNullOrEmpty(value, argument == null ? "color" : argument);
Color color = null;
if (value.startsWith("0x") || value.startsWith("0X")) {
String digits = value.substring(2);
if (digits.length() != FULL_DIGIT_LENGTH) {
throw new IllegalArgumentException(
"Incorrect Color format. Expecting exactly " + FULL_DIGIT_LENGTH
+ " digits after the '0x' prefix.");
}
int rgb = Integer.parseInt(digits.substring(0, RGB_DIGIT_LENGTH), HEX_RADIX);
float alpha = Integer.parseInt(digits.substring(RGB_DIGIT_LENGTH, FULL_DIGIT_LENGTH), HEX_RADIX)
/ EIGHT_BIT_FLOAT;
color = getColor(rgb, alpha);
} else if (value.startsWith("#")) {
String digits = value.substring(1);
if (digits.length() != RGB_DIGIT_LENGTH) {
throw new IllegalArgumentException(
"Incorrect Color format. Expecting exactly " + RGB_DIGIT_LENGTH
+ " digits after the '#' prefix.");
}
int rgb = Integer.parseInt(digits, HEX_RADIX);
float alpha = 1.0f;
color = getColor(rgb, alpha);
} else if (!value.equalsIgnoreCase("null")) {
// PIVOT-985: new fix: use the new CSSColor lookup for the name, which
// has the spelling variants already included in the X11/CSS3 list, as well
// as all the standard Java Color names, so we can do this without doing
// the expensive reflection on the Color class. The lookup here is case-insensitive.
// This method will throw if the name isn't valid.
color = CSSColor.fromString(value).getColor();
}
return color;
}
/**
* Interprets a string as a color value.
*
* @param value One of the following forms:
* <ul>
* <li>0xdddddddd - 8 hexadecimal digits, specifying 8 bits each of red,
* green, and blue, followed by 8 bits of alpha.</li>
* <li>#dddddd - 6 hexadecimal digits, specifying 8 bits each of red,
* green, and blue.</li>
* <li>Any of the names of the static colors in the Java {@link Color} class.</li>
* <li>Any of the CSS3/X11 color names from here:
* <a href="http://www.w3.org/TR/css3-color/">http://www.w3.org/TR/css3-color/</a>
* (except the Java color values will be used for the standard Java names).</li>
* <li>null - case-insensitive</li>
* </ul>
* @return A {@link Color} on successful decoding
* @throws NumberFormatException if the value in the first two cases
* contains illegal hexadecimal digits.
* @throws IllegalArgumentException if the value is not in one of the
* formats listed above.
* @see #decodeColor(String, String)
* @see CSSColor
*/
public static Color decodeColor(final String value) throws NumberFormatException {
return decodeColor(value, null);
}
/**
* Generate a full color value given the RGB value along with the alpha
* (opacity) value.
*
* @param rgb The 24-bit red, green, and blue value.
* @param alpha The opacity value (0.0 - 1.0).
* @return The full color value from these two parts.
*/
public static Color getColor(final int rgb, final float alpha) {
float red = ((rgb >> TWO_BYTES) & EIGHT_BITS) / EIGHT_BIT_FLOAT;
float green = ((rgb >> ONE_BYTE) & EIGHT_BITS) / EIGHT_BIT_FLOAT;
float blue = (rgb & EIGHT_BITS) / EIGHT_BIT_FLOAT;
return new Color(red, green, blue, alpha);
}
/**
* Interpret a string as a {@link Paint} value.
*
* @param value Either (a) One of the
* {@linkplain GraphicsUtilities#decodeColor color values recognized by
* Pivot} or (b) A {@linkplain GraphicsUtilities#decodePaint(Dictionary)
* JSON dictionary describing a Paint value}.
* @return The decoded paint value.
* @throws IllegalArgumentException if the given value is {@code null} or
* empty or there is a problem decoding the value.
*/
public static Paint decodePaint(final String value) {
Utils.checkNullOrEmpty(value, "paint");
Paint paint;
if (value.startsWith("#") || value.startsWith("0x") || value.startsWith("0X")) {
paint = decodeColor(value);
} else {
try {
paint = decodePaint(JSONSerializer.parseMap(value));
} catch (SerializationException exception) {
throw new IllegalArgumentException(exception);
}
}
return paint;
}
/**
* Interpret a dictionary as a {@link Paint} value.
*
* @param dictionary A dictionary containing a key {@value #PAINT_TYPE_KEY}
* and further elements according to its value:
* <ul>
* <li><b>solid_color</b> - key {@value #COLOR_KEY} with value being any of the
* {@linkplain GraphicsUtilities#decodeColor color values recognized by
* Pivot}</li>
* <li><b>gradient</b> - keys {@value #START_X_KEY}, {@value #START_Y_KEY},
* {@value #END_X_KEY}, {@value #END_Y_KEY} (values are coordinates),
* {@value #START_COLOR_KEY}, {@value #END_COLOR_KEY} (values are
* {@linkplain GraphicsUtilities#decodeColor colors})</li>
* <li><b>linear_gradient</b> - keys {@value #START_X_KEY},
* {@value #START_Y_KEY}, {@value #END_X_KEY}, {@value #END_Y_KEY}
* (coordinates), {@value #STOPS_KEY} (a list of dictionaries with keys
* {@value #OFFSET_KEY} (a number in [0,1]) and {@value #COLOR_KEY})</li>
* <li><b>radial_gradient</b> - keys {@value #CENTER_X_KEY},
* {@value #CENTER_Y_KEY} (coordinates), {@value #RADIUS_KEY} (a number),
* {@value #STOPS_KEY} (a list of dictionaries with keys
* {@value #OFFSET_KEY} and {@value #COLOR_KEY})</li>
* </ul>
* @return The fully decoded paint value.
* @throws IllegalArgumentException if there is no paint type key found.
*/
public static Paint decodePaint(final Dictionary<String, ?> dictionary) {
Utils.checkNull(dictionary, "paint dictionary");
String paintType = JSON.get(dictionary, PAINT_TYPE_KEY);
if (paintType == null) {
throw new IllegalArgumentException(PAINT_TYPE_KEY + " is required.");
}
Paint paint;
float startX, startY;
float endX, endY;
PaintType pType = PaintType.valueOf(paintType.toUpperCase(Locale.ENGLISH));
switch (pType) {
case SOLID_COLOR:
paint = decodeColor((String) JSON.get(dictionary, COLOR_KEY));
break;
case GRADIENT:
startX = JSON.getFloat(dictionary, START_X_KEY);
startY = JSON.getFloat(dictionary, START_Y_KEY);
endX = JSON.getFloat(dictionary, END_X_KEY);
endY = JSON.getFloat(dictionary, END_Y_KEY);
Color startColor = decodeColor((String) JSON.get(dictionary, START_COLOR_KEY));
Color endColor = decodeColor((String) JSON.get(dictionary, END_COLOR_KEY));
paint = new GradientPaint(startX, startY, startColor, endX, endY, endColor);
break;
case LINEAR_GRADIENT:
case RADIAL_GRADIENT:
@SuppressWarnings("unchecked")
List<Dictionary<String, ?>> stops = (List<Dictionary<String, ?>>) JSON.get(dictionary, STOPS_KEY);
int n = stops.getLength();
float[] fractions = new float[n];
Color[] colors = new Color[n];
for (int i = 0; i < n; i++) {
Dictionary<String, ?> stop = stops.get(i);
float offset = JSON.getFloat(stop, OFFSET_KEY);
fractions[i] = offset;
Color color = decodeColor((String) JSON.get(stop, COLOR_KEY));
colors[i] = color;
}
if (pType == PaintType.LINEAR_GRADIENT) {
startX = JSON.getFloat(dictionary, START_X_KEY);
startY = JSON.getFloat(dictionary, START_Y_KEY);
endX = JSON.getFloat(dictionary, END_X_KEY);
endY = JSON.getFloat(dictionary, END_Y_KEY);
paint = new LinearGradientPaint(startX, startY, endX, endY, fractions, colors);
} else {
float centerX = JSON.getFloat(dictionary, CENTER_X_KEY);
float centerY = JSON.getFloat(dictionary, CENTER_Y_KEY);
float radius = JSON.getFloat(dictionary, RADIUS_KEY);
paint = new RadialGradientPaint(centerX, centerY, radius, fractions, colors);
}
break;
default:
throw new UnsupportedOperationException("Paint type " + paintType + " is not supported.");
}
return paint;
}
/**
* Set the default font rendering hints for the given graphics object.
*
* @param graphics The graphics object to initialize.
* @param fontRenderContext The source of the font rendering hints.
*/
public static void setFontRenderingHints(final Graphics2D graphics, final FontRenderContext fontRenderContext) {
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
fontRenderContext.getAntiAliasingHint());
graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
fontRenderContext.getFractionalMetricsHint());
}
/**
* Set the context in the given graphics environment for subsequent font drawing.
*
* @param graphics The graphics context.
* @param fontRenderContext The font rendering context used to get the font drawing hints.
* @param font The font to use.
* @param color The foreground color for the text.
*/
public static void prepareForText(final Graphics2D graphics, final FontRenderContext fontRenderContext,
final Font font, final Color color) {
setFontRenderingHints(graphics, fontRenderContext);
graphics.setFont(font);
graphics.setColor(color);
}
/**
* Prepare for text rendering by getting the platform font render context and then setting the default
* rendering hints in the graphics object.
*
* @param graphics The graphics object to prepare.
* @return The {@link Platform#getFontRenderContext} value.
*/
public static FontRenderContext prepareForText(final Graphics2D graphics) {
FontRenderContext fontRenderContext = Platform.getFontRenderContext();
setFontRenderingHints(graphics, fontRenderContext);
return fontRenderContext;
}
/**
* Set the context in the given graphics environment for subsequent font drawing and return
* the font render context.
*
* @param graphics The graphics context.
* @param font The font to use.
* @param color The foreground color for the text.
* @return The font render context for the platform.
*/
public static FontRenderContext prepareForText(final Graphics2D graphics, final Font font, final Color color) {
FontRenderContext fontRenderContext = Platform.getFontRenderContext();
prepareForText(graphics, fontRenderContext, font, color);
return fontRenderContext;
}
/**
* Calculate the average character bounds for the given font.
* <p> This bounds is the width of the "missing glyph code" and
* the maximum character height of any glyph in the font.
*
* @param font The font in question.
* @return The bounding rectangle to use for the average character size.
* @see Platform#getFontRenderContext
*/
public static Dimensions getAverageCharacterSize(final Font font) {
int missingGlyphCode = font.getMissingGlyphCode();
FontRenderContext fontRenderContext = Platform.getFontRenderContext();
GlyphVector missingGlyphVector = font.createGlyphVector(fontRenderContext,
new int[] {missingGlyphCode});
Rectangle2D textBounds = missingGlyphVector.getLogicalBounds();
Rectangle2D maxCharBounds = font.getMaxCharBounds(fontRenderContext);
return new Dimensions(
(int) Math.ceil(textBounds.getWidth()),
(int) Math.ceil(maxCharBounds.getHeight())
);
}
/**
* Get a caret rectangle from the given attributed text.
*
* @param caret The location within the text where the caret should be located.
* @param text The attributed text iterator.
* @param leftOffset Horizontal offset within the control of the text (to add into the position).
* @param topOffset Same for vertical offset.
* @return The resulting rectangle for the caret.
*/
public static Rectangle getCaretRectangle(final TextHitInfo caret, final AttributedCharacterIterator text,
final int leftOffset, final int topOffset) {
FontRenderContext fontRenderContext = Platform.getFontRenderContext();
TextLayout layout = new TextLayout(text, fontRenderContext);
Shape caretShape = layout.getCaretShape(caret);
Rectangle caretRect = caretShape.getBounds();
caretRect.translate(leftOffset, topOffset + (int) Math.ceil(layout.getAscent() + layout.getDescent()));
return caretRect;
}
/**
* Draw borders around a rectangular area.
*
* @param graphics The graphics area to draw in.
* @param borders The borders specification.
* @param top The top coordinate (typically 0)
* @param left The left coordinate (typically 0)
* @param bottom The bottom interior coordinate (height - 1)
* @param right The right interior coordinate (width - 1)
*/
public static void drawBorders(final Graphics2D graphics, final Borders borders, final int top, final int left,
final int bottom, final int right) {
// The single line/object cases, or the first of the multiple line cases
switch (borders) {
default:
case NONE:
break;
case ALL:
graphics.drawRect(left, top, right, bottom);
break;
case TOP:
case TOP_BOTTOM:
// The top here
graphics.drawLine(left, top, right, top);
break;
case BOTTOM:
// The bottom here
graphics.drawLine(left, bottom, right, bottom);
break;
case LEFT:
case LEFT_RIGHT:
case LEFT_TOP:
case LEFT_BOTTOM:
case NOT_RIGHT:
case NOT_BOTTOM:
case NOT_TOP:
// The left here
graphics.drawLine(0, 0, 0, bottom);
break;
case RIGHT:
case RIGHT_TOP:
case RIGHT_BOTTOM:
case NOT_LEFT:
// The right here
graphics.drawLine(right, 0, right, bottom);
break;
}
// The second of the double/triple line cases
switch (borders) {
case LEFT_RIGHT:
case NOT_BOTTOM:
case NOT_TOP:
// The right side now
graphics.drawLine(right, top, right, bottom);
break;
case TOP_BOTTOM:
case LEFT_BOTTOM:
case RIGHT_BOTTOM:
case NOT_LEFT:
// The bottom now
graphics.drawLine(left, bottom, right, bottom);
break;
case LEFT_TOP:
case RIGHT_TOP:
case NOT_RIGHT:
// The top now
graphics.drawLine(left, top, right, top);
break;
default:
break;
}
// Now the third of the triple line cases
switch (borders) {
case NOT_RIGHT:
case NOT_TOP:
// The bottom now
graphics.drawLine(left, bottom, right, bottom);
break;
case NOT_LEFT:
case NOT_BOTTOM:
// The top now
graphics.drawLine(left, top, right, top);
break;
default:
break;
}
}
}