001 /* 002 * $Id: JXImageView.java,v 1.9 2006/05/14 08:12:15 dmouse Exp $ 003 * 004 * Copyright 2006 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.Color; 025 import java.awt.Component; 026 import java.awt.Cursor; 027 import java.awt.Graphics; 028 import java.awt.Graphics2D; 029 import java.awt.Image; 030 import java.awt.Paint; 031 import java.awt.Point; 032 import java.awt.RenderingHints; 033 import java.awt.datatransfer.DataFlavor; 034 import java.awt.datatransfer.Transferable; 035 import java.awt.datatransfer.UnsupportedFlavorException; 036 import java.awt.dnd.DnDConstants; 037 import java.awt.dnd.DragGestureEvent; 038 import java.awt.dnd.DragGestureListener; 039 import java.awt.dnd.DragGestureRecognizer; 040 import java.awt.dnd.DragSource; 041 import java.awt.event.ActionEvent; 042 import java.awt.event.InputEvent; 043 import java.awt.event.MouseEvent; 044 import java.awt.geom.AffineTransform; 045 import java.awt.geom.Point2D; 046 import java.awt.geom.Rectangle2D; 047 import java.awt.image.AffineTransformOp; 048 import java.awt.image.BufferedImage; 049 import java.awt.image.BufferedImageOp; 050 import java.io.File; 051 import java.io.FileInputStream; 052 import java.io.IOException; 053 import java.net.URL; 054 import java.util.ArrayList; 055 import java.util.Iterator; 056 import java.util.List; 057 import javax.imageio.ImageIO; 058 import javax.imageio.ImageReader; 059 import javax.swing.AbstractAction; 060 import javax.swing.Action; 061 import javax.swing.JComponent; 062 import javax.swing.JFileChooser; 063 import javax.swing.JList; 064 import javax.swing.TransferHandler; 065 import javax.swing.event.MouseInputAdapter; 066 import org.jdesktop.swingx.color.ColorUtil; 067 import org.jdesktop.swingx.error.ErrorListener; 068 import org.jdesktop.swingx.error.ErrorSupport; 069 import org.jdesktop.swingx.util.PaintUtils; 070 071 /** 072 * <p>A panel which shows an image centered. The user can drag an image into the 073 * panel from other applications and move the image around within the view. 074 * The JXImageView has built in actions for scaling, rotating, opening a new 075 * image, and saving. These actions can be obtained using the relevant get*Action() 076 * methods. 077 *</p> 078 * 079 * <p>TODO: has dashed rect and text indicating you should drag there.</p> 080 * 081 * 082 * <p>If the user drags more than one photo at a time into the JXImageView only 083 * the first photo will be loaded and shown. Any errors generated internally, 084 * such as dragging in a list of files which are not images, will be reported 085 * to any attached {@link org.jdesktop.swingx.error.ErrorListener} added by the 086 * <CODE>{@link #addErrorListener}()</CODE> method.</p> 087 * 088 * @author Joshua Marinacci joshua.marinacci@sun.com 089 */ 090 public class JXImageView extends JXPanel { 091 092 /* ======= instance variables ========= */ 093 // the image this view will show 094 private Image image; 095 096 // support for error listeners 097 private ErrorSupport errorSupport = new ErrorSupport(this); 098 099 // location to draw image. if null then draw in the center 100 private Point2D imageLocation; 101 // the background paint 102 private Paint checkerPaint; 103 // the scale for drawing the image 104 private double scale = 1.0; 105 // controls whether the user can move images around 106 private boolean editable = true; 107 // the handler for moving the image around within the panel 108 private MoveHandler moveHandler = new MoveHandler(this); 109 // controls the drag part of drag and drop 110 private boolean dragEnabled = false; 111 // controls the filename of the dropped file 112 private String exportName = "UntitledImage"; 113 // controls the format and filename extension of the dropped file 114 private String exportFormat = "png"; 115 116 /** Creates a new instance of JXImageView */ 117 public JXImageView() { 118 checkerPaint = ColorUtil.getCheckerPaint(Color.white,new Color(250,250,250),50); 119 setEditable(true); 120 } 121 122 123 124 /* ========= properties ========= */ 125 /** 126 * Gets the current image location. This location can be changed programmatically 127 * or by the user dragging the image within the JXImageView. 128 * @return the current image location 129 */ 130 public Point2D getImageLocation() { 131 return imageLocation; 132 } 133 134 /** 135 * Set the current image location. 136 * @param imageLocation The new image location. 137 */ 138 public void setImageLocation(Point2D imageLocation) { 139 this.imageLocation = imageLocation; 140 repaint(); 141 } 142 143 /** 144 * Gets the currently set image, or null if no image is set. 145 * @return the currently set image, or null if no image is set. 146 */ 147 public Image getImage() { 148 return image; 149 } 150 151 /** 152 * Sets the current image. Can set null if there should be no image show. 153 * @param image the new image to set, or null. 154 */ 155 public void setImage(Image image) { 156 this.image = image; 157 setImageLocation(null); 158 setScale(1.0); 159 repaint(); 160 } 161 162 /** 163 * Set the current image to an image pointed to by this URL. 164 * @param url a URL pointing to an image, or null 165 * @throws java.io.IOException thrown if the image cannot be loaded 166 */ 167 public void setImage(URL url) throws IOException { 168 setImage(ImageIO.read(url)); 169 } 170 171 /** 172 * Set the current image to an image pointed to by this File. 173 * @param file a File pointing to an image 174 * @throws java.io.IOException thrown if the image cannot be loaded 175 */ 176 public void setImage(File file) throws IOException { 177 System.out.println("reading: " + file.getAbsolutePath()); 178 setImage(ImageIO.read(file)); 179 } 180 181 /** 182 * Gets the current image scale . When the scale is set to 1.0 183 * then one image pixel = one screen pixel. When scale < 1.0 the draw image 184 * will be smaller than it's real size. When scale > 1.0 the drawn image will 185 * be larger than it's real size. 1.0 is the default value. 186 * @return the current image scale 187 */ 188 public double getScale() { 189 return scale; 190 } 191 192 /** 193 * Sets the current image scale . When the scale is set to 1.0 194 * then one image pixel = one screen pixel. When scale < 1.0 the draw image 195 * will be smaller than it's real size. When scale > 1.0 the drawn image will 196 * be larger than it's real size. 1.0 is the default value. 197 * @param scale the new image scale 198 */ 199 public void setScale(double scale) { 200 double oldScale = this.scale; 201 this.scale = scale; 202 this.firePropertyChange("scale",oldScale,scale); 203 repaint(); 204 } 205 206 /** 207 * Returns whether or not the user can drag images. 208 * @return whether or not the user can drag images 209 */ 210 public boolean isEditable() { 211 return editable; 212 } 213 214 /** 215 * Sets whether or not the user can drag images. When set to true the user can 216 * drag the photo around with their mouse. Also the cursor will be set to the 217 * 'hand' cursor. When set to false the user cannot drag photos around 218 * and the cursor will be set to the default. 219 * @param editable whether or not the user can drag images 220 */ 221 public void setEditable(boolean editable) { 222 this.editable = editable; 223 if(editable) { 224 addMouseMotionListener(moveHandler); 225 addMouseListener(moveHandler); 226 this.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 227 try { 228 this.setTransferHandler(new DnDHandler()); 229 } catch (ClassNotFoundException ex) { 230 ex.printStackTrace(); 231 fireError(ex); 232 } 233 } else { 234 removeMouseMotionListener(moveHandler); 235 removeMouseListener(moveHandler); 236 this.setCursor(Cursor.getDefaultCursor()); 237 setTransferHandler(null); 238 } 239 } 240 241 /** 242 * Sets the <CODE>dragEnabled</CODE> property, which determines whether or not 243 * the user can drag images out of the image view and into other components or 244 * application. Note: <B>setting 245 * this to true will disable the ability to move the image around within the 246 * well.</B>, though it will not change the <b>editable</b> property directly. 247 * @param dragEnabled the value to set the dragEnabled property to. 248 */ 249 public void setDragEnabled(boolean dragEnabled) { 250 this.dragEnabled = dragEnabled; 251 } 252 253 /** 254 * Gets the current value of the <CODE>dragEnabled</CODE> property. 255 * @return the current value of the <CODE>dragEnabled</CODE> property 256 */ 257 public boolean isDragEnabled() { 258 return dragEnabled; 259 } 260 261 /** 262 * Adds an ErrorListener to the list of listeners to be notified 263 * of ErrorEvents 264 * @param el an ErrorListener to add 265 */ 266 public void addErrorListener(ErrorListener el) { 267 errorSupport.addErrorListener(el); 268 } 269 270 /** 271 * Remove an ErrorListener from the list of listeners to be notified of ErrorEvents. 272 * @param el an ErrorListener to remove 273 */ 274 public void removeErrorListener(ErrorListener el) { 275 errorSupport.removeErrorListener(el); 276 } 277 278 /** 279 * Send a new ErrorEvent to all registered ErrorListeners 280 * @param throwable the Error or Exception which was thrown 281 */ 282 protected void fireError(Throwable throwable) { 283 errorSupport.fireErrorEvent(throwable); 284 } 285 286 // an action which will open a file chooser and load the selected image 287 // if any. 288 /** 289 * Returns an Action which will open a file chooser, ask the user for an image file 290 * then load the image into the view. If the load fails an error will be fired 291 * to all registered ErrorListeners 292 * @return the action 293 * @see ErrorListener 294 */ 295 public Action getOpenAction() { 296 Action action = new AbstractAction() { 297 public void actionPerformed(ActionEvent actionEvent) { 298 JFileChooser chooser = new JFileChooser(); 299 chooser.showOpenDialog(JXImageView.this); 300 File file = chooser.getSelectedFile(); 301 if(file != null) { 302 try { 303 setImage(file); 304 } catch (IOException ex) { 305 System.out.println(ex.getMessage()); 306 ex.printStackTrace(); 307 fireError(ex); 308 } 309 } 310 } 311 }; 312 action.putValue(Action.NAME,"Open"); 313 return action; 314 } 315 316 // an action that will open a file chooser then save the current image to 317 // the selected file, if any. 318 /** 319 * Returns an Action which will open a file chooser, ask the user for an image file 320 * then save the image from the view. If the save fails an error will be fired 321 * to all registered ErrorListeners 322 * @return an Action 323 */ 324 public Action getSaveAction() { 325 Action action = new AbstractAction() { 326 public void actionPerformed(ActionEvent evt) { 327 Image img = getImage(); 328 BufferedImage dst = new BufferedImage( 329 img.getWidth(null), 330 img.getHeight(null), 331 BufferedImage.TYPE_INT_ARGB); 332 Graphics2D g = (Graphics2D)dst.getGraphics(); 333 // smooth scaling 334 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 335 RenderingHints.VALUE_INTERPOLATION_BICUBIC); 336 g.drawImage(img,0,0,null); 337 g.dispose(); 338 JFileChooser chooser = new JFileChooser(); 339 chooser.showSaveDialog(JXImageView.this); 340 File file = chooser.getSelectedFile(); 341 if(file != null) { 342 try { 343 ImageIO.write(dst,"png",file); 344 } catch (IOException ex) { 345 System.out.println(ex.getMessage()); 346 ex.printStackTrace(); 347 fireError(ex); 348 } 349 } 350 } 351 }; 352 353 action.putValue(Action.NAME,"Save"); 354 return action; 355 } 356 357 /** 358 * Get an action which will rotate the currently selected image clockwise. 359 * @return an action 360 */ 361 public Action getRotateClockwiseAction() { 362 Action action = new AbstractAction() { 363 public void actionPerformed(ActionEvent evt) { 364 Image img = getImage(); 365 BufferedImage src = new BufferedImage( 366 img.getWidth(null), 367 img.getHeight(null), 368 BufferedImage.TYPE_INT_ARGB); 369 BufferedImage dst = new BufferedImage( 370 img.getHeight(null), 371 img.getWidth(null), 372 BufferedImage.TYPE_INT_ARGB); 373 Graphics2D g = (Graphics2D)src.getGraphics(); 374 // smooth scaling 375 g.drawImage(img,0,0,null); 376 g.dispose(); 377 AffineTransform trans = AffineTransform.getRotateInstance(Math.PI/2,0,0); 378 trans.translate(0,-src.getHeight()); 379 BufferedImageOp op = new AffineTransformOp(trans, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); 380 Rectangle2D rect = op.getBounds2D(src); 381 op.filter(src,dst); 382 setImage(dst); 383 } 384 }; 385 action.putValue(Action.NAME,"Rotate Clockwise"); 386 return action; 387 } 388 389 /** 390 * Gets an action which will rotate the current image counter clockwise. 391 * @return an Action 392 */ 393 public Action getRotateCounterClockwiseAction() { 394 Action action = new AbstractAction() { 395 public void actionPerformed(ActionEvent evt) { 396 Image img = getImage(); 397 BufferedImage src = new BufferedImage( 398 img.getWidth(null), 399 img.getHeight(null), 400 BufferedImage.TYPE_INT_ARGB); 401 BufferedImage dst = new BufferedImage( 402 img.getHeight(null), 403 img.getWidth(null), 404 BufferedImage.TYPE_INT_ARGB); 405 Graphics2D g = (Graphics2D)src.getGraphics(); 406 // smooth scaling 407 g.drawImage(img,0,0,null); 408 g.dispose(); 409 AffineTransform trans = AffineTransform.getRotateInstance(-Math.PI/2,0,0); 410 trans.translate(-src.getWidth(),0); 411 BufferedImageOp op = new AffineTransformOp(trans, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); 412 Rectangle2D rect = op.getBounds2D(src); 413 op.filter(src,dst); 414 setImage(dst); 415 } 416 }; 417 action.putValue(Action.NAME, "Rotate CounterClockwise"); 418 return action; 419 } 420 421 /** 422 * Gets an action which will zoom the current image out by a factor of 2. 423 * @return an action 424 */ 425 public Action getZoomOutAction() { 426 Action action = new AbstractAction() { 427 public void actionPerformed(ActionEvent actionEvent) { 428 setScale(getScale()*0.5); 429 } 430 }; 431 action.putValue(Action.NAME,"Zoom Out"); 432 return action; 433 } 434 435 /** 436 * Gets an action which will zoom the current image in by a factor of 2 437 * @return an action 438 */ 439 public Action getZoomInAction() { 440 Action action = new AbstractAction() { 441 public void actionPerformed(ActionEvent actionEvent) { 442 setScale(getScale()*2); 443 } 444 }; 445 action.putValue(Action.NAME,"Zoom In"); 446 return action; 447 } 448 /* === overriden methods === */ 449 450 /** 451 * Implementation detail. 452 * @param g 453 */ 454 protected void paintComponent(Graphics g) { 455 ((Graphics2D)g).setPaint(checkerPaint); 456 //g.setColor(getBackground()); 457 g.fillRect(0,0,getWidth(),getHeight()); 458 if(getImage() != null) { 459 Point2D center = new Point2D.Double(getWidth()/2,getHeight()/2); 460 if(getImageLocation() != null) { 461 center = getImageLocation(); 462 } 463 Point2D loc = new Point2D.Double(); 464 double width = getImage().getWidth(null)*getScale(); 465 double height = getImage().getHeight(null)*getScale(); 466 loc.setLocation(center.getX()-width/2, center.getY()-height/2); 467 g.drawImage(getImage(), (int)loc.getX(), (int)loc.getY(), 468 (int)width,(int)height, 469 null); 470 } 471 } 472 473 474 /* === Internal helper classes === */ 475 476 private class MoveHandler extends MouseInputAdapter { 477 private JXImageView panel; 478 private Point prev = null; 479 private Point start = null; 480 public MoveHandler(JXImageView panel) { 481 this.panel = panel; 482 } 483 484 public void mousePressed(MouseEvent evt) { 485 prev = evt.getPoint(); 486 start = prev; 487 } 488 489 public void mouseDragged(MouseEvent evt) { 490 Point curr = evt.getPoint(); 491 492 if(isDragEnabled()) { 493 //System.out.println("testing drag enabled: " + curr + " " + start); 494 //System.out.println("distance = " + curr.distance(start)); 495 if(curr.distance(start) > 5) { 496 System.out.println("starting the drag: "); 497 panel.getTransferHandler().exportAsDrag((JComponent)evt.getSource(),evt,TransferHandler.COPY); 498 return; 499 } 500 } 501 502 int offx = curr.x - prev.x; 503 int offy = curr.y - prev.y; 504 Point2D offset = getImageLocation(); 505 if (offset == null) { 506 if (image != null) { 507 offset = new Point2D.Double(getWidth() / 2, getHeight() / 2); 508 } else { 509 offset = new Point2D.Double(0, 0); 510 } 511 } 512 offset = new Point2D.Double(offset.getX() + offx, offset.getY() + offy); 513 setImageLocation(offset); 514 prev = curr; 515 repaint(); 516 } 517 518 public void mouseReleased(MouseEvent evt) { 519 prev = null; 520 } 521 } 522 523 private class DnDHandler extends TransferHandler { 524 DataFlavor urlFlavor; 525 526 public DnDHandler() throws ClassNotFoundException { 527 urlFlavor = new DataFlavor("application/x-java-url;class=java.net.URL"); 528 } 529 530 public void exportAsDrag(JComponent c, InputEvent evt, int action) { 531 //System.out.println("exportting as drag"); 532 super.exportAsDrag(c,evt,action); 533 } 534 public int getSourceActions(JComponent c) { 535 //System.out.println("get source actions: " + c); 536 return COPY; 537 } 538 protected void exportDone(JComponent source, Transferable data, int action) { 539 System.out.println("exportDone: " + source + " " + data + " " +action); 540 } 541 542 public boolean canImport(JComponent c, DataFlavor[] flavors) { 543 //System.out.println("canImport:" + c); 544 for (int i = 0; i < flavors.length; i++) { 545 //System.out.println("testing: "+flavors[i]); 546 if (DataFlavor.javaFileListFlavor.equals(flavors[i])) { 547 return true; 548 } 549 if (DataFlavor.imageFlavor.equals(flavors[i])) { 550 return true; 551 } 552 if (urlFlavor.match(flavors[i])) { 553 return true; 554 } 555 556 } 557 return false; 558 } 559 560 protected Transferable createTransferable(JComponent c) { 561 System.out.println("creating a transferable"); 562 JXImageView view = (JXImageView)c; 563 return new ImageTransferable(view.getImage(), 564 view.getExportName(), view.getExportFormat()); 565 } 566 public boolean importData(JComponent comp, Transferable t) { 567 System.out.println("importData called"); 568 if (canImport(comp, t.getTransferDataFlavors())) { 569 try { 570 if(t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { 571 List files = (List) t.getTransferData(DataFlavor.javaFileListFlavor); 572 System.out.println("doing file list flavor"); 573 if (files.size() > 0) { 574 File file = (File) files.get(0); 575 System.out.println("readingt hte image: " + file.getCanonicalPath()); 576 /*Iterator it = ImageIO.getImageReaders(new FileInputStream(file)); 577 while(it.hasNext()) { 578 System.out.println("can read: " + it.next()); 579 }*/ 580 BufferedImage img = ImageIO.read(file); 581 setImage(img); 582 return true; 583 } 584 } 585 //System.out.println("doing a uri list"); 586 Object obj = t.getTransferData(urlFlavor); 587 //System.out.println("obj = " + obj + " " + obj.getClass().getPackage() + " " 588 // + obj.getClass().getName()); 589 if(obj instanceof URL) { 590 setImage((URL)obj); 591 } 592 return true; 593 } catch (Exception ex) { 594 System.out.println(ex.getMessage()); 595 ex.printStackTrace(); 596 fireError(ex); 597 } 598 } 599 return false; 600 } 601 602 } 603 604 605 private class ImageTransferable implements Transferable { 606 private Image img; 607 private List files; 608 private String exportName, exportFormat; 609 public ImageTransferable(Image img, String exportName, String exportFormat) { 610 this.img = img; 611 this.exportName = exportName; 612 this.exportFormat = exportFormat; 613 } 614 615 public DataFlavor[] getTransferDataFlavors() { 616 DataFlavor[] flavors = { DataFlavor.imageFlavor, 617 DataFlavor.javaFileListFlavor }; 618 return flavors; 619 } 620 621 public boolean isDataFlavorSupported(DataFlavor flavor) { 622 if(flavor == DataFlavor.imageFlavor) { 623 return true; 624 } 625 if(flavor == DataFlavor.javaFileListFlavor) { 626 return true; 627 } 628 return false; 629 } 630 631 public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { 632 //System.out.println("doing get trans data: " + flavor); 633 if(flavor == DataFlavor.imageFlavor) { 634 return img; 635 } 636 if(flavor == DataFlavor.javaFileListFlavor) { 637 if(files == null) { 638 files = new ArrayList(); 639 File file = File.createTempFile(exportName,"."+exportFormat); 640 //System.out.println("writing to: " + file); 641 ImageIO.write(PaintUtils.convertToBufferedImage(img),exportFormat,file); 642 files.add(file); 643 } 644 //System.out.println("returning: " + files); 645 return files; 646 } 647 return null; 648 } 649 } 650 651 public String getExportName() { 652 return exportName; 653 } 654 655 public void setExportName(String exportName) { 656 this.exportName = exportName; 657 } 658 659 public String getExportFormat() { 660 return exportFormat; 661 } 662 663 public void setExportFormat(String exportFormat) { 664 this.exportFormat = exportFormat; 665 } 666 }