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