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    }