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 }