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