/* * 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; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.Insets; import java.awt.LinearGradientPaint; import java.awt.Paint; import java.awt.RadialGradientPaint; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Transparency; import java.awt.geom.Rectangle2D; import java.awt.image.VolatileImage; import java.awt.print.PrinterGraphics; import java.lang.reflect.Method; import javax.swing.JComponent; import javax.swing.JList; import javax.swing.JTable; import javax.swing.UIManager; import javax.swing.plaf.UIResource; import com.seaglasslookandfeel.SeaGlassLookAndFeel; import com.seaglasslookandfeel.painter.AbstractRegionPainter.PaintContext.CacheMode; import com.seaglasslookandfeel.painter.util.ShapeGenerator; import com.seaglasslookandfeel.state.ControlInToolBarState; import com.seaglasslookandfeel.state.State; import com.seaglasslookandfeel.util.ImageCache; import com.seaglasslookandfeel.util.SeaGlassGraphicsUtils; /** * Convenient base class for defining Painter instances for rendering a region * or component in Sea Glass. * * <p>Based on Nimbus's AbstractRegionPainter by Jasper Potts and Richard Bair. * This was package local.</p> * */ public abstract class AbstractRegionPainter implements SeaGlassPainter<JComponent> { private static final State inToolBarState = new ControlInToolBarState(); /** * Focus ring color state. */ public enum FocusType { INNER_FOCUS, OUTER_FOCUS, } /** * PaintContext, which holds a lot of the state needed for cache hinting and * x/y value decoding The data contained within the context is typically * only computed once and reused over multiple paint calls, whereas the * other values (w, h, f, leftWidth, etc) are recomputed for each call to * paint. * * <p>This field is retrieved from subclasses on each paint operation. It is * up to the subclass to compute and cache the PaintContext over multiple * calls.</p> */ private PaintContext ctx; /** The generator for almost all of the shapes we use to draw controls. */ protected ShapeGenerator shapeGenerator = new ShapeGenerator(); /** * Insets used for positioning the control border in order to leave enough * room for the focus indicator. */ protected Insets focusInsets; private Color outerFocus = decodeColor("seaGlassOuterFocus"); private Color innerFocus = decodeColor("seaGlassFocus"); private Color outerToolBarFocus = decodeColor("seaGlassToolBarOuterFocus"); private Color innerToolBarFocus = decodeColor("seaGlassToolBarFocus"); /** * Create a new AbstractRegionPainter */ protected AbstractRegionPainter() { focusInsets = UIManager.getInsets("seaGlassFocusInsets"); } /** * Returns true if we should paint focus using light colors on a blue * toolbar. * * @param c * * @return */ protected boolean isInToolBar(JComponent c) { return inToolBarState.isInState(c); } /** * {@inheritDoc} */ public final void paint(Graphics2D g, JComponent c, int w, int h) { // don't render if the width/height are too small if (w <= 0 || h <= 0) return; Object[] extendedCacheKeys = getExtendedCacheKeys(c); ctx = getPaintContext(); CacheMode cacheMode = ctx == null ? CacheMode.NO_CACHING : ctx.getCacheMode(); if (cacheMode == CacheMode.NO_CACHING || !ImageCache.getInstance().isImageCachable(w, h) || g instanceof PrinterGraphics) { paintDirectly(g, c, w, h, extendedCacheKeys); } else { paintWithCaching(g, c, w, h, extendedCacheKeys); } } /** * Get any extra attributes which the painter implementation would like to * include in the image cache lookups. This is checked for every call of the * paint(g, c, w, h) method. * * @param c The component on the current paint call * * @return Array of extra objects to be included in the cache key */ protected Object[] getExtendedCacheKeys(JComponent c) { return null; } /** * <p>Gets the PaintContext for this painting operation. This method is * called on every paint, and so should be fast and produce no garbage. The * PaintContext contains information such as cache hints. It also contains * data necessary for decoding points at runtime, such as the stretching * insets, the canvas size at which the encoded points were defined, and * whether the stretching insets are inverted.</p> * * <p>This method allows for subclasses to package the painting of different * states with possibly different canvas sizes, etc, into one * AbstractRegionPainter implementation.</p> * * @return a PaintContext associated with this paint operation. */ protected abstract PaintContext getPaintContext(); /** * Get the paint to use for a focus ring. * * @param s the shape to paint. * @param focusType the focus type. * @param useToolBarFocus whether we should use the colors for a toolbar control. * * @return the paint to use to paint the focus ring. */ public Paint getFocusPaint(Shape s, FocusType focusType, boolean useToolBarFocus) { if (focusType == FocusType.OUTER_FOCUS) { return useToolBarFocus ? outerToolBarFocus : outerFocus; } else { return useToolBarFocus ? innerToolBarFocus : innerFocus; } } /** * <p>Configures the given Graphics2D. Often, rendering hints or * compositiing rules are applied to a Graphics2D object prior to painting, * which should affect all of the subsequent painting operations. This * method provides a convenient hook for configuring the Graphics object * prior to rendering, regardless of whether the render operation is * performed to an intermediate buffer or directly to the display.</p> * * @param g The Graphics2D object to configure. Will not be null. */ protected void configureGraphics(Graphics2D g) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } /** * Actually performs the painting operation. Subclasses must implement this * method. The graphics object passed may represent the actual surface being * rendererd to, or it may be an intermediate buffer. It has also been * pre-translated. Simply render the component as if it were located at 0, 0 * and had a width of <code>width</code> and a height of <code>height</code> * . For performance reasons, you may want to read the clip from the * Graphics2D object and only render within that space. * * @param g The Graphics2D surface to paint to * @param c The JComponent related to the drawing event. For * example, if the region being rendered is Button, * then <code>c</code> will be a JButton. If the * region being drawn is ScrollBarSlider, then the * component will be JScrollBar. This value may be * null. * @param width The width of the region to paint. Note that in * the case of painting the foreground, this value * may differ from c.getWidth(). * @param height The height of the region to paint. Note that in * the case of painting the foreground, this value * may differ from c.getHeight(). * @param extendedCacheKeys The result of the call to getExtendedCacheKeys() */ protected abstract void doPaint(Graphics2D g, JComponent c, int width, int height, Object[] extendedCacheKeys); /** * Decodes and returns a base color in UI defaults. * * @param key A key corresponding to the value in the UI Defaults table of * UIManager where the base color is defined * * @return The base color. */ protected final Color decodeColor(String key) { return decodeColor(key, 0f, 0f, 0f, 0); } /** * Decodes and returns a color, which is derived from a base color in UI * defaults. * * @param key A key corresponding to the value in the UI Defaults table * of UIManager where the base color is defined * @param hOffset The hue offset used for derivation. * @param sOffset The saturation offset used for derivation. * @param bOffset The brightness offset used for derivation. * @param aOffset The alpha offset used for derivation. Between 0...255 * * @return The derived color, whos color value will change if the parent * uiDefault color changes. */ protected final Color decodeColor(String key, float hOffset, float sOffset, float bOffset, int aOffset) { if (UIManager.getLookAndFeel() instanceof SeaGlassLookAndFeel) { SeaGlassLookAndFeel laf = (SeaGlassLookAndFeel) UIManager.getLookAndFeel(); return laf.getDerivedColor(key, hOffset, sOffset, bOffset, aOffset, true); } else { // can not give a right answer as painter should not be used outside // of nimbus laf but do the best we can return Color.getHSBColor(hOffset, sOffset, bOffset); } } /** * Derive and returns a color, which is based on an existing color. * * @param src The source color from which to derive the new color. * @param hOffset The hue offset used for derivation. * @param sOffset The saturation offset used for derivation. * @param bOffset The brightness offset used for derivation. * @param aOffset The alpha offset used for derivation. Between 0...255 * * @return The derived color. */ protected Color deriveColor(Color src, float hOffset, float sOffset, float bOffset, int aOffset) { float[] tmp = Color.RGBtoHSB(src.getRed(), src.getGreen(), src.getBlue(), null); // apply offsets tmp[0] = clamp(tmp[0] + hOffset); tmp[1] = clamp(tmp[1] + sOffset); tmp[2] = clamp(tmp[2] + bOffset); int alpha = clamp(src.getAlpha() + aOffset); return new Color((Color.HSBtoRGB(tmp[0], tmp[1], tmp[2]) & 0xFFFFFF) | (alpha << 24), true); } /** * Decodes and returns a color, which is derived from a offset between two * other colors. * * @param color1 The first color * @param color2 The second color * @param midPoint The offset between color 1 and color 2, a value of 0.0 * is color 1 and 1.0 is color 2; * * @return The derived color */ protected final Color decodeColor(Color color1, Color color2, float midPoint) { return new Color(deriveARGB(color1, color2, midPoint)); } /** * Derives the ARGB value for a color based on an offset between two other * colors. * * @param color1 The first color * @param color2 The second color * @param midPoint The offset between color 1 and color 2, a value of 0.0 * is color 1 and 1.0 is color 2; * * @return the ARGB value for a new color based on this derivation */ public static int deriveARGB(Color color1, Color color2, float midPoint) { int r = color1.getRed() + (int) ((color2.getRed() - color1.getRed()) * midPoint + 0.5f); int g = color1.getGreen() + (int) ((color2.getGreen() - color1.getGreen()) * midPoint + 0.5f); int b = color1.getBlue() + (int) ((color2.getBlue() - color1.getBlue()) * midPoint + 0.5f); int a = color1.getAlpha() + (int) ((color2.getAlpha() - color1.getAlpha()) * midPoint + 0.5f); return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); } /** * Given parameters for creating a LinearGradientPaint, this method will * create and return a linear gradient paint. One primary purpose for this * method is to avoid creating a LinearGradientPaint where the start and end * points are equal. In such a case, the end y point is slightly increased * to avoid the overlap. * * @param x1 * @param y1 * @param x2 * @param y2 * @param midpoints * @param colors * * @return a valid LinearGradientPaint. This method never returns null. */ protected final LinearGradientPaint createGradient(float x1, float y1, float x2, float y2, float[] midpoints, Color[] colors) { if (x1 == x2 && y1 == y2) { y2 += .00001f; } return new LinearGradientPaint(x1, y1, x2, y2, midpoints, colors); } /** * Given parameters for creating a RadialGradientPaint, this method will * create and return a radial gradient paint. One primary purpose for this * method is to avoid creating a RadialGradientPaint where the radius is * non-positive. In such a case, the radius is just slightly increased to * avoid 0. * * @param x * @param y * @param r * @param midpoints * @param colors * * @return a valid RadialGradientPaint. This method never returns null. */ protected final RadialGradientPaint createRadialGradient(float x, float y, float r, float[] midpoints, Color[] colors) { if (r == 0f) { r = .00001f; } return new RadialGradientPaint(x, y, r, midpoints, colors); } /** * Creates a simple vertical gradient using the shape for bounds and the * colors for top and bottom colors. * * @param s the shape to use for bounds. * @param colors the colors to use for the gradient. * * @return the gradient. */ protected Paint createVerticalGradient(Shape s, TwoColors colors) { Rectangle2D bounds = s.getBounds2D(); float xCenter = (float) bounds.getCenterX(); float yMin = (float) bounds.getMinY(); float yMax = (float) bounds.getMaxY(); return createGradient(xCenter, yMin, xCenter, yMax, new float[] { 0f, 1f }, new Color[] { colors.top, colors.bottom }); } /** * Creates a simple vertical gradient using the shape for bounds and the * colors for top, two middle, and bottom colors. * * @param s the shape to use for bounds. * @param colors the colors to use for the gradient. * * @return the gradient. */ protected Paint createVerticalGradient(Shape s, FourColors colors) { Rectangle2D bounds = s.getBounds2D(); float xCenter = (float) bounds.getCenterX(); float yMin = (float) bounds.getMinY(); float yMax = (float) bounds.getMaxY(); return createGradient(xCenter, yMin, xCenter, yMax, new float[] { 0f, 0.45f, 0.62f, 1f }, new Color[] { colors.top, colors.upperMid, colors.lowerMid, colors.bottom }); } /** * Creates a simple horizontal gradient using the shape for bounds and the * colors for top and bottom colors. * * @param s the shape to use for bounds. * @param colors the colors to use for the gradient. * * @return the gradient. */ protected Paint createHorizontalGradient(Shape s, TwoColors colors) { Rectangle2D bounds = s.getBounds2D(); float xMin = (float) bounds.getMinX(); float xMax = (float) bounds.getMaxX(); float yCenter = (float) bounds.getCenterY(); return createGradient(xMin, yCenter, xMax, yCenter, new float[] { 0f, 1f }, new Color[] { colors.top, colors.bottom }); } /** * Creates a simple horizontal gradient using the shape for bounds and the * colors for top, two middle, and bottom colors. * * @param s the shape to use for bounds. * @param colors the colors to use for the gradient. * * @return the gradient. */ protected Paint createHorizontalGradient(Shape s, FourColors colors) { Rectangle2D bounds = s.getBounds2D(); float x = (float) bounds.getX(); float y = (float) bounds.getY(); float w = (float) bounds.getWidth(); float h = (float) bounds.getHeight(); return createGradient(x, (0.5f * h) + y, x + w, (0.5f * h) + y, new float[] { 0f, 0.45f, 0.62f, 1f }, new Color[] { colors.top, colors.upperMid, colors.lowerMid, colors.bottom }); } /** * Get a color property from the given JComponent. First checks for a <code> * getXXX()</code> method and if that fails checks for a client property * with key <code>property</code>. If that still fails to return a Color * then <code>defaultColor</code> is returned. * * @param c The component to get the color property from * @param property The name of a bean style property or client * property * @param defaultColor The color to return if no color was obtained * from the component. * @param saturationOffset The offset for the saturation. * @param brightnessOffset the offset for the brightness. * @param alphaOffset the offset for alpha. * * @return The color that was obtained from the component or defaultColor */ protected final Color getComponentColor(JComponent c, String property, Color defaultColor, float saturationOffset, float brightnessOffset, int alphaOffset) { Color color = null; if (c != null) { // handle some special cases for performance if ("background".equals(property)) { color = c.getBackground(); } else if ("foreground".equals(property)) { color = c.getForeground(); } else if (c instanceof JList && "selectionForeground".equals(property)) { color = ((JList) c).getSelectionForeground(); } else if (c instanceof JList && "selectionBackground".equals(property)) { color = ((JList) c).getSelectionBackground(); } else if (c instanceof JTable && "selectionForeground".equals(property)) { color = ((JTable) c).getSelectionForeground(); } else if (c instanceof JTable && "selectionBackground".equals(property)) { color = ((JTable) c).getSelectionBackground(); } else { String s = "get" + Character.toUpperCase(property.charAt(0)) + property.substring(1); try { Method method = c.getClass().getMethod(s); color = (Color) method.invoke(c); } catch (Exception e) { // don't do anything, it just didn't work, that's all. // This could be a normal occurance if you use a property // name referring to a key in clientProperties instead of // a real property } if (color == null) { Object value = c.getClientProperty(property); if (value instanceof Color) { color = (Color) value; } } } } // we return the defaultColor if the color found is null, or if // it is a UIResource. This is done because the color for the // ENABLED state is set on the component, but you don't want to use // that color for the over state. So we only respect the color // specified for the property if it was set by the user, as opposed // to set by us. if (color == null || color instanceof UIResource) { return defaultColor; } else if (saturationOffset != 0 || brightnessOffset != 0 || alphaOffset != 0) { float[] tmp = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); tmp[1] = clamp(tmp[1] + saturationOffset); tmp[2] = clamp(tmp[2] + brightnessOffset); int alpha = clamp(color.getAlpha() + alphaOffset); return new Color((Color.HSBtoRGB(tmp[0], tmp[1], tmp[2]) & 0xFFFFFF) | (alpha << 24)); } else { return color; } } /** * Returns a new color with the alpha of the old color cut in half. * * @param color the original color. * * @return the new color. */ protected Color disable(Color color) { return SeaGlassGraphicsUtils.disable(color); } /** * Returns a new color with the saturation cut to one third the original, * and the brightness moved one third closer to white. * * @param color the original color. * * @return the new color. */ protected Color desaturate(Color color) { float[] tmp = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); tmp[1] /= 3.0f; tmp[2] = clamp(1.0f - (1.0f - tmp[2]) / 3f); return new Color((Color.HSBtoRGB(tmp[0], tmp[1], tmp[2]) & 0xFFFFFF)); } /** * Returns a new TwoColors object with each color disabled using * {@link disable(Color)}. * * @param colors the original colors. * * @return the new colors. */ protected TwoColors disable(TwoColors colors) { return new TwoColors(disable(colors.top), disable(colors.bottom)); } /** * Returns a new TwoColors object with each color desaturated using * {@link desaturate(Color)}. * * @param colors the original colors. * * @return the new colors. */ protected TwoColors desaturate(TwoColors colors) { return new TwoColors(desaturate(colors.top), desaturate(colors.bottom)); } /** * Returns a new FourColors object with each color disabled using * {@link disable(Color)}. * * @param colors the original colors. * * @return the new colors. */ protected FourColors disable(FourColors colors) { return new FourColors(disable(colors.top), disable(colors.upperMid), disable(colors.lowerMid), disable(colors.bottom)); } /** * Returns a new FourColors object with each color desaturated using * {@link desaturate(Color)}. * * @param colors the original colors. * * @return the new colors. */ protected FourColors desaturate(FourColors colors) { return new FourColors(desaturate(colors.top), desaturate(colors.upperMid), desaturate(colors.lowerMid), desaturate(colors.bottom)); } // ---------------------- private methods /** * Paint the component, using a cached image if possible. * * @param g the Graphics2D context to paint with. * @param c the component to paint. * @param w the component width. * @param h the component height. * @param extendedCacheKeys extended cache keys. */ private void paintWithCaching(Graphics2D g, JComponent c, int w, int h, Object[] extendedCacheKeys) { VolatileImage img = getImage(g.getDeviceConfiguration(), c, w, h, extendedCacheKeys); if (img != null) { // render cached image g.drawImage(img, 0, 0, null); } else { // render directly paintDirectly(g, c, w, h, extendedCacheKeys); } } /** * Convenience method which creates a temporary graphics object by creating * a clone of the passed in one, configuring it, drawing with it, disposing * it. These steps have to be taken to ensure that any hints set on the * graphics are removed subsequent to painting. * * @param g the Graphics2D context to paint with. * @param c the component to paint. * @param w the component width. * @param h the component height. * @param extendedCacheKeys extended cache keys. */ private void paintDirectly(Graphics2D g, JComponent c, int w, int h, Object[] extendedCacheKeys) { g = (Graphics2D) g.create(); configureGraphics(g); doPaint(g, c, w, h, extendedCacheKeys); g.dispose(); } /** * Gets the rendered image for this painter at the requested size, either * from cache or create a new one * * @param config the graphics configuration. * @param c the component to paint. * @param w the component width. * @param h the component height. * @param extendedCacheKeys extended cache keys. * * @return the new image. */ private VolatileImage getImage(GraphicsConfiguration config, JComponent c, int w, int h, Object[] extendedCacheKeys) { ImageCache imageCache = ImageCache.getInstance(); // get the buffer for this component VolatileImage buffer = (VolatileImage) imageCache.getImage(config, w, h, this, extendedCacheKeys); int renderCounter = 0; // to avoid any potential, though unlikely, // infinite loop do { // validate the buffer so we can check for surface loss int bufferStatus = VolatileImage.IMAGE_INCOMPATIBLE; if (buffer != null) { bufferStatus = buffer.validate(config); } // If the buffer status is incompatible or restored, then we need to // re-render to the volatile image if (bufferStatus == VolatileImage.IMAGE_INCOMPATIBLE || bufferStatus == VolatileImage.IMAGE_RESTORED) { // if the buffer is null (hasn't been created), or isn't the // right size, or has lost its contents, // then recreate the buffer if (buffer == null || buffer.getWidth() != w || buffer.getHeight() != h || bufferStatus == VolatileImage.IMAGE_INCOMPATIBLE) { // clear any resources related to the old back buffer if (buffer != null) { buffer.flush(); buffer = null; } // recreate the buffer buffer = config.createCompatibleVolatileImage(w, h, Transparency.TRANSLUCENT); // put in cache for future imageCache.setImage(buffer, config, w, h, this, extendedCacheKeys); } // create the graphics context with which to paint to the buffer Graphics2D bg = buffer.createGraphics(); // clear the background before configuring the graphics bg.setComposite(AlphaComposite.Clear); bg.fillRect(0, 0, w, h); bg.setComposite(AlphaComposite.SrcOver); configureGraphics(bg); // paint the painter into buffer paintDirectly(bg, c, w, h, extendedCacheKeys); // close buffer graphics bg.dispose(); } } while (buffer.contentsLost() && renderCounter++ < 3); // check if we failed if (renderCounter == 3) return null; // return image return buffer; } /** * Clamp the value to legal limits. These are between 0.0f and 1.0f, * inclusive. * * @param value the original value. * * @return the new value. */ private float clamp(float value) { if (value < 0) { value = 0; } else if (value > 1) { value = 1; } return value; } /** * Clamp the value to legal limits. These are between 0 and 255, inclusive. * * @param value the original value. * * @return the new value. */ private int clamp(int value) { if (value < 0) { value = 0; } else if (value > 255) { value = 255; } return value; } /** * A class encapsulating state useful when painting. Generally, instances of * this class are created once, and reused for each paint request without * modification. This class contains values useful when hinting the cache * engine, and when decoding control points and bezier curve anchors. */ public static class PaintContext { /** * The cache modes. */ public static enum CacheMode { NO_CACHING, FIXED_SIZES } private CacheMode cacheMode; /** * Creates a new PaintContext. * * @param cacheMode A hint as to which caching mode to use. If null, * then set to no caching. */ public PaintContext(CacheMode cacheMode) { this.cacheMode = cacheMode == null ? CacheMode.NO_CACHING : cacheMode; } /** * Returns the cache mode. * * @return the cache mode. */ public CacheMode getCacheMode() { return cacheMode; } } /** * Two color gradients. */ public static class TwoColors { /** Top (or left) color. */ public Color top; /** Bottom (or right) color. */ public Color bottom; /** * Creates a new TwoColors object. * * @param top the top (or left) color. * @param bottom the bottom (or right) color. */ public TwoColors(Color top, Color bottom) { this.top = top; this.bottom = bottom; } } /** * Three color gradients. */ public static class ThreeColors extends TwoColors { /** Middle color. */ public Color mid; /** * Creates a new ThreeColors object. * * @param top the top (or left) color. * @param mid the middle color. * @param bottom the bottom (or right) color. */ public ThreeColors(Color top, Color mid, Color bottom) { super(top, bottom); this.mid = mid; } } /** * Four color gradients. */ public static class FourColors extends TwoColors { /** Upper (or left) middle color. */ public Color upperMid; /** Lower (or right) middle color. */ public Color lowerMid; /** * Creates a new FourColors object. * * @param top the top (or left) color. * @param upperMid the upper (or left) middle color. * @param lowerMid the lower (or right) middle color. * @param bottom the bottom (or right) color. */ public FourColors(Color top, Color upperMid, Color lowerMid, Color bottom) { super(top, bottom); this.upperMid = upperMid; this.lowerMid = lowerMid; } } }