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    }