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