/* * Copyright (c) 2009 Kathryn Huxtable and Kenneth Orr. * * This file is part of the SeaGlass Pluggable Look and Feel. * * Licensed 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. * * $Id$ */ package com.seaglasslookandfeel.painter.util; import java.awt.Shape; import java.awt.geom.Ellipse2D; import java.awt.geom.Path2D; /** * Return various shapes used by the Painter classes. * * @author Kathryn Huxtable */ public class ShapeGenerator { /** * The base radius (arc size) for most control's borders. This is used to * calculate the rest of the arc sizes in a relative manner. */ private static final double baseRadius = 4d; /** * The style of a particular corner. */ public enum CornerStyle { /** Make a square corner. */ SQUARE, /** Make a rounded corner. */ ROUNDED, } /** * The rounding amount for a corner. */ public enum CornerSize { /** * Round using half the height, producing a nice quarter of a circle for * the entire quarter of the rectangle. Use for horizontal lozenges that * are filled. */ ROUND_HEIGHT(0), /** * Round using half the height plus 1, producing a nice quarter of a * circle for the entire quarter of the rectangle. Use for horizontal * lozenges that are drawn. */ ROUND_HEIGHT_DRAW(0), /** * Round using half the width, producing a nice quarter of a circle for * the entire quarter of the rectangle. Use for vertical lozenges that * are filled. */ ROUND_WIDTH(0), /** * Round using half the width plus 1, producing a nice quarter of a * circle for the entire quarter of the rectangle. Use for vertical * lozenges that are drawn. */ ROUND_WIDTH_DRAW(0), /** Round for a generic object's interior. */ INTERIOR(baseRadius - 1), /** Round for a generic object's border. */ BORDER(baseRadius), /** Round for a generic object's inner focus ring. */ INNER_FOCUS(baseRadius + 1), /** Round for a generic object's outer focus ring. */ OUTER_FOCUS(baseRadius + 2), /** Round for a slider thumb's interior. */ SLIDER_INTERIOR(baseRadius - 2), /** Round for a slider thumb's border. */ SLIDER_BORDER(baseRadius - 1), /** Round for a slider thumb's inner focus ring. */ SLIDER_INNER_FOCUS(baseRadius), /** Round for a slider thumb's outer focus ring. */ SLIDER_OUTER_FOCUS(baseRadius + 1), /** Round for a check box's interior. */ CHECKBOX_INTERIOR(baseRadius / 2), /** Round for a check box's border. */ CHECKBOX_BORDER((baseRadius + 1) / 2), /** Round for a check box's inner focus ring. */ CHECKBOX_INNER_FOCUS((baseRadius + 2) / 2), /** Round for a check box's outer focus ring. */ CHECKBOX_OUTER_FOCUS((baseRadius + 3) / 2), /** Round for a popup menu's border. */ POPUP_BORDER(3), /** Round for a popup menu's interior. */ POPUP_INTERIOR(2.5), /** Round for a frame's border. */ FRAME_BORDER(baseRadius + 1), /** Round for a frame's inner highlight. */ FRAME_INNER_HIGHLIGHT(baseRadius), /** Round for a frame's interior. */ FRAME_INTERIOR(baseRadius - 1); /** The rounding radius. */ private double radius; /** * Create the corner size. * * @param radius the radius for rounding. */ CornerSize(double radius) { this.radius = radius; } /** * Return the rounding radius. Note that the {@code ROUND*} values are * handled specially, as the rounding is dependent on the size of the * rectangle. * * @param w the width of the rectangle. * @param h the height of the rectangle. * * @return the radius (arc size) to use for rounding. */ public double getRadius(int w, int h) { switch (this) { case ROUND_HEIGHT: return h / 2.0; case ROUND_HEIGHT_DRAW: return (h + 1) / 2.0; case ROUND_WIDTH: return w / 2.0; case ROUND_WIDTH_DRAW: return (w + 1) / 2.0; default: return radius; } } } /** Used for generic shapes. */ private Path2D path = new Path2D.Double(Path2D.WIND_EVEN_ODD); /** Used for simple elliptical or circular shapes. */ private Ellipse2D ellipse = new Ellipse2D.Float(); /** * Return a path for a rectangle with square corners. * * @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 w the width of the rectangle * @param h the height of the rectangle * * @return a path representing the shape. */ public Shape createRectangle(final int x, final int y, final int w, final int h) { return createRoundRectangleInternal(x, y, w, h, 0, CornerStyle.SQUARE, CornerStyle.SQUARE, CornerStyle.SQUARE, CornerStyle.SQUARE); } /** * Return a path for a rectangle with rounded corners. * * @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 w the width of the rectangle * @param h the height of the rectangle * @param size the CornerSize value representing the amount of rounding * * @return a path representing the shape. */ public Shape createRoundRectangle(final int x, final int y, final int w, final int h, final CornerSize size) { return createRoundRectangle(x, y, w, h, size, CornerStyle.ROUNDED, CornerStyle.ROUNDED, CornerStyle.ROUNDED, CornerStyle.ROUNDED); } /** * Return a path for a rectangle with optionally rounded corners. * * @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 w the width of the rectangle * @param h the height of the rectangle * @param size the CornerSize value representing the amount of * rounding * @param topLeft the CornerStyle of the upper-left corner. This must * be one of {@code CornerStyle.SQUARE} or * {@code CornerStyle.ROUNDED}. * @param bottomLeft the CornerStyle of the lower-left corner. This must * be one of {@code CornerStyle.SQUARE} or * {@code CornerStyle.ROUNDED}. * @param bottomRight the CornerStyle of the lower-right corner. This must * be one of {@code CornerStyle.SQUARE} or * {@code CornerStyle.ROUNDED}. * @param topRight the CornerStyle of the upper-right corner. This must * be one of {@code CornerStyle.SQUARE} or * {@code CornerStyle.ROUNDED}. * * @return a path representing the shape. */ public Shape createRoundRectangle(final int x, final int y, final int w, final int h, final CornerSize size, final CornerStyle topLeft, final CornerStyle bottomLeft, final CornerStyle bottomRight, final CornerStyle topRight) { return createRoundRectangleInternal(x, y, w, h, size.getRadius(w, h), topLeft, bottomLeft, bottomRight, topRight); } /** * Return a path for a rectangle with square corners and no right side. This * is used for text fields that are part of a larger control, which is * placed to their left, e.g. spinners and editable combo boxes. * * <p>This path is suitable for drawing, not for filling.</p> * * @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 w the width of the rectangle * @param h the height of the rectangle * * @return a path representing the shape. */ public Shape createOpenRectangle(final int x, final int y, final int w, final int h) { path.reset(); path.moveTo(x + w, y); path.lineTo(x, y); path.lineTo(x, y + h); path.lineTo(x + w, y + h); return path; } /** * Return a path for a check mark. * * @param x the X coordinate of the upper-left corner of the check mark * @param y the Y coordinate of the upper-left corner of the check mark * @param w the width of the check mark * @param h the height of the check mark * * @return a path representing the shape. */ public Shape createCheckMark(final int x, final int y, final int w, final int h) { double xf = w / 12.0; double hf = h / 12.0; path.reset(); path.moveTo(x, y + 7.0 * hf); path.lineTo(x + 2.0 * xf, y + 7.0 * hf); path.lineTo(x + 4.75 * xf, y + 10.0 * hf); path.lineTo(x + 9.0 * xf, y); path.lineTo(x + 11.0 * xf, y); path.lineTo(x + 5.0 * xf, y + 12.0 * hf); path.closePath(); return path; } /** * Return a path for an arrow pointing to the left. * * @param x the X coordinate of the upper-left corner of the arrow * @param y the Y coordinate of the upper-left corner of the arrow * @param w the width of the arrow * @param h the height of the arrow * * @return a path representing the shape. */ public Shape createArrowLeft(final double x, final double y, final double w, final double h) { path.reset(); path.moveTo(x + w, y); path.lineTo(x, y + h / 2.0); path.lineTo(x + w, y + h); path.closePath(); return path; } /** * Return a path for an arrow pointing to the right. * * @param x the X coordinate of the upper-left corner of the arrow * @param y the Y coordinate of the upper-left corner of the arrow * @param w the width of the arrow * @param h the height of the arrow * * @return a path representing the shape. */ public Shape createArrowRight(final double x, final double y, final double w, final double h) { path.reset(); path.moveTo(x, y); path.lineTo(x + w, y + h / 2); path.lineTo(x, y + h); path.closePath(); return path; } /** * Return a path for an arrow pointing up. * * @param x the X coordinate of the upper-left corner of the arrow * @param y the Y coordinate of the upper-left corner of the arrow * @param w the width of the arrow * @param h the height of the arrow * * @return a path representing the shape. */ public Shape createArrowUp(final double x, final double y, final double w, final double h) { path.reset(); path.moveTo(x, y + h); path.lineTo(x + w / 2, y); path.lineTo(x + w, y + h); path.closePath(); return path; } /** * Return a path for an arrow pointing down. * * @param x the X coordinate of the upper-left corner of the arrow * @param y the Y coordinate of the upper-left corner of the arrow * @param w the width of the arrow * @param h the height of the arrow * * @return a path representing the shape. */ public Shape createArrowDown(final double x, final double y, final double w, final double h) { path.reset(); path.moveTo(x, y); path.lineTo(x + w / 2, y + h); path.lineTo(x + w, y); path.closePath(); return path; } /** * Return a path for the patterned portion of an indeterminate progress bar. * * @param x the X coordinate of the upper-left corner of the region * @param y the Y coordinate of the upper-left corner of the region * @param w the width of the region * @param h the height of the region * * @return a path representing the shape. */ public Shape createProgressBarIndeterminatePattern(int x, int y, int w, int h) { final double wHalf = w / 2.0; final double xOffset = 5; path.reset(); path.moveTo(xOffset, 0); path.lineTo(xOffset+wHalf, 0); path.curveTo(xOffset+wHalf-5, h/2-4, xOffset+wHalf+5, h/2+4, xOffset+wHalf, h); path.lineTo(xOffset, h); path.curveTo(xOffset+5, h/2+4, xOffset-5, h/2-4, xOffset, 0); path.closePath(); return path; } /** * Return a path for a rounded internal drop shadow. This is used for * progress bar tracks and search fields. * * @param x the X coordinate of the upper-left corner of the shadow * @param y the Y coordinate of the upper-left corner of the shadow * @param w the width of the shadow * @param h the height of the rectangle * * @return a path representing the shadow. */ public Shape createInternalDropShadowRounded(final int x, final int y, final int w, final int h) { final double radius = h / 2; final int right = x + w; final double bottom = y + radius; path.reset(); // Upper edge. path.moveTo(x, bottom); path.quadTo(x, y, x + radius, y); path.lineTo(right - radius, y); path.quadTo(right, y, right, bottom); // Lower edge. path.lineTo(right - 1, bottom); path.quadTo(right - 2, y + 2, right - radius - 1, y + 2); path.lineTo(x + radius + 1, y + 2); path.quadTo(x + 2, y + 2, x + 1, bottom); path.closePath(); return path; } /** * Return a path for a focus rectangle. * * <p>This path is suitable for filling.</p> * * @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 w the width of the rectangle * @param h the height of the rectangle * * @return a path representing the shape. */ public Shape createFillableFocusRectangle(int x, int y, int w, int h) { final int left = x; final int top = y; final int right = x + w; final int bottom = y + h; path.reset(); path.moveTo(left, top); path.lineTo(left, bottom); path.lineTo(right, bottom); path.lineTo(right, top); final float offset = 1.4f; final float left2 = left + offset; final float top2 = top + offset; final float right2 = right - offset; final float bottom2 = bottom - offset; // TODO These two lines were curveTo in Nimbus. Perhaps we should // revisit this? path.lineTo(right2, top); path.lineTo(right2, bottom2); path.lineTo(left2, bottom2); path.lineTo(left2, top2); path.lineTo(right2, top2); path.lineTo(right2, top); path.closePath(); return path; } /** * Return a path for a simple bullet. * * @param x the X coordinate of the upper-left corner of the bullet * @param y the Y coordinate of the upper-left corner of the bullet * @param diameter the diameter of the bullet * * @return a path representing the shape. */ public Shape createBullet(int x, int y, int diameter) { return createEllipseInternal(x, y, diameter, diameter); } /** * Return a path for a radio button's concentric sections. * * @param x the X coordinate of the upper-left corner of the section * @param y the Y coordinate of the upper-left corner of the section * @param diameter the diameter of the section * * @return a path representing the shape. */ public Shape createRadioButton(int x, int y, int diameter) { return createEllipseInternal(x, y, diameter, diameter); } /** * Return a path for a continuous slider thumb's concentric sections. * * @param x the X coordinate of the upper-left corner of the section * @param y the Y coordinate of the upper-left corner of the section * @param diameter the diameter of the section * * @return a path representing the shape. */ public Shape createSliderThumbContinuous(final int x, final int y, final int diameter) { return createEllipseInternal(x, y, diameter, diameter); } /** * Return a path for a discrete slider thumb's concentric sections. * * @param x the X coordinate of the upper-left corner of the section * @param y the Y coordinate of the upper-left corner of the section * @param w the width of the section * @param h the height of the section * @param size the CornerSize representing the rounding amount for the * section * * @return a path representing the shape. */ public Shape createSliderThumbDiscrete(final int x, final int y, final int w, final int h, final CornerSize size) { final double topArc = size.getRadius(w, h); final double bottomArcH = size == CornerSize.INTERIOR ? 0 : 1; final double bottomArcW = 3; path.reset(); path.moveTo(x, y + topArc); path.quadTo(x, y, x + topArc, y); path.lineTo(x + w - topArc, y); path.quadTo(x + w, y, x + w, y + topArc); path.lineTo(x + w, y + h / 2.0); path.quadTo(x + w - bottomArcW, y + h - bottomArcH, x + w / 2.0, y + h); path.quadTo(x + bottomArcW, y + h - bottomArcH, x, y + h / 2.0); path.closePath(); return path; } /** * Return a path for a "cancel" icon. This is a circle with a punched out * "x" in it. * * @param x the X coordinate of the upper-left corner of the icon * @param y the Y coordinate of the upper-left corner of the icon * @param w the width of the icon * @param h the height of the icon * * @return a path representing the shape. */ public Shape createCancelIcon(int x, int y, int w, int h) { final double xMid = x + w / 2.0; final double yMid = y + h / 2.0; // Draw the circle. path.reset(); path.moveTo(xMid, y); path.quadTo(x, y, x, yMid); path.quadTo(x, y + h, xMid, y + h); path.quadTo(x + w, y + h, x + w, yMid); path.quadTo(x + w, y, xMid, y); path.closePath(); final double xOffsetL = w / 2.0 - 3; final double xOffsetS = w / 2.0 - 4; final double yOffsetL = h / 2.0 - 3; final double yOffsetS = h / 2.0 - 4; final double offsetC = 1.5; // Erase the "x" with an inner subpath. path.moveTo(xMid, yMid - offsetC); path.lineTo(xMid + xOffsetS, yMid - yOffsetL); path.lineTo(yMid + xOffsetL, yMid - yOffsetS); path.lineTo(xMid + offsetC, yMid); path.lineTo(xMid + xOffsetL, yMid + yOffsetS); path.lineTo(xMid + xOffsetS, yMid + yOffsetL); path.lineTo(xMid, yMid + offsetC); path.lineTo(xMid - xOffsetS, yMid + yOffsetL); path.lineTo(xMid - xOffsetL, yMid + yOffsetS); path.lineTo(xMid - offsetC, yMid); path.lineTo(xMid - xOffsetL, yMid - yOffsetS); path.lineTo(xMid - xOffsetS, yMid - yOffsetL); path.closePath(); return path; } /** * Return a path for a "cancel" icon. This is a circle with a punched out * "x" in it. * * @param x the X coordinate of the upper-left corner of the icon * @param y the Y coordinate of the upper-left corner of the icon * @param w the width of the icon * @param h the height of the icon * * @return a path representing the shape. */ public Shape createTabCloseIcon(int x, int y, int w, int h) { final double xMid = x + w / 2.0; final double yMid = y + h / 2.0; path.reset(); final double xOffsetL = w / 2.0; final double xOffsetS = w / 2.0 - 1; final double yOffsetL = h / 2.0; final double yOffsetS = h / 2.0 - 1; final double offsetC = 1; path.moveTo(xMid, yMid - offsetC); path.lineTo(xMid + xOffsetS, yMid - yOffsetL); path.lineTo(yMid + xOffsetL, yMid - yOffsetS); path.lineTo(xMid + offsetC, yMid); path.lineTo(xMid + xOffsetL, yMid + yOffsetS); path.lineTo(xMid + xOffsetS, yMid + yOffsetL); path.lineTo(xMid, yMid + offsetC); path.lineTo(xMid - xOffsetS, yMid + yOffsetL); path.lineTo(xMid - xOffsetL, yMid + yOffsetS); path.lineTo(xMid - offsetC, yMid); path.lineTo(xMid - xOffsetL, yMid - yOffsetS); path.lineTo(xMid - xOffsetS, yMid - yOffsetL); path.closePath(); return path; } /** * Return a path for a scroll bar cap. This is used when the buttons are * placed together at the opposite end of the scroll bar. * * @param x the X coordinate of the upper-left corner of the cap * @param y the Y coordinate of the upper-left corner of the cap * @param w the width of the cap * @param h the height of the cap * * @return a path representing the shape. */ public Shape createScrollCap(int x, int y, int w, int h) { path.reset(); path.moveTo(x, y); path.lineTo(x, y + h); path.lineTo(x + w, y + h); addScrollGapPath(x, y, w, h, true); path.closePath(); return path; } /** * Return a path for a scroll bar button. This is used when the buttons are * placed apart at opposite ends of the scroll bar. This is a common shape * that is transformed to the appropriate button. * * @param x the X coordinate of the upper-left corner of the button * @param y the Y coordinate of the upper-left corner of the button * @param w the width of the button * @param h the height of the button * * @return a path representing the shape. */ public Shape createScrollButtonApart(int x, int y, int w, int h) { path.reset(); path.moveTo(x, y); path.lineTo(x, y + h); path.lineTo(x + w, y + h); addScrollGapPath(x, y, w, h, true); path.closePath(); return path; } /** * Return a path for a scroll bar decrease button. This is used when the * buttons are placed together at one end of the scroll bar. * * @param x the X coordinate of the upper-left corner of the button * @param y the Y coordinate of the upper-left corner of the button * @param w the width of the button * @param h the height of the button * * @return a path representing the shape. */ public Shape createScrollButtonTogetherDecrease(int x, int y, int w, int h) { path.reset(); path.moveTo(x + w, y); path.lineTo(x + w, y + h); path.lineTo(x, y + h); addScrollGapPath(x, y, w, h, false); path.closePath(); return path; } /** * Return a path for a scroll bar increase button. This is used when the * buttons are placed together at one end of the scroll bar. * * @param x the X coordinate of the upper-left corner of the button * @param y the Y coordinate of the upper-left corner of the button * @param w the width of the button * @param h the height of the button * * @return a path representing the shape. */ public Shape createScrollButtonTogetherIncrease(int x, int y, int w, int h) { return createRectangle(x, y, w, h); } /** * Adds a hemispherical section to the current path. This is used to create * the gap in a scroll bar button or cap into which the scroll bar thumb * will fit. * * @param x the X coordinate of the upper-left corner of the button * or cap * @param y the Y coordinate of the upper-left corner of the button * or cap * @param w the width of the button or cap * @param h the height of the button or cap * @param isAtLeft {@code true} if the gap is at the left end of the button, * {@code false} if it is at the right. */ private void addScrollGapPath(int x, int y, int w, int h, boolean isAtLeft) { final double hHalf = h / 2.0; final double wFull = isAtLeft ? w : 0; final double wHalfOff = isAtLeft ? w - hHalf : hHalf; path.quadTo(x + wHalfOff, y + h, x + wHalfOff, y + hHalf); path.quadTo(x + wHalfOff, y, x + wFull, y); } /** * Return a path for an ellipse. * * @param x the X coordinate of the upper-left corner of the ellipse * @param y the Y coordinate of the upper-left corner of the ellipse * @param w the width of the ellipse * @param h the height of the ellipse * * @return a path representing the shape. */ private Shape createEllipseInternal(int x, int y, int w, int h) { ellipse.setFrame(x, y, w, h); return ellipse; } /** * Return a path for a rectangle with optionally rounded corners. * * @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 w the width of the rectangle * @param h the height of the rectangle * @param radius the radius (arc size) used for rounding * @param topLeft the CornerStyle of the upper-left corner. This must * be one of {@code CornerStyle.SQUARE} or * {@code CornerStyle.ROUNDED}. * @param bottomLeft the CornerStyle of the lower-left corner. This must * be one of {@code CornerStyle.SQUARE} or * {@code CornerStyle.ROUNDED}. * @param bottomRight the CornerStyle of the lower-right corner. This must * be one of {@code CornerStyle.SQUARE} or * {@code CornerStyle.ROUNDED}. * @param topRight the CornerStyle of the upper-right corner. This must * be one of {@code CornerStyle.SQUARE} or * {@code CornerStyle.ROUNDED}. * * @return a path representing the shape. */ private Shape createRoundRectangleInternal(final int x, final int y, final int w, final int h, final double radius, final CornerStyle topLeft, final CornerStyle bottomLeft, final CornerStyle bottomRight, final CornerStyle topRight) { // Convenience variables. final int left = x; final int top = y; final int right = x + w; final int bottom = y + h; // Start the path. path.reset(); // Move to top left and draw rounded corner if requested. switch (topLeft) { case SQUARE: path.moveTo(left, top); break; case ROUNDED: path.moveTo(left + radius, top); path.quadTo(left, top, left, top + radius); break; } // Draw through bottom left corner. switch (bottomLeft) { case SQUARE: path.lineTo(left, bottom); break; case ROUNDED: path.lineTo(left, bottom - radius); path.quadTo(left, bottom, left + radius, bottom); break; } // Draw through bottom right corner. switch (bottomRight) { case SQUARE: path.lineTo(right, bottom); break; case ROUNDED: path.lineTo(right - radius, bottom); path.quadTo(right, bottom, right, bottom - radius); } // Draw through top right corner. switch (topRight) { case SQUARE: path.lineTo(right, top); break; case ROUNDED: path.lineTo(right, top + radius); path.quadTo(right, top, right - radius, top); break; } // Close the path. path.closePath(); return path; } }