001 /* 002 * $Id: AbstractPainter.java 3256 2009-02-10 20:09:41Z kschaefe $ 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.*; 025 import java.awt.image.BufferedImage; 026 import java.awt.image.BufferedImageOp; 027 import java.lang.ref.SoftReference; 028 029 import org.jdesktop.beans.AbstractBean; 030 import org.jdesktop.swingx.graphics.GraphicsUtilities; 031 032 /** 033 * <p>A convenient base class from which concrete {@link Painter} implementations may 034 * extend. It extends {@link org.jdesktop.beans.AbstractBean} as a convenience for 035 * adding property change notification support. In addition, <code>AbstractPainter</code> 036 * provides subclasses with the ability to cacheable painting operations, configure the 037 * drawing surface with common settings (such as antialiasing and interpolation), and 038 * toggle whether a subclass paints or not via the <code>visibility</code> property.</p> 039 * 040 * <p>Subclasses of <code>AbstractPainter</code> generally need only override the 041 * {@link #doPaint(Graphics2D, Object, int, int)} method. If a subclass requires more control 042 * over whether cacheing is enabled, or for configuring the graphics state, then it 043 * may override the appropriate protected methods to interpose its own behavior.</p> 044 * 045 * <p>For example, here is the doPaint method of a simple <code>Painter</code> that 046 * paints an opaque rectangle: 047 * <pre><code> 048 * public void doPaint(Graphics2D g, T obj, int width, int height) { 049 * g.setPaint(Color.BLUE); 050 * g.fillRect(0, 0, width, height); 051 * } 052 * </code></pre></p> 053 * 054 * @author rbair 055 */ 056 public abstract class AbstractPainter<T> extends AbstractBean implements Painter<T> { 057 /** 058 * An enum representing the possible interpolation values of Bicubic, Bilinear, and 059 * Nearest Neighbor. These map to the underlying RenderingHints, 060 * but are easier to use and serialization safe. 061 */ 062 public enum Interpolation { 063 /** 064 * use bicubic interpolation 065 */ 066 Bicubic(RenderingHints.VALUE_INTERPOLATION_BICUBIC), 067 /** 068 * use bilinear interpolation 069 */ 070 Bilinear(RenderingHints.VALUE_INTERPOLATION_BILINEAR), 071 /** 072 * use nearest neighbor interpolation 073 */ 074 NearestNeighbor(RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); 075 076 private Object value; 077 Interpolation(Object value) { 078 this.value = value; 079 } 080 private void configureGraphics(Graphics2D g) { 081 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, value); 082 } 083 } 084 085 //--------------------------------------------------- Instance Variables 086 /** 087 * The cached image, if shouldUseCache() returns true 088 */ 089 private transient SoftReference<BufferedImage> cachedImage; 090 private boolean cacheCleared = true; 091 private boolean cacheable = false; 092 private boolean dirty = false; 093 private BufferedImageOp[] filters = new BufferedImageOp[0]; 094 private boolean antialiasing = true; 095 private Interpolation interpolation = Interpolation.NearestNeighbor; 096 private boolean visible = true; 097 098 /** 099 * Creates a new instance of AbstractPainter. 100 */ 101 public AbstractPainter() { } 102 103 /** 104 * Creates a new instance of AbstractPainter. 105 * @param cacheable indicates if this painter should be cacheable 106 */ 107 public AbstractPainter(boolean cacheable) { 108 setCacheable(cacheable); 109 } 110 111 /** 112 * A defensive copy of the Effects to apply to the results 113 * of the AbstractPainter's painting operation. The array may 114 * be empty but it will never be null. 115 * @return the array of filters applied to this painter 116 */ 117 public final BufferedImageOp[] getFilters() { 118 BufferedImageOp[] results = new BufferedImageOp[filters.length]; 119 System.arraycopy(filters, 0, results, 0, results.length); 120 return results; 121 } 122 123 /** 124 * <p>A convenience method for specifying the filters to use based on 125 * BufferedImageOps. These will each be individually wrapped by an ImageFilter 126 * and then setFilters(Effect... filters) will be called with the resulting 127 * array</p> 128 * 129 * 130 * @param effects the BufferedImageOps to wrap as filters 131 */ 132 public void setFilters(BufferedImageOp ... effects) { 133 if (effects == null) effects = new BufferedImageOp[0]; 134 BufferedImageOp[] old = getFilters(); 135 this.filters = new BufferedImageOp[effects == null ? 0 : effects.length]; 136 System.arraycopy(effects, 0, this.filters, 0, this.filters.length); 137 setDirty(true); 138 firePropertyChange("filters", old, getFilters()); 139 } 140 141 /** 142 * Returns if antialiasing is turned on or not. The default value is true. 143 * This is a bound property. 144 * @return the current antialiasing setting 145 */ 146 public boolean isAntialiasing() { 147 return antialiasing; 148 } 149 /** 150 * Sets the antialiasing setting. This is a bound property. 151 * @param value the new antialiasing setting 152 */ 153 public void setAntialiasing(boolean value) { 154 boolean old = isAntialiasing(); 155 antialiasing = value; 156 if (old != value) setDirty(true); 157 firePropertyChange("antialiasing", old, isAntialiasing()); 158 } 159 160 /** 161 * Gets the current interpolation setting. This property determines if interpolation will 162 * be used when drawing scaled images. @see java.awt.RenderingHints.KEY_INTERPOLATION. 163 * @return the current interpolation setting 164 */ 165 public Interpolation getInterpolation() { 166 return interpolation; 167 } 168 169 /** 170 * Sets a new value for the interpolation setting. This setting determines if interpolation 171 * should be used when drawing scaled images. @see java.awt.RenderingHints.KEY_INTERPOLATION. 172 * @param value the new interpolation setting 173 */ 174 public void setInterpolation(Interpolation value) { 175 Object old = getInterpolation(); 176 this.interpolation = value == null ? Interpolation.NearestNeighbor : value; 177 if (old != value) setDirty(true); 178 firePropertyChange("interpolation", old, getInterpolation()); 179 } 180 181 /** 182 * Gets the visible property. This controls if the painter should 183 * paint itself. It is true by default. Setting visible to false 184 * is good when you want to temporarily turn off a painter. An example 185 * of this is a painter that you only use when a button is highlighted. 186 * 187 * @return current value of visible property 188 */ 189 public boolean isVisible() { 190 return this.visible; 191 } 192 193 /** 194 * <p>Sets the visible property. This controls if the painter should 195 * paint itself. It is true by default. Setting visible to false 196 * is good when you want to temporarily turn off a painter. An example 197 * of this is a painter that you only use when a button is highlighted.</p> 198 * 199 * @param visible New value of visible property. 200 */ 201 public void setVisible(boolean visible) { 202 boolean old = isVisible(); 203 this.visible = visible; 204 if (old != visible) setDirty(true); //not the most efficient, but I must do this otherwise a CompoundPainter 205 //or other aggregate painter won't know that it is now invalid 206 //there might be a tricky solution but that is a performance optimization 207 firePropertyChange("visible", old, isVisible()); 208 } 209 210 /** 211 * <p>Gets whether this <code>AbstractPainter</code> can be cached as an image. 212 * If cacheing is enabled, then it is the responsibility of the developer to 213 * invalidate the painter (via {@link #clearCache}) if external state has 214 * changed in such a way that the painter is invalidated and needs to be 215 * repainted.</p> 216 * 217 * @return whether this is cacheable 218 */ 219 public boolean isCacheable() { 220 return cacheable; 221 } 222 223 /** 224 * <p>Sets whether this <code>AbstractPainter</code> can be cached as an image. 225 * If true, this is treated as a hint. That is, a cacheable may or may not be used. 226 * The {@link #shouldUseCache} method actually determines whether the cacheable is used. 227 * However, if false, then this is treated as an absolute value. That is, no 228 * cacheable will be used.</p> 229 * 230 * <p>If set to false, then #clearCache is called to free system resources.</p> 231 * 232 * @param cacheable 233 */ 234 public void setCacheable(boolean cacheable) { 235 boolean old = isCacheable(); 236 this.cacheable = cacheable; 237 firePropertyChange("cacheable", old, isCacheable()); 238 if (!isCacheable()) { 239 clearCache(); 240 } 241 } 242 243 /** 244 * <p>Call this method to clear the cacheable. This may be called whether there is 245 * a cacheable being used or not. If cleared, on the next call to <code>paint</code>, 246 * the painting routines will be called.</p> 247 * 248 * <p><strong>Subclasses</strong>If overridden in subclasses, you 249 * <strong>must</strong> call super.clearCache, or physical 250 * resources (such as an Image) may leak.</p> 251 */ 252 public void clearCache() { 253 BufferedImage cache = cachedImage == null ? null : cachedImage.get(); 254 if (cache != null) { 255 cache.flush(); 256 } 257 cacheCleared = true; 258 if (!isCacheable()) { 259 cachedImage = null; 260 } 261 } 262 263 /** 264 * Only made package private for testing. Don't call this method outside 265 * of this class! This is NOT a bound property 266 */ 267 boolean isCacheCleared() { 268 return cacheCleared; 269 } 270 271 /** 272 * <p>Called to allow <code>Painter</code> subclasses a chance to see if any state 273 * in the given object has changed from the last paint operation. If it has, then 274 * the <code>Painter</code> has a chance to mark itself as dirty, thus causing a 275 * repaint, even if cached.</p> 276 * 277 * @param object 278 */ 279 protected void validate(T object) { } 280 281 /** 282 * Ye olde dirty bit. If true, then the painter is considered dirty and in need of 283 * being repainted. This is a bound property. 284 * 285 * @return true if the painter state has changed and the painter needs to be 286 * repainted. 287 */ 288 protected boolean isDirty() { 289 return dirty; 290 } 291 292 /** 293 * Sets the dirty bit. If true, then the painter is considered dirty, and the cache 294 * will be cleared. This property is bound. 295 * 296 * @param d whether this <code>Painter</code> is dirty. 297 */ 298 protected void setDirty(boolean d) { 299 boolean old = isDirty(); 300 this.dirty = d; 301 firePropertyChange("dirty", old, isDirty()); 302 if (isDirty()) { 303 clearCache(); 304 } 305 } 306 307 /** 308 * <p>Returns true if the painter should use caching. This method allows subclasses to 309 * specify the heuristics regarding whether to cache or not. If a <code>Painter</code> 310 * has intelligent rules regarding painting times, and can more accurately indicate 311 * whether it should be cached, it could implement that logic in this method.</p> 312 * 313 * @return whether or not a cache should be used 314 */ 315 protected boolean shouldUseCache() { 316 return isCacheable() || filters.length > 0; //NOTE, I can only do this because getFilters() is final 317 } 318 319 /** 320 * <p>This method is called by the <code>paint</code> method prior to 321 * any drawing operations to configure the drawing surface. The default 322 * implementation sets the rendering hints that have been specified for 323 * this <code>AbstractPainter</code>.</p> 324 * 325 * <p>This method can be overriden by subclasses to modify the drawing 326 * surface before any painting happens.</p> 327 * 328 * @param g the graphics surface to configure. This will never be null. 329 * @see #paint(Graphics2D, Object, int, int) 330 */ 331 protected void configureGraphics(Graphics2D g) { 332 //configure antialiasing 333 if(isAntialiasing()) { 334 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 335 RenderingHints.VALUE_ANTIALIAS_ON); 336 } else { 337 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 338 RenderingHints.VALUE_ANTIALIAS_OFF); 339 } 340 341 getInterpolation().configureGraphics(g); 342 } 343 344 345 /** 346 * Subclasses must implement this method and perform custom painting operations 347 * here. 348 * @param width 349 * @param height 350 * @param g The Graphics2D object in which to paint 351 * @param object 352 */ 353 protected abstract void doPaint(Graphics2D g, T object, int width, int height); 354 355 /** 356 * @inheritDoc 357 */ 358 public final void paint(Graphics2D g, T obj, int width, int height) { 359 if (g == null) { 360 throw new NullPointerException("The Graphics2D must be supplied"); 361 } 362 363 if(!isVisible() || width < 1 || height < 1) { 364 return; 365 } 366 367 configureGraphics(g); 368 369 //paint to a temporary image if I'm caching, or if there are filters to apply 370 if (shouldUseCache() || filters.length > 0) { 371 validate(obj); 372 BufferedImage cache = cachedImage == null ? null : cachedImage.get(); 373 boolean invalidCache = null == cache || 374 cache.getWidth() != width || 375 cache.getHeight() != height; 376 377 if (cacheCleared || invalidCache || isDirty()) { 378 //rebuild the cacheable. I do this both if a cacheable is needed, and if any 379 //filters exist. I only *save* the resulting image if caching is turned on 380 if (invalidCache) { 381 cache = GraphicsUtilities.createCompatibleTranslucentImage(width, height); 382 } 383 Graphics2D gfx = cache.createGraphics(); 384 385 try { 386 gfx.setClip(0, 0, width, height); 387 388 if (!invalidCache) { 389 // If we are doing a repaint, but we didn't have to 390 // recreate the image, we need to clear it back 391 // to a fully transparent background. 392 Composite composite = gfx.getComposite(); 393 gfx.setComposite(AlphaComposite.Clear); 394 gfx.fillRect(0, 0, width, height); 395 gfx.setComposite(composite); 396 } 397 398 configureGraphics(gfx); 399 doPaint(gfx, obj, width, height); 400 } finally { 401 gfx.dispose(); 402 } 403 404 for (BufferedImageOp f : getFilters()) { 405 cache = f.filter(cache, null); 406 } 407 408 //only save the temporary image as the cacheable if I'm caching 409 if (shouldUseCache()) { 410 cachedImage = new SoftReference<BufferedImage>(cache); 411 cacheCleared = false; 412 } 413 } 414 415 g.drawImage(cache, 0, 0, null); 416 } else { 417 //can't use the cacheable, so just paint 418 doPaint(g, obj, width, height); 419 } 420 421 //painting has occured, so restore the dirty bit to false 422 setDirty(false); 423 } 424 }