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