001    /*
002     * $Id: JXImagePanel.java 3353 2009-05-28 02:07:01Z 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;
023    
024    import java.awt.Cursor;
025    import java.awt.Dimension;
026    import java.awt.Graphics;
027    import java.awt.Graphics2D;
028    import java.awt.Image;
029    import java.awt.Insets;
030    import java.awt.Rectangle;
031    import java.awt.event.MouseAdapter;
032    import java.awt.event.MouseEvent;
033    import java.io.File;
034    import java.lang.ref.SoftReference;
035    import java.net.URL;
036    import java.util.concurrent.Callable;
037    import java.util.concurrent.ExecutionException;
038    import java.util.concurrent.ExecutorService;
039    import java.util.concurrent.Executors;
040    import java.util.concurrent.FutureTask;
041    import java.util.logging.Level;
042    import java.util.logging.Logger;
043    
044    import javax.imageio.ImageIO;
045    import javax.swing.ImageIcon;
046    import javax.swing.JFileChooser;
047    import javax.swing.SwingUtilities;
048    
049    /**
050     * <p>
051     * A panel that draws an image. The standard mode is to draw the specified image
052     * centered and unscaled. The component&amp;s preferred size is based on the
053     * image, unless explicitly set by the user.
054     * </p>
055     * <p>
056     * Images to be displayed can be set based on URL, Image, etc. This is
057     * accomplished by passing in an image loader.
058     * 
059     * <pre>
060     * public class URLImageLoader extends Callable&lt;Image&gt; {
061     *     private URL url;
062     * 
063     *     public URLImageLoader(URL url) {
064     *         url.getClass(); //null check
065     *         this.url = url;
066     *     }
067     * 
068     *     public Image call() throws Exception {
069     *         return ImageIO.read(url);
070     *     }
071     * }
072     * 
073     * imagePanel.setImageLoader(new URLImageLoader(url));
074     * </pre>
075     * 
076     * </p>
077     * <p>
078     * This component also supports allowing the user to set the image. If the
079     * <code>JXImagePanel</code> is editable, then when the user clicks on the
080     * <code>JXImagePanel</code> a FileChooser is shown allowing the user to pick
081     * some other image to use within the <code>JXImagePanel</code>.
082     * </p>
083     * <p>
084     * TODO In the future, the JXImagePanel will also support tiling of images,
085     * scaling, resizing, cropping, segues etc.
086     * </p>
087     * <p>
088     * TODO other than the image loading this component can be replicated by a
089     * JXPanel with the appropriate Painter. What's the point?
090     * </p>
091     * 
092     * @author rbair
093     */
094    public class JXImagePanel extends JXPanel {
095        public static enum Style {
096            CENTERED, TILED, SCALED, SCALED_KEEP_ASPECT_RATIO
097        }
098    
099        private static final Logger LOG = Logger.getLogger(JXImagePanel.class.getName());
100    
101        /**
102         * Text informing the user that clicking on this component will allow them
103         * to set the image
104         */
105        private static final String TEXT = "<html><i><b>Click here<br>to set the image</b></i></html>";
106    
107        /**
108         * The image to draw
109         */
110        private SoftReference<Image> img = new SoftReference<Image>(null);
111    
112        /**
113         * If true, then the image can be changed. Perhaps a better name is
114         * &quot;readOnly&quot;, but editable was chosen to be more consistent with
115         * other Swing components.
116         */
117        private boolean editable = false;
118    
119        /**
120         * The mouse handler that is used if the component is editable
121         */
122        private MouseHandler mhandler = new MouseHandler();
123    
124        /**
125         * Specifies how to draw the image, i.e. what kind of Style to use when
126         * drawing
127         */
128        private Style style = Style.CENTERED;
129    
130        private Image defaultImage;
131    
132        private Callable<Image> imageLoader;
133    
134        private static final ExecutorService service = Executors.newFixedThreadPool(5);
135    
136        public JXImagePanel() {
137        }
138    
139        //TODO remove this constructor; no where else can a URL be used in this class
140        public JXImagePanel(URL imageUrl) {
141            try {
142                setImage(ImageIO.read(imageUrl));
143            } catch (Exception e) {
144                // TODO need convert to something meaningful
145                LOG.log(Level.WARNING, "", e);
146            }
147        }
148    
149        /**
150         * Sets the image to use for the background of this panel. This image is
151         * painted whether the panel is opaque or translucent.
152         * 
153         * @param image if null, clears the image. Otherwise, this will set the
154         *        image to be painted. If the preferred size has not been explicitly
155         *        set, then the image dimensions will alter the preferred size of
156         *        the panel.
157         */
158        public void setImage(Image image) {
159            if (image != img.get()) {
160                Image oldImage = img.get();
161                img = new SoftReference<Image>(image);
162                firePropertyChange("image", oldImage, img);
163                invalidate();
164                repaint();
165            }
166        }
167    
168        /**
169         * @return the image used for painting the background of this panel
170         */
171        public Image getImage() {
172            Image image = img.get();
173            
174            //TODO perhaps we should have a default image loader?
175            if (image == null && imageLoader != null) {
176                try {
177                    image = imageLoader.call();
178                    img = new SoftReference<Image>(image);
179                } catch (Exception e) {
180                    LOG.log(Level.WARNING, "", e);
181                }
182            }
183            return image;
184        }
185    
186        /**
187         * @param editable
188         */
189        public void setEditable(boolean editable) {
190            if (editable != this.editable) {
191                // if it was editable, remove the mouse handler
192                if (this.editable) {
193                    removeMouseListener(mhandler);
194                }
195                this.editable = editable;
196                // if it is now editable, add the mouse handler
197                if (this.editable) {
198                    addMouseListener(mhandler);
199                }
200                setToolTipText(editable ? TEXT : "");
201                firePropertyChange("editable", !editable, editable);
202                repaint();
203            }
204        }
205    
206        /**
207         * @return whether the image for this panel can be changed or not via the
208         *         UI. setImage may still be called, even if <code>isEditable</code>
209         *         returns false.
210         */
211        public boolean isEditable() {
212            return editable;
213        }
214    
215        /**
216         * Sets what style to use when painting the image
217         * 
218         * @param s
219         */
220        public void setStyle(Style s) {
221            if (style != s) {
222                Style oldStyle = style;
223                style = s;
224                firePropertyChange("style", oldStyle, s);
225                repaint();
226            }
227        }
228    
229        /**
230         * @return the Style used for drawing the image (CENTERED, TILED, etc).
231         */
232        public Style getStyle() {
233            return style;
234        }
235    
236        /**
237         *  {@inheritDoc}
238         *  The old property value in PCE fired by this method might not be always correct!
239         */
240        public Dimension getPreferredSize() {
241            if (!isPreferredSizeSet() && img != null) {
242                Image img = this.img.get();
243                // was img GCed in the mean time?
244                if (img != null) {
245                    // it has not been explicitly set, so return the width/height of
246                    // the image
247                    int width = img.getWidth(null);
248                    int height = img.getHeight(null);
249                    if (width == -1 || height == -1) {
250                        return super.getPreferredSize();
251                    }
252                    Insets insets = getInsets();
253                    width += insets.left + insets.right;
254                    height += insets.top + insets.bottom;
255                    return new Dimension(width, height);
256                }
257            }
258            return super.getPreferredSize();
259        }
260    
261        /**
262         * Overridden to paint the image on the panel
263         * 
264         * @param g
265         */
266        protected void paintComponent(Graphics g) {
267            super.paintComponent(g);
268            Graphics2D g2 = (Graphics2D) g;
269            Image img = this.img.get();
270            if (img == null && imageLoader != null) {
271                // schedule for loading (will repaint itself once loaded)
272                // have to use new future task every time as it holds strong
273                // reference to the object it retrieved and doesn't allow to reset
274                // it.
275                service.execute(new FutureTask<Image>(imageLoader) {
276    
277                    @Override
278                    protected void done() {
279                        super.done();
280                        
281                        SwingUtilities.invokeLater(new Runnable() {
282                            public void run() {
283                                try {
284                                    JXImagePanel.this.setImage(get());
285                                } catch (InterruptedException e) {
286                                    // ignore - canceled image load
287                                } catch (ExecutionException e) {
288                                    LOG.log(Level.WARNING, "", e);
289                                }
290                            }
291                        });
292                    }
293    
294                });
295                img = defaultImage;
296            }
297            if (img != null) {
298                final int imgWidth = img.getWidth(null);
299                final int imgHeight = img.getHeight(null);
300                if (imgWidth == -1 || imgHeight == -1) {
301                    // image hasn't completed loading, return
302                    return;
303                }
304    
305                Insets insets = getInsets();
306                final int pw = getWidth() - insets.left - insets.right;
307                final int ph = getHeight() - insets.top - insets.bottom;
308    
309                switch (style) {
310                case CENTERED:
311                    Rectangle clipRect = g2.getClipBounds();
312                    int imageX = (pw - imgWidth) / 2 + insets.left;
313                    int imageY = (ph - imgHeight) / 2 + insets.top;
314                    Rectangle r = SwingUtilities.computeIntersection(imageX, imageY, imgWidth, imgHeight, clipRect);
315                    if (r.x == 0 && r.y == 0 && (r.width == 0 || r.height == 0)) {
316                        return;
317                    }
318                    // I have my new clipping rectangle "r" in clipRect space.
319                    // It is therefore the new clipRect.
320                    clipRect = r;
321                    // since I have the intersection, all I need to do is adjust the
322                    // x & y values for the image
323                    int txClipX = clipRect.x - imageX;
324                    int txClipY = clipRect.y - imageY;
325                    int txClipW = clipRect.width;
326                    int txClipH = clipRect.height;
327    
328                    g2.drawImage(img, clipRect.x, clipRect.y, clipRect.x + clipRect.width, clipRect.y + clipRect.height, txClipX, txClipY, txClipX + txClipW, txClipY + txClipH, null);
329                    break;
330                case TILED:
331                    g2.translate(insets.left, insets.top);
332                    Rectangle clip = g2.getClipBounds();
333                    g2.setClip(0, 0, pw, ph);
334    
335                    int totalH = 0;
336    
337                    while (totalH < ph) {
338                        int totalW = 0;
339    
340                        while (totalW < pw) {
341                            g2.drawImage(img, totalW, totalH, null);
342                            totalW += img.getWidth(null);
343                        }
344    
345                        totalH += img.getHeight(null);
346                    }
347    
348                    g2.setClip(clip);
349                    g2.translate(-insets.left, -insets.top);
350                    break;
351                case SCALED:
352                    g2.drawImage(img, insets.left, insets.top, pw, ph, null);
353                    break;
354                case SCALED_KEEP_ASPECT_RATIO:
355                    int w = pw;
356                    int h = ph;
357                    final float ratioW = ((float) w) / ((float) imgWidth);
358                    final float ratioH = ((float) h) / ((float) imgHeight);
359    
360                    if (ratioW < ratioH) {
361                        h = (int) (imgHeight * ratioW);
362                    } else {
363                        w = (int) (imgWidth * ratioH);
364                    }
365    
366                    final int x = (pw - w) / 2 + insets.left;
367                    final int y = (ph - h) / 2 + insets.top;
368                    g2.drawImage(img, x, y, w, h, null);
369                    break;
370                default:
371                    LOG.fine("unimplemented");
372                    g2.drawImage(img, insets.left, insets.top, this);
373                    break;
374                }
375            }
376        }
377    
378        /**
379         * Handles click events on the component
380         */
381        private class MouseHandler extends MouseAdapter {
382            private Cursor oldCursor;
383    
384            private JFileChooser chooser;
385    
386            public void mouseClicked(MouseEvent evt) {
387                if (chooser == null) {
388                    chooser = new JFileChooser();
389                }
390                int retVal = chooser.showOpenDialog(JXImagePanel.this);
391                if (retVal == JFileChooser.APPROVE_OPTION) {
392                    File file = chooser.getSelectedFile();
393                    try {
394                        setImage(new ImageIcon(file.toURI().toURL()).getImage());
395                    } catch (Exception ex) {
396                    }
397                }
398            }
399    
400            public void mouseEntered(MouseEvent evt) {
401                if (oldCursor == null) {
402                    oldCursor = getCursor();
403                    setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
404                }
405            }
406    
407            public void mouseExited(MouseEvent evt) {
408                if (oldCursor != null) {
409                    setCursor(oldCursor);
410                    oldCursor = null;
411                }
412            }
413        }
414    
415        public void setDefaultImage(Image def) {
416            this.defaultImage = def;
417        }
418    
419        public void setImageLoader(Callable<Image> loadImage) {
420            this.imageLoader = loadImage;
421    
422        }
423    }