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&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<Image> {
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 * "readOnly", 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 }