001 /* 002 * $Id: DropShadowBorder.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.border; 023 024 import org.jdesktop.swingx.graphics.GraphicsUtilities; 025 026 import javax.swing.border.Border; 027 import java.awt.*; 028 import java.awt.geom.RoundRectangle2D; 029 import java.awt.image.BufferedImage; 030 import java.awt.image.ConvolveOp; 031 import java.awt.image.Kernel; 032 import java.io.Serializable; 033 import java.util.HashMap; 034 import java.util.Map; 035 036 /** 037 * Implements a DropShadow for components. In general, the DropShadowBorder will 038 * work with any rectangular components that do not have a default border 039 * installed as part of the look and feel, or otherwise. For example, 040 * DropShadowBorder works wonderfully with JPanel, but horribly with JComboBox. 041 * <p> 042 * Note: {@code DropShadowBorder} should usually be added to non-opaque 043 * components, otherwise the background is likely to bleed through.</p> 044 * <p>Note: Since generating drop shadows is relatively expensive operation, 045 * {@code DropShadowBorder} keeps internal static cache that allows sharing 046 * same border for multiple re-rendering and between different instances of the 047 * class. Since this cache is shared at class level and never reset, it might 048 * bleed your app memory in case you tend to create many different borders 049 * rapidly.</p> 050 * @author rbair 051 */ 052 public class DropShadowBorder implements Border, Serializable { 053 /** 054 * 055 */ 056 private static final long serialVersionUID = 715287754750604058L; 057 058 private static enum Position {TOP, TOP_LEFT, LEFT, BOTTOM_LEFT, 059 BOTTOM, BOTTOM_RIGHT, RIGHT, TOP_RIGHT} 060 061 private static final Map<Double,Map<Position,BufferedImage>> CACHE 062 = new HashMap<Double,Map<Position,BufferedImage>>(); 063 064 private Color shadowColor; 065 public void setShadowColor(Color shadowColor) { 066 this.shadowColor = shadowColor; 067 } 068 069 public void setShadowSize(int shadowSize) { 070 this.shadowSize = shadowSize; 071 } 072 073 public void setShadowOpacity(float shadowOpacity) { 074 this.shadowOpacity = shadowOpacity; 075 } 076 077 public void setCornerSize(int cornerSize) { 078 this.cornerSize = cornerSize; 079 } 080 081 public void setShowTopShadow(boolean showTopShadow) { 082 this.showTopShadow = showTopShadow; 083 } 084 085 public void setShowLeftShadow(boolean showLeftShadow) { 086 this.showLeftShadow = showLeftShadow; 087 } 088 089 public void setShowBottomShadow(boolean showBottomShadow) { 090 this.showBottomShadow = showBottomShadow; 091 } 092 093 public void setShowRightShadow(boolean showRightShadow) { 094 this.showRightShadow = showRightShadow; 095 } 096 097 private int shadowSize; 098 private float shadowOpacity; 099 private int cornerSize; 100 private boolean showTopShadow; 101 private boolean showLeftShadow; 102 private boolean showBottomShadow; 103 private boolean showRightShadow; 104 105 public DropShadowBorder() { 106 this(Color.BLACK, 5); 107 } 108 109 public DropShadowBorder(Color shadowColor, int shadowSize) { 110 this(shadowColor, shadowSize, .5f, 12, false, false, true, true); 111 } 112 113 public DropShadowBorder(boolean showLeftShadow) { 114 this(Color.BLACK, 5, .5f, 12, false, showLeftShadow, true, true); 115 } 116 117 public DropShadowBorder(Color shadowColor, int shadowSize, 118 float shadowOpacity, int cornerSize, boolean showTopShadow, 119 boolean showLeftShadow, boolean showBottomShadow, boolean showRightShadow) { 120 this.shadowColor = shadowColor; 121 this.shadowSize = shadowSize; 122 this.shadowOpacity = shadowOpacity; 123 this.cornerSize = cornerSize; 124 this.showTopShadow = showTopShadow; 125 this.showLeftShadow = showLeftShadow; 126 this.showBottomShadow = showBottomShadow; 127 this.showRightShadow = showRightShadow; 128 } 129 130 /** 131 * {@inheritDoc} 132 */ 133 public void paintBorder(Component c, Graphics graphics, int x, int y, int width, int height) { 134 /* 135 * 1) Get images for this border 136 * 2) Paint the images for each side of the border that should be painted 137 */ 138 Map<Position,BufferedImage> images = getImages((Graphics2D)graphics); 139 140 Graphics2D g2 = (Graphics2D)graphics.create(); 141 142 try { 143 //The location and size of the shadows depends on which shadows are being 144 //drawn. For instance, if the left & bottom shadows are being drawn, then 145 //the left shadow extends all the way down to the corner, a corner is drawn, 146 //and then the bottom shadow begins at the corner. If, however, only the 147 //bottom shadow is drawn, then the bottom-left corner is drawn to the 148 //right of the corner, and the bottom shadow is somewhat shorter than before. 149 150 int shadowOffset = 2; //the distance between the shadow and the edge 151 152 Point topLeftShadowPoint = null; 153 if (showLeftShadow || showTopShadow) { 154 topLeftShadowPoint = new Point(); 155 if (showLeftShadow && !showTopShadow) { 156 topLeftShadowPoint.setLocation(x, y + shadowOffset); 157 } else if (showLeftShadow && showTopShadow) { 158 topLeftShadowPoint.setLocation(x, y); 159 } else if (!showLeftShadow && showTopShadow) { 160 topLeftShadowPoint.setLocation(x + shadowSize, y); 161 } 162 } 163 164 Point bottomLeftShadowPoint = null; 165 if (showLeftShadow || showBottomShadow) { 166 bottomLeftShadowPoint = new Point(); 167 if (showLeftShadow && !showBottomShadow) { 168 bottomLeftShadowPoint.setLocation(x, y + height - shadowSize - shadowSize); 169 } else if (showLeftShadow && showBottomShadow) { 170 bottomLeftShadowPoint.setLocation(x, y + height - shadowSize); 171 } else if (!showLeftShadow && showBottomShadow) { 172 bottomLeftShadowPoint.setLocation(x + shadowSize, y + height - shadowSize); 173 } 174 } 175 176 Point bottomRightShadowPoint = null; 177 if (showRightShadow || showBottomShadow) { 178 bottomRightShadowPoint = new Point(); 179 if (showRightShadow && !showBottomShadow) { 180 bottomRightShadowPoint.setLocation(x + width - shadowSize, y + height - shadowSize - shadowSize); 181 } else if (showRightShadow && showBottomShadow) { 182 bottomRightShadowPoint.setLocation(x + width - shadowSize, y + height - shadowSize); 183 } else if (!showRightShadow && showBottomShadow) { 184 bottomRightShadowPoint.setLocation(x + width - shadowSize - shadowSize, y + height - shadowSize); 185 } 186 } 187 188 Point topRightShadowPoint = null; 189 if (showRightShadow || showTopShadow) { 190 topRightShadowPoint = new Point(); 191 if (showRightShadow && !showTopShadow) { 192 topRightShadowPoint.setLocation(x + width - shadowSize, y + shadowOffset); 193 } else if (showRightShadow && showTopShadow) { 194 topRightShadowPoint.setLocation(x + width - shadowSize, y); 195 } else if (!showRightShadow && showTopShadow) { 196 topRightShadowPoint.setLocation(x + width - shadowSize - shadowSize, y); 197 } 198 } 199 200 g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 201 RenderingHints.VALUE_INTERPOLATION_BILINEAR); 202 g2.setRenderingHint(RenderingHints.KEY_RENDERING, 203 RenderingHints.VALUE_RENDER_SPEED); 204 205 if (showLeftShadow) { 206 Rectangle leftShadowRect = 207 new Rectangle(x, 208 topLeftShadowPoint.y + shadowSize, 209 shadowSize, 210 bottomLeftShadowPoint.y - topLeftShadowPoint.y - shadowSize); 211 g2.drawImage(images.get(Position.LEFT), 212 leftShadowRect.x, leftShadowRect.y, 213 leftShadowRect.width, leftShadowRect.height, null); 214 } 215 216 if (showBottomShadow) { 217 Rectangle bottomShadowRect = 218 new Rectangle(bottomLeftShadowPoint.x + shadowSize, 219 y + height - shadowSize, 220 bottomRightShadowPoint.x - bottomLeftShadowPoint.x - shadowSize, 221 shadowSize); 222 g2.drawImage(images.get(Position.BOTTOM), 223 bottomShadowRect.x, bottomShadowRect.y, 224 bottomShadowRect.width, bottomShadowRect.height, null); 225 } 226 227 if (showRightShadow) { 228 Rectangle rightShadowRect = 229 new Rectangle(x + width - shadowSize, 230 topRightShadowPoint.y + shadowSize, 231 shadowSize, 232 bottomRightShadowPoint.y - topRightShadowPoint.y - shadowSize); 233 g2.drawImage(images.get(Position.RIGHT), 234 rightShadowRect.x, rightShadowRect.y, 235 rightShadowRect.width, rightShadowRect.height, null); 236 } 237 238 if (showTopShadow) { 239 Rectangle topShadowRect = 240 new Rectangle(topLeftShadowPoint.x + shadowSize, 241 y, 242 topRightShadowPoint.x - topLeftShadowPoint.x - shadowSize, 243 shadowSize); 244 g2.drawImage(images.get(Position.TOP), 245 topShadowRect.x, topShadowRect.y, 246 topShadowRect.width, topShadowRect.height, null); 247 } 248 249 if (showLeftShadow || showTopShadow) { 250 g2.drawImage(images.get(Position.TOP_LEFT), 251 topLeftShadowPoint.x, topLeftShadowPoint.y, null); 252 } 253 if (showLeftShadow || showBottomShadow) { 254 g2.drawImage(images.get(Position.BOTTOM_LEFT), 255 bottomLeftShadowPoint.x, bottomLeftShadowPoint.y, null); 256 } 257 if (showRightShadow || showBottomShadow) { 258 g2.drawImage(images.get(Position.BOTTOM_RIGHT), 259 bottomRightShadowPoint.x, bottomRightShadowPoint.y, null); 260 } 261 if (showRightShadow || showTopShadow) { 262 g2.drawImage(images.get(Position.TOP_RIGHT), 263 topRightShadowPoint.x, topRightShadowPoint.y, null); 264 } 265 } finally { 266 g2.dispose(); 267 } 268 } 269 270 private Map<Position,BufferedImage> getImages(Graphics2D g2) { 271 //first, check to see if an image for this size has already been rendered 272 //if so, use the cache. Else, draw and save 273 Map<Position,BufferedImage> images = CACHE.get(shadowSize + (shadowColor.hashCode() * .3) + (shadowOpacity * .12));//TODO do a real hash 274 if (images == null) { 275 images = new HashMap<Position,BufferedImage>(); 276 277 /* 278 * To draw a drop shadow, I have to: 279 * 1) Create a rounded rectangle 280 * 2) Create a BufferedImage to draw the rounded rect in 281 * 3) Translate the graphics for the image, so that the rectangle 282 * is centered in the drawn space. The border around the rectangle 283 * needs to be shadowWidth wide, so that there is space for the 284 * shadow to be drawn. 285 * 4) Draw the rounded rect as shadowColor, with an opacity of shadowOpacity 286 * 5) Create the BLUR_KERNEL 287 * 6) Blur the image 288 * 7) copy off the corners, sides, etc into images to be used for 289 * drawing the Border 290 */ 291 int rectWidth = cornerSize + 1; 292 RoundRectangle2D rect = new RoundRectangle2D.Double(0, 0, rectWidth, rectWidth, cornerSize, cornerSize); 293 int imageWidth = rectWidth + shadowSize * 2; 294 BufferedImage image = GraphicsUtilities.createCompatibleTranslucentImage(imageWidth, imageWidth); 295 Graphics2D buffer = (Graphics2D)image.getGraphics(); 296 297 try { 298 buffer.setPaint(new Color(shadowColor.getRed(), shadowColor.getGreen(), 299 shadowColor.getBlue(), (int)(shadowOpacity * 255))); 300 // buffer.setColor(new Color(0.0f, 0.0f, 0.0f, shadowOpacity)); 301 buffer.translate(shadowSize, shadowSize); 302 buffer.fill(rect); 303 } finally { 304 buffer.dispose(); 305 } 306 307 float blurry = 1.0f / (float)(shadowSize * shadowSize); 308 float[] blurKernel = new float[shadowSize * shadowSize]; 309 for (int i=0; i<blurKernel.length; i++) { 310 blurKernel[i] = blurry; 311 } 312 ConvolveOp blur = new ConvolveOp(new Kernel(shadowSize, shadowSize, blurKernel)); 313 BufferedImage targetImage = GraphicsUtilities.createCompatibleTranslucentImage(imageWidth, imageWidth); 314 ((Graphics2D)targetImage.getGraphics()).drawImage(image, blur, -(shadowSize/2), -(shadowSize/2)); 315 316 int x = 1; 317 int y = 1; 318 int w = shadowSize; 319 int h = shadowSize; 320 images.put(Position.TOP_LEFT, getSubImage(targetImage, x, y, w, h)); 321 x = 1; 322 y = h; 323 w = shadowSize; 324 h = 1; 325 images.put(Position.LEFT, getSubImage(targetImage, x, y, w, h)); 326 x = 1; 327 y = rectWidth; 328 w = shadowSize; 329 h = shadowSize; 330 images.put(Position.BOTTOM_LEFT, getSubImage(targetImage, x, y, w, h)); 331 x = cornerSize + 1; 332 y = rectWidth; 333 w = 1; 334 h = shadowSize; 335 images.put(Position.BOTTOM, getSubImage(targetImage, x, y, w, h)); 336 x = rectWidth; 337 y = x; 338 w = shadowSize; 339 h = shadowSize; 340 images.put(Position.BOTTOM_RIGHT, getSubImage(targetImage, x, y, w, h)); 341 x = rectWidth; 342 y = cornerSize + 1; 343 w = shadowSize; 344 h = 1; 345 images.put(Position.RIGHT, getSubImage(targetImage, x, y, w, h)); 346 x = rectWidth; 347 y = 1; 348 w = shadowSize; 349 h = shadowSize; 350 images.put(Position.TOP_RIGHT, getSubImage(targetImage, x, y, w, h)); 351 x = shadowSize; 352 y = 1; 353 w = 1; 354 h = shadowSize; 355 images.put(Position.TOP, getSubImage(targetImage, x, y, w, h)); 356 357 image.flush(); 358 CACHE.put(shadowSize + (shadowColor.hashCode() * .3) + (shadowOpacity * .12), images); //TODO do a real hash 359 } 360 return images; 361 } 362 363 /** 364 * Returns a new BufferedImage that represents a subregion of the given 365 * BufferedImage. (Note that this method does not use 366 * BufferedImage.getSubimage(), which will defeat image acceleration 367 * strategies on later JDKs.) 368 */ 369 private BufferedImage getSubImage(BufferedImage img, 370 int x, int y, int w, int h) { 371 BufferedImage ret = GraphicsUtilities.createCompatibleTranslucentImage(w, h); 372 Graphics2D g2 = ret.createGraphics(); 373 374 try { 375 g2.drawImage(img, 376 0, 0, w, h, 377 x, y, x+w, y+h, 378 null); 379 } finally { 380 g2.dispose(); 381 } 382 383 return ret; 384 } 385 386 /** 387 * @inheritDoc 388 */ 389 public Insets getBorderInsets(Component c) { 390 int top = showTopShadow ? shadowSize : 0; 391 int left = showLeftShadow ? shadowSize : 0; 392 int bottom = showBottomShadow ? shadowSize : 0; 393 int right = showRightShadow ? shadowSize : 0; 394 return new Insets(top, left, bottom, right); 395 } 396 397 /** 398 * {@inheritDoc} 399 */ 400 public boolean isBorderOpaque() { 401 return false; 402 } 403 404 public boolean isShowTopShadow() { 405 return showTopShadow; 406 } 407 408 public boolean isShowLeftShadow() { 409 return showLeftShadow; 410 } 411 412 public boolean isShowRightShadow() { 413 return showRightShadow; 414 } 415 416 public boolean isShowBottomShadow() { 417 return showBottomShadow; 418 } 419 420 public int getShadowSize() { 421 return shadowSize; 422 } 423 424 public Color getShadowColor() { 425 return shadowColor; 426 } 427 428 public float getShadowOpacity() { 429 return shadowOpacity; 430 } 431 432 public int getCornerSize() { 433 return cornerSize; 434 } 435 }