1 /*
2  * Copyright (c) 2005, 2013, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 package javax.swing.plaf.nimbus;
26 
27 import java.awt.*;
28 import java.awt.image.*;
29 import java.lang.reflect.Method;
30 import javax.swing.*;
31 import javax.swing.plaf.UIResource;
32 import javax.swing.Painter;
33 import java.awt.print.PrinterGraphics;
34 import sun.reflect.misc.MethodUtil;
35 
36 /**
37  * Convenient base class for defining Painter instances for rendering a
38  * region or component in Nimbus.
39  *
40  * @author Jasper Potts
41  * @author Richard Bair
42  */
43 public abstract class AbstractRegionPainter implements Painter<JComponent> {
44     /**
45      * PaintContext, which holds a lot of the state needed for cache hinting and x/y value decoding
46      * The data contained within the context is typically only computed once and reused over
47      * multiple paint calls, whereas the other values (w, h, f, leftWidth, etc) are recomputed
48      * for each call to paint.
49      *
50      * This field is retrieved from subclasses on each paint operation. It is up
51      * to the subclass to compute and cache the PaintContext over multiple calls.
52      */
53     private PaintContext ctx;
54     /**
55      * The scaling factor. Recomputed on each call to paint.
56      */
57     private float f;
58     /*
59       Various metrics used for decoding x/y values based on the canvas size
60       and stretching insets.
61 
62       On each call to paint, we first ask the subclass for the PaintContext.
63       From the context we get the canvas size and stretching insets, and whether
64       the algorithm should be "inverted", meaning the center section remains
65       a fixed size and the other sections scale.
66 
67       We then use these values to compute a series of metrics (listed below)
68       which are used to decode points in a specific axis (x or y).
69 
70       The leftWidth represents the distance from the left edge of the region
71       to the first stretching inset, after accounting for any scaling factor
72       (such as DPI scaling). The centerWidth is the distance between the leftWidth
73       and the rightWidth. The rightWidth is the distance from the right edge,
74       to the right inset (after scaling has been applied).
75 
76       The same logic goes for topHeight, centerHeight, and bottomHeight.
77 
78       The leftScale represents the proportion of the width taken by the left section.
79       The same logic is applied to the other scales.
80 
81       The various widths/heights are used to decode control points. The
82       various scales are used to decode bezier handles (or anchors).
83     */
84     /**
85      * The width of the left section. Recomputed on each call to paint.
86      */
87     private float leftWidth;
88     /**
89      * The height of the top section. Recomputed on each call to paint.
90      */
91     private float topHeight;
92     /**
93      * The width of the center section. Recomputed on each call to paint.
94      */
95     private float centerWidth;
96     /**
97      * The height of the center section. Recomputed on each call to paint.
98      */
99     private float centerHeight;
100     /**
101      * The width of the right section. Recomputed on each call to paint.
102      */
103     private float rightWidth;
104     /**
105      * The height of the bottom section. Recomputed on each call to paint.
106      */
107     private float bottomHeight;
108     /**
109      * The scaling factor to use for the left section. Recomputed on each call to paint.
110      */
111     private float leftScale;
112     /**
113      * The scaling factor to use for the top section. Recomputed on each call to paint.
114      */
115     private float topScale;
116     /**
117      * The scaling factor to use for the center section, in the horizontal
118      * direction. Recomputed on each call to paint.
119      */
120     private float centerHScale;
121     /**
122      * The scaling factor to use for the center section, in the vertical
123      * direction. Recomputed on each call to paint.
124      */
125     private float centerVScale;
126     /**
127      * The scaling factor to use for the right section. Recomputed on each call to paint.
128      */
129     private float rightScale;
130     /**
131      * The scaling factor to use for the bottom section. Recomputed on each call to paint.
132      */
133     private float bottomScale;
134 
135     /**
136      * Create a new AbstractRegionPainter
137      */
AbstractRegionPainter()138     protected AbstractRegionPainter() { }
139 
140     /**
141      * {@inheritDoc}
142      */
143     @Override
paint(Graphics2D g, JComponent c, int w, int h)144     public final void paint(Graphics2D g, JComponent c, int w, int h) {
145         //don't render if the width/height are too small
146         if (w <= 0 || h <=0) return;
147 
148         Object[] extendedCacheKeys = getExtendedCacheKeys(c);
149         ctx = getPaintContext();
150         PaintContext.CacheMode cacheMode = ctx == null ? PaintContext.CacheMode.NO_CACHING : ctx.cacheMode;
151         if (cacheMode == PaintContext.CacheMode.NO_CACHING ||
152                 !ImageCache.getInstance().isImageCachable(w, h) ||
153                 g instanceof PrinterGraphics) {
154             // no caching so paint directly
155             paint0(g, c, w, h, extendedCacheKeys);
156         } else if (cacheMode == PaintContext.CacheMode.FIXED_SIZES) {
157             paintWithFixedSizeCaching(g, c, w, h, extendedCacheKeys);
158         } else {
159             // 9 Square caching
160             paintWith9SquareCaching(g, ctx, c, w, h, extendedCacheKeys);
161         }
162     }
163 
164     /**
165      * Get any extra attributes which the painter implementation would like
166      * to include in the image cache lookups. This is checked for every call
167      * of the paint(g, c, w, h) method.
168      *
169      * @param c The component on the current paint call
170      * @return Array of extra objects to be included in the cache key
171      */
getExtendedCacheKeys(JComponent c)172     protected Object[] getExtendedCacheKeys(JComponent c) {
173         return null;
174     }
175 
176     /**
177      * <p>Gets the PaintContext for this painting operation. This method is called on every
178      * paint, and so should be fast and produce no garbage. The PaintContext contains
179      * information such as cache hints. It also contains data necessary for decoding
180      * points at runtime, such as the stretching insets, the canvas size at which the
181      * encoded points were defined, and whether the stretching insets are inverted.</p>
182      *
183      * <p> This method allows for subclasses to package the painting of different states
184      * with possibly different canvas sizes, etc, into one AbstractRegionPainter implementation.</p>
185      *
186      * @return a PaintContext associated with this paint operation.
187      */
getPaintContext()188     protected abstract PaintContext getPaintContext();
189 
190     /**
191      * <p>Configures the given Graphics2D. Often, rendering hints or compositing rules are
192      * applied to a Graphics2D object prior to painting, which should affect all of the
193      * subsequent painting operations. This method provides a convenient hook for configuring
194      * the Graphics object prior to rendering, regardless of whether the render operation is
195      * performed to an intermediate buffer or directly to the display.</p>
196      *
197      * @param g The Graphics2D object to configure. Will not be null.
198      */
configureGraphics(Graphics2D g)199     protected void configureGraphics(Graphics2D g) {
200         g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
201     }
202 
203     /**
204      * Actually performs the painting operation. Subclasses must implement this method.
205      * The graphics object passed may represent the actual surface being rendered to,
206      * or it may be an intermediate buffer. It has also been pre-translated. Simply render
207      * the component as if it were located at 0, 0 and had a width of <code>width</code>
208      * and a height of <code>height</code>. For performance reasons, you may want to read
209      * the clip from the Graphics2D object and only render within that space.
210      *
211      * @param g The Graphics2D surface to paint to
212      * @param c The JComponent related to the drawing event. For example, if the
213      *          region being rendered is Button, then <code>c</code> will be a
214      *          JButton. If the region being drawn is ScrollBarSlider, then the
215      *          component will be JScrollBar. This value may be null.
216      * @param width The width of the region to paint. Note that in the case of
217      *              painting the foreground, this value may differ from c.getWidth().
218      * @param height The height of the region to paint. Note that in the case of
219      *               painting the foreground, this value may differ from c.getHeight().
220      * @param extendedCacheKeys The result of the call to getExtendedCacheKeys()
221      */
doPaint(Graphics2D g, JComponent c, int width, int height, Object[] extendedCacheKeys)222     protected abstract void doPaint(Graphics2D g, JComponent c, int width,
223                                     int height, Object[] extendedCacheKeys);
224 
225     /**
226      * Decodes and returns a float value representing the actual pixel location for
227      * the given encoded X value.
228      *
229      * @param x an encoded x value (0...1, or 1...2, or 2...3)
230      * @return the decoded x value
231      * @throws IllegalArgumentException
232      *      if {@code x < 0} or {@code x > 3}
233      */
decodeX(float x)234     protected final float decodeX(float x) {
235         if (x >= 0 && x <= 1) {
236             return x * leftWidth;
237         } else if (x > 1 && x < 2) {
238             return ((x-1) * centerWidth) + leftWidth;
239         } else if (x >= 2 && x <= 3) {
240             return ((x-2) * rightWidth) + leftWidth + centerWidth;
241         } else {
242             throw new IllegalArgumentException("Invalid x");
243         }
244     }
245 
246     /**
247      * Decodes and returns a float value representing the actual pixel location for
248      * the given encoded y value.
249      *
250      * @param y an encoded y value (0...1, or 1...2, or 2...3)
251      * @return the decoded y value
252      * @throws IllegalArgumentException
253      *      if {@code y < 0} or {@code y > 3}
254      */
decodeY(float y)255     protected final float decodeY(float y) {
256         if (y >= 0 && y <= 1) {
257             return y * topHeight;
258         } else if (y > 1 && y < 2) {
259             return ((y-1) * centerHeight) + topHeight;
260         } else if (y >= 2 && y <= 3) {
261             return ((y-2) * bottomHeight) + topHeight + centerHeight;
262         } else {
263             throw new IllegalArgumentException("Invalid y");
264         }
265     }
266 
267     /**
268      * Decodes and returns a float value representing the actual pixel location for
269      * the anchor point given the encoded X value of the control point, and the offset
270      * distance to the anchor from that control point.
271      *
272      * @param x an encoded x value of the bezier control point (0...1, or 1...2, or 2...3)
273      * @param dx the offset distance to the anchor from the control point x
274      * @return the decoded x location of the control point
275      * @throws IllegalArgumentException
276      *      if {@code x < 0} or {@code x > 3}
277      */
decodeAnchorX(float x, float dx)278     protected final float decodeAnchorX(float x, float dx) {
279         if (x >= 0 && x <= 1) {
280             return decodeX(x) + (dx * leftScale);
281         } else if (x > 1 && x < 2) {
282             return decodeX(x) + (dx * centerHScale);
283         } else if (x >= 2 && x <= 3) {
284             return decodeX(x) + (dx * rightScale);
285         } else {
286             throw new IllegalArgumentException("Invalid x");
287         }
288     }
289 
290     /**
291      * Decodes and returns a float value representing the actual pixel location for
292      * the anchor point given the encoded Y value of the control point, and the offset
293      * distance to the anchor from that control point.
294      *
295      * @param y an encoded y value of the bezier control point (0...1, or 1...2, or 2...3)
296      * @param dy the offset distance to the anchor from the control point y
297      * @return the decoded y position of the control point
298      * @throws IllegalArgumentException
299      *      if {@code y < 0} or {@code y > 3}
300      */
decodeAnchorY(float y, float dy)301     protected final float decodeAnchorY(float y, float dy) {
302         if (y >= 0 && y <= 1) {
303             return decodeY(y) + (dy * topScale);
304         } else if (y > 1 && y < 2) {
305             return decodeY(y) + (dy * centerVScale);
306         } else if (y >= 2 && y <= 3) {
307             return decodeY(y) + (dy * bottomScale);
308         } else {
309             throw new IllegalArgumentException("Invalid y");
310         }
311     }
312 
313     /**
314      * Decodes and returns a color, which is derived from a base color in UI
315      * defaults.
316      *
317      * @param key     A key corresponding to the value in the UI Defaults table
318      *                of UIManager where the base color is defined
319      * @param hOffset The hue offset used for derivation.
320      * @param sOffset The saturation offset used for derivation.
321      * @param bOffset The brightness offset used for derivation.
322      * @param aOffset The alpha offset used for derivation. Between 0...255
323      * @return The derived color, whose color value will change if the parent
324      *         uiDefault color changes.
325      */
decodeColor(String key, float hOffset, float sOffset, float bOffset, int aOffset)326     protected final Color decodeColor(String key, float hOffset, float sOffset,
327                                       float bOffset, int aOffset) {
328         if (UIManager.getLookAndFeel() instanceof NimbusLookAndFeel){
329             NimbusLookAndFeel laf = (NimbusLookAndFeel) UIManager.getLookAndFeel();
330             return laf.getDerivedColor(key, hOffset, sOffset, bOffset, aOffset, true);
331         } else {
332             // can not give a right answer as painter sould not be used outside
333             // of nimbus laf but do the best we can
334             return Color.getHSBColor(hOffset,sOffset,bOffset);
335         }
336     }
337 
338     /**
339      * Decodes and returns a color, which is derived from a offset between two
340      * other colors.
341      *
342      * @param color1   The first color
343      * @param color2   The second color
344      * @param midPoint The offset between color 1 and color 2, a value of 0.0 is
345      *                 color 1 and 1.0 is color 2;
346      * @return The derived color
347      */
decodeColor(Color color1, Color color2, float midPoint)348     protected final Color decodeColor(Color color1, Color color2,
349                                       float midPoint) {
350         return new Color(NimbusLookAndFeel.deriveARGB(color1, color2, midPoint));
351     }
352 
353     /**
354      * Given parameters for creating a LinearGradientPaint, this method will
355      * create and return a linear gradient paint. One primary purpose for this
356      * method is to avoid creating a LinearGradientPaint where the start and
357      * end points are equal. In such a case, the end y point is slightly
358      * increased to avoid the overlap.
359      *
360      * @param x1
361      * @param y1
362      * @param x2
363      * @param y2
364      * @param midpoints
365      * @param colors
366      * @return a valid LinearGradientPaint. This method never returns null.
367      * @throws NullPointerException
368      *      if {@code midpoints} array is null,
369      *      or {@code colors} array is null,
370      * @throws IllegalArgumentException
371      *      if start and end points are the same points,
372      *      or {@code midpoints.length != colors.length},
373      *      or {@code colors} is less than 2 in size,
374      *      or a {@code midpoints} value is less than 0.0 or greater than 1.0,
375      *      or the {@code midpoints} are not provided in strictly increasing order
376      */
decodeGradient(float x1, float y1, float x2, float y2, float[] midpoints, Color[] colors)377     protected final LinearGradientPaint decodeGradient(float x1, float y1, float x2, float y2, float[] midpoints, Color[] colors) {
378         if (x1 == x2 && y1 == y2) {
379             y2 += .00001f;
380         }
381         return new LinearGradientPaint(x1, y1, x2, y2, midpoints, colors);
382     }
383 
384     /**
385      * Given parameters for creating a RadialGradientPaint, this method will
386      * create and return a radial gradient paint. One primary purpose for this
387      * method is to avoid creating a RadialGradientPaint where the radius
388      * is non-positive. In such a case, the radius is just slightly
389      * increased to avoid 0.
390      *
391      * @param x
392      * @param y
393      * @param r
394      * @param midpoints
395      * @param colors
396      * @return a valid RadialGradientPaint. This method never returns null.
397      * @throws NullPointerException
398      *      if {@code midpoints} array is null,
399      *      or {@code colors} array is null
400      * @throws IllegalArgumentException
401      *      if {@code r} is non-positive,
402      *      or {@code midpoints.length != colors.length},
403      *      or {@code colors} is less than 2 in size,
404      *      or a {@code midpoints} value is less than 0.0 or greater than 1.0,
405      *      or the {@code midpoints} are not provided in strictly increasing order
406      */
decodeRadialGradient(float x, float y, float r, float[] midpoints, Color[] colors)407     protected final RadialGradientPaint decodeRadialGradient(float x, float y, float r, float[] midpoints, Color[] colors) {
408         if (r == 0f) {
409             r = .00001f;
410         }
411         return new RadialGradientPaint(x, y, r, midpoints, colors);
412     }
413 
414     /**
415      * Get a color property from the given JComponent. First checks for a
416      * <code>getXXX()</code> method and if that fails checks for a client
417      * property with key <code>property</code>. If that still fails to return
418      * a Color then <code>defaultColor</code> is returned.
419      *
420      * @param c The component to get the color property from
421      * @param property The name of a bean style property or client property
422      * @param defaultColor The color to return if no color was obtained from
423      *        the component.
424      * @return The color that was obtained from the component or defaultColor
425      */
getComponentColor(JComponent c, String property, Color defaultColor, float saturationOffset, float brightnessOffset, int alphaOffset)426     protected final Color getComponentColor(JComponent c, String property,
427                                             Color defaultColor,
428                                             float saturationOffset,
429                                             float brightnessOffset,
430                                             int alphaOffset) {
431         Color color = null;
432         if (c != null) {
433             // handle some special cases for performance
434             if ("background".equals(property)) {
435                 color = c.getBackground();
436             } else if ("foreground".equals(property)) {
437                 color = c.getForeground();
438             } else if (c instanceof JList && "selectionForeground".equals(property)) {
439                 color = ((JList) c).getSelectionForeground();
440             } else if (c instanceof JList && "selectionBackground".equals(property)) {
441                 color = ((JList) c).getSelectionBackground();
442             } else if (c instanceof JTable && "selectionForeground".equals(property)) {
443                 color = ((JTable) c).getSelectionForeground();
444             } else if (c instanceof JTable && "selectionBackground".equals(property)) {
445                 color = ((JTable) c).getSelectionBackground();
446             } else {
447                 String s = "get" + Character.toUpperCase(property.charAt(0)) + property.substring(1);
448                 try {
449                     Method method = MethodUtil.getMethod(c.getClass(), s, null);
450                     color = (Color) MethodUtil.invoke(method, c, null);
451                 } catch (Exception e) {
452                     //don't do anything, it just didn't work, that's all.
453                     //This could be a normal occurance if you use a property
454                     //name referring to a key in clientProperties instead of
455                     //a real property
456                 }
457                 if (color == null) {
458                     Object value = c.getClientProperty(property);
459                     if (value instanceof Color) {
460                         color = (Color) value;
461                     }
462                 }
463             }
464         }
465         // we return the defaultColor if the color found is null, or if
466         // it is a UIResource. This is done because the color for the
467         // ENABLED state is set on the component, but you don't want to use
468         // that color for the over state. So we only respect the color
469         // specified for the property if it was set by the user, as opposed
470         // to set by us.
471         if (color == null || color instanceof UIResource) {
472             return defaultColor;
473         } else if (saturationOffset != 0 || brightnessOffset != 0 || alphaOffset != 0) {
474             float[] tmp = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null);
475             tmp[1] = clamp(tmp[1] + saturationOffset);
476             tmp[2] = clamp(tmp[2] + brightnessOffset);
477             int alpha = clamp(color.getAlpha() + alphaOffset);
478             return new Color((Color.HSBtoRGB(tmp[0], tmp[1], tmp[2]) & 0xFFFFFF) | (alpha <<24));
479         } else {
480             return color;
481         }
482     }
483 
484     /**
485      * A class encapsulating state useful when painting. Generally, instances of this
486      * class are created once, and reused for each paint request without modification.
487      * This class contains values useful when hinting the cache engine, and when decoding
488      * control points and bezier curve anchors.
489      */
490     protected static class PaintContext {
491         protected static enum CacheMode {
492             NO_CACHING, FIXED_SIZES, NINE_SQUARE_SCALE
493         }
494 
495         private static Insets EMPTY_INSETS = new Insets(0, 0, 0, 0);
496 
497         private Insets stretchingInsets;
498         private Dimension canvasSize;
499         private boolean inverted;
500         private CacheMode cacheMode;
501         private double maxHorizontalScaleFactor;
502         private double maxVerticalScaleFactor;
503 
504         private float a; // insets.left
505         private float b; // canvasSize.width - insets.right
506         private float c; // insets.top
507         private float d; // canvasSize.height - insets.bottom;
508         private float aPercent; // only used if inverted == true
509         private float bPercent; // only used if inverted == true
510         private float cPercent; // only used if inverted == true
511         private float dPercent; // only used if inverted == true
512 
513         /**
514          * Creates a new PaintContext which does not attempt to cache or scale any cached
515          * images.
516          *
517          * @param insets The stretching insets. May be null. If null, then assumed to be 0, 0, 0, 0.
518          * @param canvasSize The size of the canvas used when encoding the various x/y values. May be null.
519          *                   If null, then it is assumed that there are no encoded values, and any calls
520          *                   to one of the "decode" methods will return the passed in value.
521          * @param inverted Whether to "invert" the meaning of the 9-square grid and stretching insets
522          */
PaintContext(Insets insets, Dimension canvasSize, boolean inverted)523         public PaintContext(Insets insets, Dimension canvasSize, boolean inverted) {
524             this(insets, canvasSize, inverted, null, 1, 1);
525         }
526 
527         /**
528          * Creates a new PaintContext.
529          *
530          * @param insets The stretching insets. May be null. If null, then assumed to be 0, 0, 0, 0.
531          * @param canvasSize The size of the canvas used when encoding the various x/y values. May be null.
532          *                   If null, then it is assumed that there are no encoded values, and any calls
533          *                   to one of the "decode" methods will return the passed in value.
534          * @param inverted Whether to "invert" the meaning of the 9-square grid and stretching insets
535          * @param cacheMode A hint as to which caching mode to use. If null, then set to no caching.
536          * @param maxH The maximum scale in the horizontal direction to use before punting and redrawing from scratch.
537          *             For example, if maxH is 2, then we will attempt to scale any cached images up to 2x the canvas
538          *             width before redrawing from scratch. Reasonable maxH values may improve painting performance.
539          *             If set too high, then you may get poor looking graphics at higher zoom levels. Must be &gt;= 1.
540          * @param maxV The maximum scale in the vertical direction to use before punting and redrawing from scratch.
541          *             For example, if maxV is 2, then we will attempt to scale any cached images up to 2x the canvas
542          *             height before redrawing from scratch. Reasonable maxV values may improve painting performance.
543          *             If set too high, then you may get poor looking graphics at higher zoom levels. Must be &gt;= 1.
544          */
PaintContext(Insets insets, Dimension canvasSize, boolean inverted, CacheMode cacheMode, double maxH, double maxV)545         public PaintContext(Insets insets, Dimension canvasSize, boolean inverted,
546                             CacheMode cacheMode, double maxH, double maxV) {
547             if (maxH < 1 || maxH < 1) {
548                 throw new IllegalArgumentException("Both maxH and maxV must be >= 1");
549             }
550 
551             this.stretchingInsets = insets == null ? EMPTY_INSETS : insets;
552             this.canvasSize = canvasSize;
553             this.inverted = inverted;
554             this.cacheMode = cacheMode == null ? CacheMode.NO_CACHING : cacheMode;
555             this.maxHorizontalScaleFactor = maxH;
556             this.maxVerticalScaleFactor = maxV;
557 
558             if (canvasSize != null) {
559                 a = stretchingInsets.left;
560                 b = canvasSize.width - stretchingInsets.right;
561                 c = stretchingInsets.top;
562                 d = canvasSize.height - stretchingInsets.bottom;
563                 this.canvasSize = canvasSize;
564                 this.inverted = inverted;
565                 if (inverted) {
566                     float available = canvasSize.width - (b - a);
567                     aPercent = available > 0f ? a / available : 0f;
568                     bPercent = available > 0f ? b / available : 0f;
569                     available = canvasSize.height - (d - c);
570                     cPercent = available > 0f ? c / available : 0f;
571                     dPercent = available > 0f ? d / available : 0f;
572                 }
573             }
574         }
575     }
576 
577     //---------------------- private methods
578 
579     //initializes the class to prepare it for being able to decode points
prepare(float w, float h)580     private void prepare(float w, float h) {
581         //if no PaintContext has been specified, reset the values and bail
582         //also bail if the canvasSize was not set (since decoding will not work)
583         if (ctx == null || ctx.canvasSize == null) {
584             f = 1f;
585             leftWidth = centerWidth = rightWidth = 0f;
586             topHeight = centerHeight = bottomHeight = 0f;
587             leftScale = centerHScale = rightScale = 0f;
588             topScale = centerVScale = bottomScale = 0f;
589             return;
590         }
591 
592         //calculate the scaling factor, and the sizes for the various 9-square sections
593         Number scale = (Number)UIManager.get("scale");
594         f = scale == null ? 1f : scale.floatValue();
595 
596         if (ctx.inverted) {
597             centerWidth = (ctx.b - ctx.a) * f;
598             float availableSpace = w - centerWidth;
599             leftWidth = availableSpace * ctx.aPercent;
600             rightWidth = availableSpace * ctx.bPercent;
601             centerHeight = (ctx.d - ctx.c) * f;
602             availableSpace = h - centerHeight;
603             topHeight = availableSpace * ctx.cPercent;
604             bottomHeight = availableSpace * ctx.dPercent;
605         } else {
606             leftWidth = ctx.a * f;
607             rightWidth = (float)(ctx.canvasSize.getWidth() - ctx.b) * f;
608             centerWidth = w - leftWidth - rightWidth;
609             topHeight = ctx.c * f;
610             bottomHeight = (float)(ctx.canvasSize.getHeight() - ctx.d) * f;
611             centerHeight = h - topHeight - bottomHeight;
612         }
613 
614         leftScale = ctx.a == 0f ? 0f : leftWidth / ctx.a;
615         centerHScale = (ctx.b - ctx.a) == 0f ? 0f : centerWidth / (ctx.b - ctx.a);
616         rightScale = (ctx.canvasSize.width - ctx.b) == 0f ? 0f : rightWidth / (ctx.canvasSize.width - ctx.b);
617         topScale = ctx.c == 0f ? 0f : topHeight / ctx.c;
618         centerVScale = (ctx.d - ctx.c) == 0f ? 0f : centerHeight / (ctx.d - ctx.c);
619         bottomScale = (ctx.canvasSize.height - ctx.d) == 0f ? 0f : bottomHeight / (ctx.canvasSize.height - ctx.d);
620     }
621 
paintWith9SquareCaching(Graphics2D g, PaintContext ctx, JComponent c, int w, int h, Object[] extendedCacheKeys)622     private void paintWith9SquareCaching(Graphics2D g, PaintContext ctx,
623                                          JComponent c, int w, int h,
624                                          Object[] extendedCacheKeys) {
625         // check if we can scale to the requested size
626         Dimension canvas = ctx.canvasSize;
627         Insets insets = ctx.stretchingInsets;
628         if (w <= (canvas.width * ctx.maxHorizontalScaleFactor) && h <= (canvas.height * ctx.maxVerticalScaleFactor)) {
629             // get image at canvas size
630             VolatileImage img = getImage(g.getDeviceConfiguration(), c, canvas.width, canvas.height, extendedCacheKeys);
631             if (img != null) {
632                 // calculate dst inserts
633                 // todo: destination inserts need to take into acount scale factor for high dpi. Note: You can use f for this, I think
634                 Insets dstInsets;
635                 if (ctx.inverted){
636                     int leftRight = (w-(canvas.width-(insets.left+insets.right)))/2;
637                     int topBottom = (h-(canvas.height-(insets.top+insets.bottom)))/2;
638                     dstInsets = new Insets(topBottom,leftRight,topBottom,leftRight);
639                 } else {
640                     dstInsets = insets;
641                 }
642                 // paint 9 square scaled
643                 Object oldScaleingHints = g.getRenderingHint(RenderingHints.KEY_INTERPOLATION);
644                 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR);
645                 ImageScalingHelper.paint(g, 0, 0, w, h, img, insets, dstInsets,
646                         ImageScalingHelper.PaintType.PAINT9_STRETCH, ImageScalingHelper.PAINT_ALL);
647                 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
648                     oldScaleingHints!=null?oldScaleingHints:RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
649             } else {
650                 // render directly
651                 paint0(g, c, w, h, extendedCacheKeys);
652             }
653         } else {
654             // paint directly
655             paint0(g, c, w, h, extendedCacheKeys);
656         }
657     }
658 
paintWithFixedSizeCaching(Graphics2D g, JComponent c, int w, int h, Object[] extendedCacheKeys)659     private void paintWithFixedSizeCaching(Graphics2D g, JComponent c, int w,
660                                            int h, Object[] extendedCacheKeys) {
661         VolatileImage img = getImage(g.getDeviceConfiguration(), c, w, h, extendedCacheKeys);
662         if (img != null) {
663             //render cached image
664             g.drawImage(img, 0, 0, null);
665         } else {
666             // render directly
667             paint0(g, c, w, h, extendedCacheKeys);
668         }
669     }
670 
671     /** Gets the rendered image for this painter at the requested size, either from cache or create a new one */
getImage(GraphicsConfiguration config, JComponent c, int w, int h, Object[] extendedCacheKeys)672     private VolatileImage getImage(GraphicsConfiguration config, JComponent c,
673                                    int w, int h, Object[] extendedCacheKeys) {
674         ImageCache imageCache = ImageCache.getInstance();
675         //get the buffer for this component
676         VolatileImage buffer = (VolatileImage) imageCache.getImage(config, w, h, this, extendedCacheKeys);
677 
678         int renderCounter = 0; //to avoid any potential, though unlikely, infinite loop
679         do {
680             //validate the buffer so we can check for surface loss
681             int bufferStatus = VolatileImage.IMAGE_INCOMPATIBLE;
682             if (buffer != null) {
683                 bufferStatus = buffer.validate(config);
684             }
685 
686             //If the buffer status is incompatible or restored, then we need to re-render to the volatile image
687             if (bufferStatus == VolatileImage.IMAGE_INCOMPATIBLE || bufferStatus == VolatileImage.IMAGE_RESTORED) {
688                 //if the buffer is null (hasn't been created), or isn't the right size, or has lost its contents,
689                 //then recreate the buffer
690                 if (buffer == null || buffer.getWidth() != w || buffer.getHeight() != h ||
691                         bufferStatus == VolatileImage.IMAGE_INCOMPATIBLE) {
692                     //clear any resources related to the old back buffer
693                     if (buffer != null) {
694                         buffer.flush();
695                         buffer = null;
696                     }
697                     //recreate the buffer
698                     buffer = config.createCompatibleVolatileImage(w, h,
699                             Transparency.TRANSLUCENT);
700                     // put in cache for future
701                     imageCache.setImage(buffer, config, w, h, this, extendedCacheKeys);
702                 }
703                 //create the graphics context with which to paint to the buffer
704                 Graphics2D bg = buffer.createGraphics();
705                 //clear the background before configuring the graphics
706                 bg.setComposite(AlphaComposite.Clear);
707                 bg.fillRect(0, 0, w, h);
708                 bg.setComposite(AlphaComposite.SrcOver);
709                 configureGraphics(bg);
710                 // paint the painter into buffer
711                 paint0(bg, c, w, h, extendedCacheKeys);
712                 //close buffer graphics
713                 bg.dispose();
714             }
715         } while (buffer.contentsLost() && renderCounter++ < 3);
716         // check if we failed
717         if (renderCounter == 3) return null;
718         // return image
719         return buffer;
720     }
721 
722     //convenience method which creates a temporary graphics object by creating a
723     //clone of the passed in one, configuring it, drawing with it, disposing it.
724     //These steps have to be taken to ensure that any hints set on the graphics
725     //are removed subsequent to painting.
paint0(Graphics2D g, JComponent c, int width, int height, Object[] extendedCacheKeys)726     private void paint0(Graphics2D g, JComponent c, int width, int height,
727                         Object[] extendedCacheKeys) {
728         prepare(width, height);
729         g = (Graphics2D)g.create();
730         configureGraphics(g);
731         doPaint(g, c, width, height, extendedCacheKeys);
732         g.dispose();
733     }
734 
clamp(float value)735     private float clamp(float value) {
736         if (value < 0) {
737             value = 0;
738         } else if (value > 1) {
739             value = 1;
740         }
741         return value;
742     }
743 
clamp(int value)744     private int clamp(int value) {
745         if (value < 0) {
746             value = 0;
747         } else if (value > 255) {
748             value = 255;
749         }
750         return value;
751     }
752 }
753