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 }