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