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 }