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}