001    /*
002     * $Id: AbstractPainter.java,v 1.17 2006/05/14 08:19:44 dmouse Exp $
003     *
004     * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005     * Santa Clara, California 95054, U.S.A. All rights reserved.
006     *
007     * This library is free software; you can redistribute it and/or
008     * modify it under the terms of the GNU Lesser General Public
009     * License as published by the Free Software Foundation; either
010     * version 2.1 of the License, or (at your option) any later version.
011     * 
012     * This library is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015     * Lesser General Public License for more details.
016     * 
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this library; if not, write to the Free Software
019     * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020     */
021    
022    package org.jdesktop.swingx.painter;
023    
024    import java.awt.Color;
025    import java.awt.Composite;
026    import java.awt.Font;
027    import java.awt.Graphics2D;
028    import java.awt.Paint;
029    import java.awt.RenderingHints;
030    import java.awt.Shape;
031    import java.awt.Stroke;
032    import java.awt.Transparency;
033    import java.awt.geom.AffineTransform;
034    import java.awt.geom.RoundRectangle2D;
035    import java.awt.image.BufferedImage;
036    import java.awt.image.BufferedImageOp;
037    import java.lang.ref.SoftReference;
038    import java.util.HashMap;
039    import java.util.Map;
040    import javax.swing.JComponent;
041    import org.jdesktop.swingx.JavaBean;
042    import org.jdesktop.swingx.util.PaintUtils;
043    import org.jdesktop.swingx.util.Resize;
044    
045    /**
046     * <p>A convenient base class from which concrete Painter implementations may
047     * extend. It extends JavaBean and thus provides property change notification
048     * (which is crucial for the Painter implementations to be available in a
049     * GUI builder). It also saves off the Graphics2D state in its "saveState" method,
050     * and restores that state in the "restoreState" method. Sublasses simply need
051     * to extend AbstractPainter and implement the paintBackground method.
052     * 
053     * <p>For example, here is the paintBackground method of BackgroundPainter:
054     * <pre><code>
055     *  public void paintBackground(Graphics2D g, JComponent component) {
056     *      g.setColor(component.getBackground());
057     *      g.fillRect(0, 0, component.getWidth(), component.getHeight());
058     *  }
059     * </code></pre>
060     * 
061     * <p>AbstractPainter provides a very useful default implementation of
062     * the paint method. It:
063     * <ol>
064     *  <li>Saves off the old state</li>
065     *  <li>Sets any specified rendering hints</li>
066     *  <li>Sets the Clip if there is one</li>
067     *  <li>Sets the Composite if there is one</li>
068     *  <li>Delegates to paintBackground</li>
069     *  <li>Restores the original Graphics2D state</li>
070     * <ol></p>
071     *
072     * <p>Specifying rendering hints can greatly improve the visual impact of your
073     * applications. For example, by default Swing doesn't do much in the way of
074     * antialiasing (except for Fonts, but that's another story). Pinstripes don't
075     * look so good without antialiasing. So if I were going to paint pinstripes, I
076     * might do it like this:
077     * <pre><code>
078     *   PinstripePainter p = new PinstripePainter();
079     *   p.setAntialiasing(RenderingHints.VALUE_ANTIALIAS_ON);
080     * </code></pre></p>
081     *
082     * <p>You can read more about antialiasing and other rendering hints in the
083     * java.awt.RenderingHints documentation. <strong>By nature, changing the rendering
084     * hints may have an impact on performance. Certain hints require more
085     * computation, others require less</strong></p>
086     * 
087     * @author rbair
088     */
089    public abstract class AbstractPainter<T extends JComponent> extends JavaBean implements Painter<T> {
090        //------------------------------------------------- Saved Graphics State
091        private boolean stateSaved = false;
092        private Paint oldPaint;
093        private Font oldFont;
094        private Stroke oldStroke;
095        private AffineTransform oldTransform;
096        private Composite oldComposite;
097        private Shape oldClip;
098        private Color oldBackground;
099        private Color oldColor;
100        private RenderingHints oldRenderingHints;
101        
102        //--------------------------------------------------- Instance Variables
103        /**
104         * A Shape that is used to clip the graphics area. Anything within this
105         * clip shape is included in the final output.
106         */
107        private Shape clip;
108        /**
109         * A Resize value indicating if and how the clip should be resized
110         * according to the size of the Component
111         */
112        private Resize resizeClip = Resize.BOTH;
113        /**
114         * The composite to use. By default this is a reasonable AlphaComposite,
115         * but you may want to specify a different composite
116         */
117        private Composite composite;
118        /**
119         * RenderingHints to apply when painting
120         */
121        private Map<RenderingHints.Key,Object> renderingHints;
122        /**
123         * A hint as to whether or not to attempt caching the image
124         */
125        private boolean useCache = false;
126        /**
127         * The cached image, if useCache is true
128         */
129        private SoftReference<BufferedImage> cachedImage;
130        /**
131         * The Effects to apply to the results of the paint() operation
132         */
133        private Effect[] effects = new Effect[0];
134        
135        /**
136         * Creates a new instance of AbstractPainter
137         */
138        public AbstractPainter() {
139            renderingHints = new HashMap<RenderingHints.Key,Object>();
140        }
141        
142        /**
143         * <p>Sets whether to cache the painted image with a SoftReference in a BufferedImage
144         * between calls. If true, and if the size of the component hasn't changed,
145         * then the cached image will be used rather than causing a painting operation.</p>
146         *
147         * <p>This should be considered a hint, rather than absolute. Several factors may
148         * force repainting, including low memory, different component sizes, or possibly
149         * new rendering hint settings, etc.</p>
150         *
151         * @param b whether or not to use the cache
152         */
153        public void setUseCache(boolean b) {
154            boolean old = isUseCache();
155            useCache = b;
156            firePropertyChange("useCache", old, isUseCache());
157            //if there was a cached image and I'm no longer using the cache, blow it away
158            if (cachedImage != null && !isUseCache()) {
159                cachedImage = null;
160            }
161        }
162        
163        /**
164         * @return whether or not the cache should be used
165         */
166        public boolean isUseCache() {
167            return useCache;
168        }
169        
170        /**
171         * <p>Sets the effects to apply to the results of the AbstractPainter's
172         * painting operation. Some common effects include blurs, shadows, embossing,
173         * and so forth. If the given effects is a null array, no effects will be used</p>
174         *
175         * @param effects the Effects to apply to the results of the AbstractPainter's
176         *                painting operation
177         */
178        public void setEffects(Effect... effects) {
179            Effect[] old = getEffects();
180            this.effects = new Effect[effects == null ? 0 : effects.length];
181            if (effects != null) {
182                System.arraycopy(effects, 0, this.effects, 0, effects.length);
183            }
184            firePropertyChange("effects", old, getEffects());
185            firePropertyChange("effects", old, getEffects());
186        }
187        
188        /**
189         * <p>A convenience method for specifying the effects to use based on
190         * BufferedImageOps. These will each be individually wrapped by an ImageEffect
191         * and then setEffects(Effect... effects) will be called with the resulting
192         * array</p>
193         *
194         * @param filters the BufferedImageOps to wrap as effects
195         */
196        public void setEffects(BufferedImageOp... filters) {
197            Effect[] effects = new Effect[filters == null ? 0 : filters.length];
198            if (filters != null) {
199                int index = 0;
200                for (BufferedImageOp op : filters) {
201                    effects[index++] = new ImageEffect(op);
202                }
203            }
204            setEffects(effects);
205        }
206        
207        /**
208         * @return effects a defensive copy of the Effects to apply to the results
209         *          of the AbstractPainter's painting operation. Will never null
210         */
211        public Effect[] getEffects() {
212            Effect[] results = new Effect[effects.length];
213            System.arraycopy(effects, 0, results, 0, results.length);
214            return results;
215        }
216        
217        /**
218         * Specifies the Shape to use for clipping the painting area. This
219         * may be null
220         *
221         * @param clip the Shape to use to clip the area. Whatever is inside this
222         *        shape will be kept, everything else "clipped". May be null. If
223         *        null, the clipping is not set on the graphics object
224         */
225        public void setClip(Shape clip) {
226            Shape old = getClip();
227            this.clip = clip;
228            firePropertyChange("clip", old, getClip());
229        }
230        
231        /**
232         * @return the clipping shape
233         */
234        public Shape getClip() {
235            return clip;
236        }
237        
238        /**
239         * Specifies the resize behavior of the clip. As with all other properties
240         * that rely on Resize, the value of the width/height of the shape will
241         * represent a percentage of the width/height of the component, as a value
242         * between 0 and 1
243         *
244         * @param r value indication whether/how to resize the clip. If null,
245         *        Resize.NONE will be used
246         */
247        public void setResizeClip(Resize r) {
248            Resize old = getResizeClip();
249            this.resizeClip = r == null ? Resize.NONE : r;
250            firePropertyChange("resizeClip", old, getResizeClip());
251        }
252        
253        /**
254         * @return value indication whether/how to resize the clip. Will never be null
255         */
256        public Resize getResizeClip() {
257            return resizeClip;
258        }
259        
260        /**
261         * Sets the Composite to use. For example, you may specify a specific
262         * AlphaComposite so that when this Painter paints, any content in the
263         * drawing area is handled properly
264         *
265         * @param c The composite to use. If null, then no composite will be
266         *        specified on the graphics object
267         */
268        public void setComposite(Composite c) {
269            Composite old = getComposite();
270            this.composite = c;
271            firePropertyChange("composite", old, getComposite());
272        }
273        
274        /**
275         * @return the composite
276         */
277        public Composite getComposite() {
278            return composite;
279        }
280    
281        /**
282         * @return the technique used for interpolating alpha values. May be one
283         * of:
284         * <ul>
285         *  <li>RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED</li>
286         *  <li>RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY</li>
287         *  <li>RenderingHints.VALUE_ALPHA_INTERPOLATION_DEFAULT</li>
288         * </ul>
289         */
290        public Object getAlphaInterpolation() {
291            return renderingHints.get(RenderingHints.KEY_ALPHA_INTERPOLATION);
292        }
293    
294        /**
295         * Sets the technique used for interpolating alpha values.
296         *
297         * @param alphaInterpolation
298         * May be one of:
299         * <ul>
300         *  <li>RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED</li>
301         *  <li>RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY</li>
302         *  <li>RenderingHints.VALUE_ALPHA_INTERPOLATION_DEFAULT</li>
303         * </ul>
304         */
305        public void setAlphaInterpolation(Object alphaInterpolation) {
306            if (alphaInterpolation != null && 
307                    !RenderingHints.KEY_ALPHA_INTERPOLATION.isCompatibleValue(alphaInterpolation)) {
308                throw new IllegalArgumentException(alphaInterpolation + " is not an acceptable value");
309            }
310            Object old = getAlphaInterpolation();
311            renderingHints.put(RenderingHints.KEY_ALPHA_INTERPOLATION, alphaInterpolation);
312            firePropertyChange("alphaInterpolation", old, getAlphaInterpolation());
313        }
314    
315        /**
316         * @return whether or not to antialias
317         *          May be one of:
318         * <ul>
319         *  <li>RenderingHints.VALUE_ANTIALIAS_DEFAULT</li>
320         *  <li>RenderingHints.VALUE_ANTIALIAS_OFF</li>
321         *  <li>RenderingHints.VALUE_ANTIALIAS_ON</li>
322         * </ul>
323         */
324        public Object getAntialiasing() {
325            return renderingHints.get(RenderingHints.KEY_ANTIALIASING);
326        }
327    
328        /**
329         * Sets whether or not to antialias
330         * @param antialiasing
331         *          May be one of:
332         * <ul>
333         *  <li>RenderingHints.VALUE_ANTIALIAS_DEFAULT</li>
334         *  <li>RenderingHints.VALUE_ANTIALIAS_OFF</li>
335         *  <li>RenderingHints.VALUE_ANTIALIAS_ON</li>
336         * </ul>
337         */
338        public void setAntialiasing(Object antialiasing) {
339            if (antialiasing != null && 
340                    !RenderingHints.KEY_ANTIALIASING.isCompatibleValue(antialiasing)) {
341                throw new IllegalArgumentException(antialiasing + " is not an acceptable value");
342            }
343            Object old = getAntialiasing();
344            renderingHints.put(RenderingHints.KEY_ANTIALIASING, antialiasing);
345            firePropertyChange("antialiasing", old, getAntialiasing());
346        }
347    
348        /**
349         * @return the technique to use for rendering colors
350         *          May be one of:
351         * <ul>
352         *  <li>RenderingHints.VALUE_COLOR_RENDER_DEFAULT</li>
353         *  <li>RenderingHints.VALUE_RENDER_QUALITY</li>
354         *  <li>RenderingHints.VALUE_RENDER_SPEED</li>
355         * </ul>
356         */
357        public Object getColorRendering() {
358            return renderingHints.get(RenderingHints.KEY_COLOR_RENDERING);
359        }
360    
361        /**
362         * Sets the technique to use for rendering colors
363         * @param colorRendering
364         *          May be one of:
365         * <ul>
366         *  <li>RenderingHints.VALUE_COLOR_RENDER_DEFAULT</li>
367         *  <li>RenderingHints.VALUE_RENDER_QUALITY</li>
368         *  <li>RenderingHints.VALUE_RENDER_SPEED</li>
369         * </ul>
370         */
371        public void setColorRendering(Object colorRendering) {
372            if (colorRendering != null && 
373                    !RenderingHints.KEY_COLOR_RENDERING.isCompatibleValue(colorRendering)) {
374                throw new IllegalArgumentException(colorRendering + " is not an acceptable value");
375            }
376            Object old = getColorRendering();
377            renderingHints.put(RenderingHints.KEY_COLOR_RENDERING, colorRendering);
378            firePropertyChange("colorRendering", old, getColorRendering());
379        }
380    
381        /**
382         * @return whether or not to dither
383         *          May be one of:
384         * <ul>
385         *  <li>RenderingHints.VALUE_DITHER_DEFAULT</li>
386         *  <li>RenderingHints.VALUE_DITHER_ENABLE</li>
387         *  <li>RenderingHints.VALUE_DITHER_DISABLE</li>
388         * </ul>
389         */
390        public Object getDithering() {
391            return renderingHints.get(RenderingHints.KEY_DITHERING);
392        }
393    
394        /**
395         * Sets whether or not to dither
396         * @param dithering
397         *          May be one of:
398         * <ul>
399         *  <li>RenderingHints.VALUE_DITHER_DEFAULT</li>
400         *  <li>RenderingHints.VALUE_DITHER_ENABLE</li>
401         *  <li>RenderingHints.VALUE_DITHER_DISABLE</li>
402         * </ul>
403         */
404        public void setDithering(Object dithering) {
405            if (dithering != null && 
406                    !RenderingHints.KEY_DITHERING.isCompatibleValue(dithering)) {
407                throw new IllegalArgumentException(dithering + " is not an acceptable value");
408            }
409            Object old = getDithering();
410            renderingHints.put(RenderingHints.KEY_DITHERING, dithering);
411            firePropertyChange("dithering", old, getDithering());
412        }
413    
414        /**
415         * @return whether or not to use fractional metrics
416         *          May be one of:
417         * <ul>
418         *  <li>RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT</li>
419         *  <li>RenderingHints.VALUE_FRACTIONALMETRICS_OFF</li>
420         *  <li>RenderingHints.VALUE_FRACTIONALMETRICS_ON</li>
421         * </ul>
422         */
423        public Object getFractionalMetrics() {
424            return renderingHints.get(RenderingHints.KEY_FRACTIONALMETRICS);
425        }
426    
427        /**
428         * Sets whether or not to use fractional metrics
429         *
430         * @param fractionalMetrics
431         *          May be one of:
432         * <ul>
433         *  <li>RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT</li>
434         *  <li>RenderingHints.VALUE_FRACTIONALMETRICS_OFF</li>
435         *  <li>RenderingHints.VALUE_FRACTIONALMETRICS_ON</li>
436         * </ul>
437         */
438        public void setFractionalMetrics(Object fractionalMetrics) {
439            if (fractionalMetrics != null && 
440                    !RenderingHints.KEY_FRACTIONALMETRICS.isCompatibleValue(fractionalMetrics)) {
441                throw new IllegalArgumentException(fractionalMetrics + " is not an acceptable value");
442            }
443            Object old = getFractionalMetrics();
444            renderingHints.put(RenderingHints.KEY_FRACTIONALMETRICS, fractionalMetrics);
445            firePropertyChange("fractionalMetrics", old, getFractionalMetrics());
446        }
447    
448        /**
449         * @return the technique to use for interpolation (used esp. when scaling)
450         *          May be one of:
451         * <ul>
452         *  <li>RenderingHints.VALUE_INTERPOLATION_BICUBIC</li>
453         *  <li>RenderingHints.VALUE_INTERPOLATION_BILINEAR</li>
454         *  <li>RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR</li>
455         * </ul>
456         */
457        public Object getInterpolation() {
458            return renderingHints.get(RenderingHints.KEY_INTERPOLATION);
459        }
460    
461        /**
462         * Sets the technique to use for interpolation (used esp. when scaling)
463         * @param interpolation
464         *          May be one of:
465         * <ul>
466         *  <li>RenderingHints.VALUE_INTERPOLATION_BICUBIC</li>
467         *  <li>RenderingHints.VALUE_INTERPOLATION_BILINEAR</li>
468         *  <li>RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR</li>
469         * </ul>
470         */
471        public void setInterpolation(Object interpolation) {
472            if (interpolation != null &&
473                    !RenderingHints.KEY_INTERPOLATION.isCompatibleValue(interpolation)) {
474                throw new IllegalArgumentException(interpolation + " is not an acceptable value");
475            }
476            Object old = getInterpolation();
477            renderingHints.put(RenderingHints.KEY_INTERPOLATION, interpolation);
478            firePropertyChange("interpolation", old, getInterpolation());
479        }
480    
481        /**
482         * @return a hint as to techniques to use with regards to rendering quality vs. speed
483         *          May be one of:
484         * <ul>
485         *  <li>RenderingHints.VALUE_RENDER_QUALITY</li>
486         *  <li>RenderingHints.VALUE_RENDER_SPEED</li>
487         *  <li>RenderingHints.VALUE_RENDER_DEFAULT</li>
488         * </ul>
489         */
490        public Object getRendering() {
491            return renderingHints.get(RenderingHints.KEY_RENDERING);
492        }
493    
494        /**
495         * Specifies a hint as to techniques to use with regards to rendering quality vs. speed
496         *
497         * @param rendering
498         *          May be one of:
499         * <ul>
500         *  <li>RenderingHints.VALUE_RENDER_QUALITY</li>
501         *  <li>RenderingHints.VALUE_RENDER_SPEED</li>
502         *  <li>RenderingHints.VALUE_RENDER_DEFAULT</li>
503         * </ul>
504         */
505        public void setRendering(Object rendering) {
506            if (rendering != null && 
507                    !RenderingHints.KEY_RENDERING.isCompatibleValue(rendering)) {
508                throw new IllegalArgumentException(rendering + " is not an acceptable value");
509            }
510            Object old = getRendering();
511            renderingHints.put(RenderingHints.KEY_RENDERING, rendering);
512            firePropertyChange("rendering", old, getRendering());
513        }
514    
515        /**
516         * @return technique for rendering strokes
517         *          May be one of:
518         * <ul>
519         *  <li>RenderingHints.VALUE_STROKE_DEFAULT</li>
520         *  <li>RenderingHints.VALUE_STROKE_NORMALIZE</li>
521         *  <li>RenderingHints.VALUE_STROKE_PURE</li>
522         * </ul>
523         */
524        public Object getStrokeControl() {
525            return renderingHints.get(RenderingHints.KEY_STROKE_CONTROL);
526        }
527    
528        /**
529         * Specifies a technique for rendering strokes
530         *
531         * @param strokeControl
532         *          May be one of:
533         * <ul>
534         *  <li>RenderingHints.VALUE_STROKE_DEFAULT</li>
535         *  <li>RenderingHints.VALUE_STROKE_NORMALIZE</li>
536         *  <li>RenderingHints.VALUE_STROKE_PURE</li>
537         * </ul>
538         */
539        public void setStrokeControl(Object strokeControl) {
540            if (strokeControl != null && 
541                    !RenderingHints.KEY_STROKE_CONTROL.isCompatibleValue(strokeControl)) {
542                throw new IllegalArgumentException(strokeControl + " is not an acceptable value");
543            }
544            Object old = getStrokeControl();
545            renderingHints.put(RenderingHints.KEY_STROKE_CONTROL, strokeControl);
546            firePropertyChange("strokeControl", old, getStrokeControl());
547        }
548    
549        /**
550         * @return technique for anti-aliasing text.
551         *          (TODO this needs to be updated for Mustang. You may use the
552         *           new Mustang values, and everything will work, but support in
553         *           the GUI builder and documentation need to be added once we
554         *           branch for Mustang)<br/>
555         *          May be one of:
556         * <ul>
557         *  <li>RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT</li>
558         *  <li>RenderingHints.VALUE_TEXT_ANTIALIAS_OFF</li>
559         *  <li>RenderingHints.VALUE_TEXT_ANTIALIAS_ON</li>
560         * </ul>
561         */
562        public Object getTextAntialiasing() {
563            return renderingHints.get(RenderingHints.KEY_TEXT_ANTIALIASING);
564        }
565    
566        /**
567         * Sets the technique for anti-aliasing text.
568         *          (TODO this needs to be updated for Mustang. You may use the
569         *           new Mustang values, and everything will work, but support in
570         *           the GUI builder and documentation need to be added once we
571         *           branch for Mustang)<br/>
572         *
573         * @param textAntialiasing
574         *          May be one of:
575         * <ul>
576         *  <li>RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT</li>
577         *  <li>RenderingHints.VALUE_TEXT_ANTIALIAS_OFF</li>
578         *  <li>RenderingHints.VALUE_TEXT_ANTIALIAS_ON</li>
579         * </ul>
580         */
581        public void setTextAntialiasing(Object textAntialiasing) {
582            if (textAntialiasing != null && 
583                    !RenderingHints.KEY_TEXT_ANTIALIASING.isCompatibleValue(textAntialiasing)) {
584                throw new IllegalArgumentException(textAntialiasing + " is not an acceptable value");
585            }
586            Object old = getTextAntialiasing();
587            renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, textAntialiasing);
588            firePropertyChange("textAntialiasing", old, getTextAntialiasing());
589        }
590    
591        /**
592         * @return the rendering hint associated with the given key. May return null
593         */
594        public Object getRenderingHint(RenderingHints.Key key) {
595            return renderingHints.get(key);
596        }
597    
598        /**
599         * Set the given hint for the given key. This will end up firing the appropriate
600         * property change event if the key is recognized. For example, if the key is
601         * RenderingHints.KEY_ANTIALIASING, then the setAntialiasing method will be
602         * called firing an "antialiasing" property change event if necessary. If
603         * the key is not recognized, no event will be fired but the key will be saved.
604         * The key must not be null
605         *
606         * @param key cannot be null
607         * @param hint must be a hint compatible with the given key
608         */
609        public void setRenderingHint(RenderingHints.Key key, Object hint) {
610            if (key == RenderingHints.KEY_ALPHA_INTERPOLATION) {
611                setAlphaInterpolation(hint);
612            } else if (key == RenderingHints.KEY_ANTIALIASING) {
613                setAntialiasing(hint);
614            } else if (key == RenderingHints.KEY_COLOR_RENDERING) {
615                setColorRendering(hint);
616            } else if (key == RenderingHints.KEY_DITHERING) {
617                setDithering(hint);
618            } else if (key == RenderingHints.KEY_FRACTIONALMETRICS) {
619                setFractionalMetrics(hint);
620            } else if (key == RenderingHints.KEY_INTERPOLATION) {
621                setInterpolation(hint);
622            } else if (key == RenderingHints.KEY_RENDERING) {
623                setRendering(hint);
624            } else if (key == RenderingHints.KEY_STROKE_CONTROL) {
625                setStrokeControl(hint);
626            } else if (key == RenderingHints.KEY_TEXT_ANTIALIASING) {
627                setTextAntialiasing(hint);
628            } else {
629                renderingHints.put(key, hint);
630            }
631        }
632    
633        /**
634         * @return a copy of the map of rendering hints held by this class. This
635         *         returned value will never be null
636         */
637        public Map<RenderingHints.Key,Object> getRenderingHints() {
638            return new HashMap<RenderingHints.Key,Object>(renderingHints);
639        }
640    
641        /**
642         * Sets the rendering hints to use. This will <strong>replace</strong> the
643         * rendering hints entirely, clearing any hints that were previously set.
644         *
645         * @param renderingHints map of hints. May be null. I null, a new Map of
646         * rendering hints will be created
647         */
648        public void setRenderingHints(Map<RenderingHints.Key,Object> renderingHints) {
649            if (renderingHints != null) {
650                this.renderingHints = new HashMap<RenderingHints.Key,Object>(renderingHints);
651            } else {
652                this.renderingHints = new HashMap<RenderingHints.Key, Object>();
653            }
654            firePropertyChange("renderingHints", null, getRenderingHints());
655        }
656        
657        /**
658         * Saves the state in the given Graphics2D object so that it may be
659         * restored later.
660         *
661         * @param g the Graphics2D object who's state will be saved
662         */
663        protected void saveState(Graphics2D g) {
664            oldPaint = g.getPaint();
665            oldFont = g.getFont();
666            oldStroke = g.getStroke();
667            oldTransform = g.getTransform();
668            oldComposite = g.getComposite();
669            oldClip = g.getClip();
670            oldBackground = g.getBackground();
671            oldColor = g.getColor();
672            
673            //save off the old rendering hints
674            oldRenderingHints = (RenderingHints)g.getRenderingHints().clone();
675            
676            stateSaved = true;
677        }
678        
679        /**
680         * Restores previously saved state. A call to saveState must have occured
681         * prior to calling restoreState, or an IllegalStateException will be thrown.
682         * 
683         * @param g the Graphics2D object to restore previously saved state to
684         */
685        protected void restoreState(Graphics2D g) {
686            if (!stateSaved) {
687                throw new IllegalStateException("A call to saveState must occur " +
688                        "prior to calling restoreState");
689            }
690            
691            g.setPaint(oldPaint);
692            g.setFont(oldFont);
693            g.setTransform(oldTransform);
694            g.setStroke(oldStroke);
695            g.setComposite(oldComposite);
696            g.setClip(oldClip);
697            g.setBackground(oldBackground);
698            g.setColor(oldColor);
699            
700            //restore the rendering hints
701            g.setRenderingHints(oldRenderingHints);
702            
703            stateSaved = false;
704        }
705            
706        /**
707         * @inheritDoc
708         */
709        public void paint(Graphics2D g, T component) {
710            saveState(g);
711            
712            configureGraphics(g, component);
713            
714            //if I am cacheing, and the cache is not null, and the image has the
715            //same dimensions as the component, then simply paint the image
716            BufferedImage image = cachedImage == null ? null : cachedImage.get();
717            if (isUseCache() && image != null 
718                    && image.getWidth() == component.getWidth()
719                    && image.getHeight() == component.getHeight()) {
720                g.drawImage(image, 0, 0, null);
721            } else {
722                Effect[] effects = getEffects();
723                if (effects.length > 0 || isUseCache()) {
724                    image = PaintUtils.createCompatibleImage(
725                            component.getWidth(),
726                            component.getHeight(),
727                            Transparency.TRANSLUCENT);
728                    
729                    Graphics2D gfx = image.createGraphics();
730                    configureGraphics(gfx, component);
731                    paintBackground(gfx, component);
732                    gfx.dispose();
733    
734                    for (Effect effect : effects) {
735                        image = effect.apply(image);
736                    }
737                    
738                    g.drawImage(image, 0, 0, null);
739                    
740                    if (isUseCache()) {
741                        cachedImage = new SoftReference<BufferedImage>(image);
742                    }
743                } else {
744                    paintBackground(g, component);
745                }
746            }
747            
748            restoreState(g);
749        }
750        
751        /**
752         * Utility method for configuring the given Graphics2D with the rendering hints,
753         * composite, and clip
754         */
755        private void configureGraphics(Graphics2D g, T c) {
756            Map<RenderingHints.Key,Object> hints = getRenderingHints();
757            //merge these hints with the existing ones, otherwise I won't inherit
758            //any of the hints from the Graphics2D
759            for (Object key : hints.keySet()) {
760                Object value = hints.get(key);
761                if (value != null) {
762                    g.setRenderingHint((RenderingHints.Key)key, hints.get(key));
763                }
764            }
765    
766            if (getComposite() != null) {
767                g.setComposite(getComposite());
768            }
769            Shape clip = getClip();
770            if (clip != null) {
771                //resize the clip if necessary
772                double width = 1;
773                double height = 1;
774                Resize resizeClip = getResizeClip();
775                if (resizeClip == Resize.HORIZONTAL || resizeClip == Resize.BOTH) {
776                    width = c.getWidth();
777                }
778                if (resizeClip == Resize.VERTICAL || resizeClip == Resize.BOTH) {
779                    height = c.getHeight();
780                }
781                if (clip instanceof RoundRectangle2D) {
782                    RoundRectangle2D rect = (RoundRectangle2D)clip;
783                    clip = new RoundRectangle2D.Double(
784                            rect.getX(), rect.getY(), width, height,
785                            rect.getArcWidth(), rect.getArcHeight());
786                } else {
787                    clip = AffineTransform.getScaleInstance(
788                            width, height).createTransformedShape(clip);
789                }
790                g.setClip(clip);
791            }
792        }
793    
794        /**
795         * Subclasses should implement this method and perform custom painting operations
796         * here. Common behavior, such as setting the clip and composite, saving and restoring
797         * state, is performed in the "paint" method automatically, and then delegated here.
798         *
799         * @param g The Graphics2D object in which to paint
800         * @param component The JComponent that the Painter is delegate for.
801         */
802        protected abstract void paintBackground(Graphics2D g, T component);
803    }