View Javadoc

1   /*
2    *                Doelan development code
3    *
4    * This code may be freely distributed and modified under the
5    * terms of the GNU General Public Licence.  This should
6    * be distributed with the code. If you do not have a copy,
7    * see:
8    *
9    *      http://www.gnu.org/copyleft/gpl.txt
10   *
11   * Copyright (c) 2004-2005 ENS Microarray Platform
12   * Copyright for this code is held jointly by the individual
13   * authors.  These should be listed in @author doc comments.
14   *
15   * For more information on the Doelan project and its aims,
16   * or to join the Doelan mailing list, visit the home page
17   * at:
18   *
19   *      http://www.transcriptome.ens.fr/doelan
20   */
21  
22  package fr.ens.transcriptome.doelan.gui;
23  
24  import java.awt.Color;
25  import java.awt.Component;
26  import java.awt.Container;
27  import java.awt.Dimension;
28  import java.awt.Graphics;
29  import java.awt.Image;
30  import java.awt.Point;
31  import java.awt.Rectangle;
32  import java.awt.Shape;
33  import java.awt.Toolkit;
34  import java.awt.event.MouseEvent;
35  import java.awt.event.MouseListener;
36  import java.awt.event.MouseMotionListener;
37  import java.awt.image.ImageObserver;
38  import java.io.BufferedInputStream;
39  import java.io.ByteArrayOutputStream;
40  import java.io.IOException;
41  import java.io.InputStream;
42  import java.net.MalformedURLException;
43  import java.net.URL;
44  import java.util.Dictionary;
45  import java.util.Map;
46  
47  import javax.swing.Icon;
48  import javax.swing.ImageIcon;
49  import javax.swing.JEditorPane;
50  import javax.swing.event.DocumentEvent;
51  import javax.swing.text.AbstractDocument;
52  import javax.swing.text.AttributeSet;
53  import javax.swing.text.BadLocationException;
54  import javax.swing.text.Document;
55  import javax.swing.text.Element;
56  import javax.swing.text.JTextComponent;
57  import javax.swing.text.MutableAttributeSet;
58  import javax.swing.text.Position;
59  import javax.swing.text.SimpleAttributeSet;
60  import javax.swing.text.StyledDocument;
61  import javax.swing.text.View;
62  import javax.swing.text.ViewFactory;
63  import javax.swing.text.html.HTML;
64  import javax.swing.text.html.HTMLDocument;
65  import javax.swing.text.html.StyleSheet;
66  
67  import org.apache.log4j.Logger;
68  
69  /***
70   * Image viewer
71   * @author Laurent Jourdren
72   */
73  public class MyImageView extends View implements ImageObserver, MouseListener,
74      MouseMotionListener {
75  
76    private Logger log = Logger.getLogger(MyImageView.class);
77  
78    // --- Attribute Values ------------------------------------------
79  
80    public static final String TOP = "top", TEXTTOP = "texttop",
81        MIDDLE = "middle", ABSMIDDLE = "absmiddle", CENTER = "center",
82        BOTTOM = "bottom";
83  
84    // --- Construction ----------------------------------------------
85  
86    /***
87     * Creates a new view that represents an IMG element.
88     * @param elem the element to create a view for
89     * @param mapImage The map with all the images
90     */
91    public MyImageView(final Element elem, final Map mapImage) {
92      super(elem);
93      setMapImages(mapImage);
94      initialize(elem);
95      StyleSheet sheet = getStyleSheet();
96      attr = sheet.getViewAttributes(this);
97    }
98  
99    private void initialize(final Element elem) {
100     synchronized (this) {
101       loading = true;
102       fWidth = 0;
103       fHeight = 0;
104     }
105     int width = 0;
106     int height = 0;
107     boolean customWidth = false;
108     boolean customHeight = false;
109     try {
110       fElement = elem;
111 
112       // Request image from document's cache:
113       //AttributeSet attr = elem.getAttributes();
114       if (isURL()) {
115         URL src = getSourceURL();
116         if (src != null) {
117           Dictionary cache = (Dictionary) getDocument().getProperty(
118               IMAGE_CACHE_PROPERTY);
119           if (cache != null)
120             fImage = (Image) cache.get(src);
121           else
122             fImage = Toolkit.getDefaultToolkit().getImage(src);
123         }
124       } else {
125 
126         /*** ****** Code to load from relative path ************ */
127         String src = (String) fElement.getAttributes().getAttribute(
128             HTML.Attribute.SRC);
129         log.debug("src = " + src);
130 
131         if (getMapImages() == null) {
132           fImage = null;
133         } else
134           fImage = (Image) getMapImages().get(src);
135 
136         log.debug("fimage:" + fImage);
137         /*** *************************************************** */
138 
139       }
140 
141       // Get height/width from params or image or defaults:
142       height = getIntAttr(HTML.Attribute.HEIGHT, -1);
143       customHeight = (height > 0);
144       if (!customHeight && fImage != null)
145         height = fImage.getHeight(this);
146       if (height <= 0)
147         height = DEFAULT_HEIGHT;
148 
149       width = getIntAttr(HTML.Attribute.WIDTH, -1);
150       customWidth = (width > 0);
151       if (!customWidth && fImage != null)
152         width = fImage.getWidth(this);
153       if (width <= 0)
154         width = DEFAULT_WIDTH;
155 
156       // Make sure the image starts loading:
157       if (fImage != null)
158         if (customWidth && customHeight)
159           Toolkit.getDefaultToolkit().prepareImage(fImage, height, width, this);
160         else
161           Toolkit.getDefaultToolkit().prepareImage(fImage, -1, -1, this);
162 
163     } finally {
164       synchronized (this) {
165         loading = false;
166         if (customWidth || fWidth == 0) {
167           fWidth = width;
168         }
169         if (customHeight || fHeight == 0) {
170           fHeight = height;
171         }
172       }
173     }
174   }
175 
176   /*** Determines if path is in the form of a URL */
177   private boolean isURL() {
178     String src = (String) fElement.getAttributes().getAttribute(
179         HTML.Attribute.SRC);
180     return src.toLowerCase().startsWith("file")
181         || src.toLowerCase().startsWith("http");
182   }
183 
184   /*
185    * Added this guy to make sure an image is loaded - ie no broken images. So
186    * far its used only for images loaded off the disk (non-URL). It seems to
187    * work marvelously. By the way, it does the same thing as MediaTracker, but
188    * you dont need to know the component its being rendered on. Rob
189    */
190   /*
191    * private void waitForImage() throws InterruptedException { int w =
192    * fImage.getWidth(this); int h = fImage.getHeight(this); while (true) { int
193    * flags = Toolkit.getDefaultToolkit().checkImage(fImage, w, h, this); if
194    * (((flags & ERROR) != 0) || ((flags & ABORT) != 0)) throw new
195    * InterruptedException(); else if ((flags & (ALLBITS | FRAMEBITS)) != 0)
196    * return; Thread.sleep(10); } }
197    */
198 
199   /***
200    * Fetches the attributes to use when rendering. This is implemented to
201    * multiplex the attributes specified in the model with a StyleSheet.
202    * @return The attributes
203    */
204   public AttributeSet getAttributes() {
205     return attr;
206   }
207 
208   /*** Is this image within a link? */
209   boolean isLink() {
210     //! It would be nice to cache this but in an editor it can change
211     // See if I have an HREF attribute courtesy of the enclosing A tag:
212     AttributeSet anchorAttr = (AttributeSet) fElement.getAttributes()
213         .getAttribute(HTML.Tag.A);
214     if (anchorAttr != null) {
215       return anchorAttr.isDefined(HTML.Attribute.HREF);
216     }
217     return false;
218   }
219 
220   /*** Returns the size of the border to use. */
221   int getBorder() {
222     return getIntAttr(HTML.Attribute.BORDER, isLink() ? DEFAULT_BORDER : 0);
223   }
224 
225   /*** Returns the amount of extra space to add along an axis. */
226   int getSpace(final int axis) {
227     return getIntAttr(axis == X_AXIS ? HTML.Attribute.HSPACE
228         : HTML.Attribute.VSPACE, 0);
229   }
230 
231   /*** Returns the border's color, or null if this is not a link. */
232   Color getBorderColor() {
233     StyledDocument doc = (StyledDocument) getDocument();
234     return doc.getForeground(getAttributes());
235   }
236 
237   /*** Returns the image's vertical alignment. */
238   float getVerticalAlignment() {
239     String align = (String) fElement.getAttributes().getAttribute(
240         HTML.Attribute.ALIGN);
241     if (align != null) {
242       align = align.toLowerCase();
243       if (align.equals(TOP) || align.equals(TEXTTOP))
244         return 0.0f;
245       else if (align.equals(CENTER) || align.equals(MIDDLE)
246           || align.equals(ABSMIDDLE))
247         return 0.5f;
248     }
249     return 1.0f; // default alignment is bottom
250   }
251 
252   boolean hasPixels(final ImageObserver obs) {
253     return fImage != null && fImage.getHeight(obs) > 0
254         && fImage.getWidth(obs) > 0;
255   }
256 
257   /***
258    * Return a URL for the image source, or null if it could not be determined.
259    */
260   private URL getSourceURL() {
261     String src = (String) fElement.getAttributes().getAttribute(
262         HTML.Attribute.SRC);
263     if (src == null)
264       return null;
265 
266     URL reference = ((HTMLDocument) getDocument()).getBase();
267     try {
268       URL u = new URL(reference, src);
269       return u;
270     } catch (MalformedURLException e) {
271       return null;
272     }
273   }
274 
275   /*** Look up an integer-valued attribute. <b>Not </b> recursive. */
276   private int getIntAttr(final HTML.Attribute name, final int deflt) {
277     AttributeSet attr = fElement.getAttributes();
278     if (attr.isDefined(name)) { // does not check parents!
279       int i;
280       String val = (String) attr.getAttribute(name);
281       if (val == null)
282         i = deflt;
283       else
284         try {
285           i = Math.max(0, Integer.parseInt(val));
286         } catch (NumberFormatException x) {
287           i = deflt;
288         }
289       return i;
290     }
291     return deflt;
292   }
293 
294   /***
295    * Establishes the parent view for this view. Seize this moment to cache the
296    * AWT Container I'm in.
297    * @param parent The parent of this view
298    */
299   public void setParent(final View parent) {
300     super.setParent(parent);
301     fContainer = parent != null ? getContainer() : null;
302     if (parent == null && fComponent != null) {
303       fComponent.getParent().remove(fComponent);
304       fComponent = null;
305     }
306   }
307 
308   /***
309    * My attributes may have changed.
310    * @param e DocumentEvent
311    * @param a Shape
312    * @param f View factory
313    */
314   public void changedUpdate(final DocumentEvent e, final Shape a,
315       final ViewFactory f) {
316 
317     log.debug("ImageView: changedUpdate begin...");
318     super.changedUpdate(e, a, f);
319     float align = getVerticalAlignment();
320 
321     int height = fHeight;
322     int width = fWidth;
323 
324     initialize(getElement());
325 
326     boolean hChanged = fHeight != height;
327     boolean wChanged = fWidth != width;
328     if (hChanged || wChanged || getVerticalAlignment() != align) {
329 
330       log.debug("ImageView: calling preferenceChanged");
331       getParent().preferenceChanged(this, hChanged, wChanged);
332     }
333     log.debug("ImageView: changedUpdate end; valign=" + getVerticalAlignment());
334   }
335 
336   // --- Painting --------------------------------------------------------
337 
338   /***
339    * Paints the image.
340    * @param g the rendering surface to use
341    * @param a the allocated region to render into
342    * @see View#paint
343    */
344   public void paint(final Graphics g, final Shape a) {
345     Color oldColor = g.getColor();
346     fBounds = a.getBounds();
347     int border = getBorder();
348     int x = fBounds.x + border + getSpace(X_AXIS);
349     int y = fBounds.y + border + getSpace(Y_AXIS);
350     int width = fWidth;
351     int height = fHeight;
352     int sel = getSelectionState();
353 
354     // Make sure my Component is in the right place:
355     /*
356      * if( fComponent == null ) { fComponent = new Component() { };
357      * fComponent.addMouseListener(this);
358      * fComponent.addMouseMotionListener(this);
359      * fComponent.setCursor(Cursor.getDefaultCursor()); // use arrow cursor
360      * fContainer.add(fComponent); } fComponent.setBounds(x,y,width,height);
361      */
362     // If no pixels yet, draw gray outline and icon:
363     if (!hasPixels(this)) {
364       g.setColor(Color.lightGray);
365       g.drawRect(x, y, width - 1, height - 1);
366       g.setColor(oldColor);
367       loadIcons();
368       Icon icon = fImage == null ? sMissingImageIcon : sPendingImageIcon;
369       if (icon != null)
370         icon.paintIcon(getContainer(), g, x, y);
371     }
372 
373     // Draw image:
374     if (fImage != null) {
375       g.drawImage(fImage, x, y, width, height, this);
376       // Use the following instead of g.drawImage when
377       // BufferedImageGraphics2D.setXORMode is fixed (4158822).
378 
379       //  Use Xor mode when selected/highlighted.
380       //! Could darken image instead, but it would be more expensive.
381       /*
382        * if( sel > 0 ) g.setXORMode(Color.white); g.drawImage(fImage,x, y,
383        * width,height,this); if( sel > 0 ) g.setPaintMode();
384        */
385     }
386 
387     // If selected exactly, we need a black border & grow-box:
388     Color bc = getBorderColor();
389     if (sel == 2) {
390       // Make sure there's room for a border:
391       int delta = 2 - border;
392       if (delta > 0) {
393         x += delta;
394         y += delta;
395         width -= delta << 1;
396         height -= delta << 1;
397         border = 2;
398       }
399       bc = null;
400       g.setColor(Color.black);
401       // Draw grow box:
402       g.fillRect(x + width - 5, y + height - 5, 5, 5);
403     }
404 
405     // Draw border:
406     if (border > 0) {
407       if (bc != null)
408         g.setColor(bc);
409       // Draw a thick rectangle:
410       for (int i = 1; i <= border; i++)
411         g.drawRect(x - i, y - i, width - 1 + i + i, height - 1 + i + i);
412       g.setColor(oldColor);
413     }
414   }
415 
416   /***
417    * Request that this view be repainted. Assumes the view is still at its
418    * last-drawn location.
419    * @param delay Delay to repaint
420    */
421   protected void repaint(final long delay) {
422     if (fContainer != null && fBounds != null) {
423       fContainer.repaint(delay, fBounds.x, fBounds.y, fBounds.width,
424           fBounds.height);
425     }
426   }
427 
428   /***
429    * Determines whether the image is selected, and if it's the only thing
430    * selected.
431    * @return 0 if not selected, 1 if selected, 2 if exclusively selected.
432    *               "Exclusive" selection is only returned when editable.
433    */
434   protected int getSelectionState() {
435     int p0 = fElement.getStartOffset();
436     int p1 = fElement.getEndOffset();
437     if (fContainer instanceof JTextComponent) {
438       JTextComponent textComp = (JTextComponent) fContainer;
439       int start = textComp.getSelectionStart();
440       int end = textComp.getSelectionEnd();
441       if (start <= p0 && end >= p1) {
442         if (start == p0 && end == p1 && isEditable())
443           return 2;
444 
445         return 1;
446       }
447     }
448     return 0;
449   }
450 
451   protected boolean isEditable() {
452     return fContainer instanceof JEditorPane
453         && ((JEditorPane) fContainer).isEditable();
454   }
455 
456   /*** Returns the text editor's highlight color. */
457   protected Color getHighlightColor() {
458     JTextComponent textComp = (JTextComponent) fContainer;
459     return textComp.getSelectionColor();
460   }
461 
462   // --- Progressive display ---------------------------------------------
463 
464   // This can come on any thread. If we are in the process of reloading
465   // the image and determining our state (loading == true) we don't fire
466   // preference changed, or repaint, we just reset the fWidth/fHeight as
467   // necessary and return. This is ok as we know when loading finishes
468   // it will pick up the new height/width, if necessary.
469   /***
470    * Update the image.
471    * @param img Image
472    * @param flags Flags
473    * @param x X coordinate
474    * @param y Y coordinate
475    * @param width Width
476    * @param height Height
477    * @return true if no need to repaint
478    */
479   public boolean imageUpdate(final Image img, final int flags, final int x,
480       final int y, final int width, final int height) {
481     if (fImage == null || fImage != img)
482       return false;
483 
484     // Bail out if there was an error:
485     if ((flags & (ABORT | ERROR)) != 0) {
486       fImage = null;
487       repaint(0);
488       return false;
489     }
490 
491     // Resize image if necessary:
492     short changed = 0;
493     if ((flags & ImageObserver.HEIGHT) != 0)
494       if (!getElement().getAttributes().isDefined(HTML.Attribute.HEIGHT)) {
495         changed |= 1;
496       }
497     if ((flags & ImageObserver.WIDTH) != 0)
498       if (!getElement().getAttributes().isDefined(HTML.Attribute.WIDTH)) {
499         changed |= 2;
500       }
501     synchronized (this) {
502       if ((changed & 1) == 1) {
503         fWidth = width;
504       }
505       if ((changed & 2) == 2) {
506         fHeight = height;
507       }
508       if (loading) {
509         // No need to resize or repaint, still in the process of
510         // loading.
511         return true;
512       }
513     }
514     if (changed != 0) {
515       // May need to resize myself, asynchronously:
516       log.debug("ImageView: resized to " + fWidth + "x" + fHeight);
517 
518       Document doc = getDocument();
519       try {
520         if (doc instanceof AbstractDocument) {
521           ((AbstractDocument) doc).readLock();
522         }
523         preferenceChanged(this, true, true);
524       } finally {
525         if (doc instanceof AbstractDocument) {
526           ((AbstractDocument) doc).readUnlock();
527         }
528       }
529 
530       return true;
531     }
532 
533     // Repaint when done or when new pixels arrive:
534     if ((flags & (FRAMEBITS | ALLBITS)) != 0)
535       repaint(0);
536     else if ((flags & SOMEBITS) != 0)
537       if (sIsInc)
538         repaint(sIncRate);
539 
540     return ((flags & ALLBITS) == 0);
541   }
542 
543   /*
544    * /** Static properties for incremental drawing. Swiped from Component.java
545    * @see #imageUpdate
546    */
547   private static boolean sIsInc = true;
548   private static int sIncRate = 100;
549 
550   // --- Layout ----------------------------------------------------------
551 
552   /***
553    * Determines the preferred span for this view along an axis.
554    * @param axis may be either X_AXIS or Y_AXIS
555    * @return the span the view would like to be rendered into. Typically the
556    *               view is told to render into the span that is returned, although
557    *               there is no guarantee. The parent may choose to resize or break the
558    *               view.
559    */
560   public float getPreferredSpan(final int axis) {
561 
562     int extra = 2 * (getBorder() + getSpace(axis));
563     switch (axis) {
564     case View.X_AXIS:
565       return fWidth + extra;
566     case View.Y_AXIS:
567       return fHeight + extra;
568     default:
569       throw new IllegalArgumentException("Invalid axis: " + axis);
570     }
571   }
572 
573   /***
574    * Determines the desired alignment for this view along an axis. This is
575    * implemented to give the alignment to the bottom of the icon along the y
576    * axis, and the default along the x axis.
577    * @param axis may be either X_AXIS or Y_AXIS
578    * @return the desired alignment. This should be a value between 0.0 and 1.0
579    *               where 0 indicates alignment at the origin and 1.0 indicates
580    *               alignment to the full span away from the origin. An alignment of
581    *               0.5 would be the center of the view.
582    */
583   public float getAlignment(final int axis) {
584     switch (axis) {
585     case View.Y_AXIS:
586       return getVerticalAlignment();
587     default:
588       return super.getAlignment(axis);
589     }
590   }
591 
592   /***
593    * Provides a mapping from the document model coordinate space to the
594    * coordinate space of the view mapped to it.
595    * @param pos the position to convert
596    * @param a the allocated region to render into
597    * @param b ???
598    * @return the bounding box of the given position
599    * @exception BadLocationException if the given position does not represent a
600    *                      valid location in the associated document
601    * @see View modelToView
602    */
603   public Shape modelToView(final int pos, final Shape a, final Position.Bias b)
604       throws BadLocationException {
605     int p0 = getStartOffset();
606     int p1 = getEndOffset();
607     if ((pos >= p0) && (pos <= p1)) {
608       Rectangle r = a.getBounds();
609       if (pos == p1) {
610         r.x += r.width;
611       }
612       r.width = 0;
613       return r;
614     }
615     return null;
616   }
617 
618   /***
619    * Provides a mapping from the view coordinate space to the logical coordinate
620    * space of the model.
621    * @param x the X coordinate
622    * @param y the Y coordinate
623    * @param a the allocated region to render into
624    * @param bias ???
625    * @return the location within the model that best represents the given point
626    *               of view
627    * @see View viewToModel
628    */
629   public int viewToModel(final float x, final float y, final Shape a,
630       final Position.Bias[] bias) {
631     Rectangle alloc = (Rectangle) a;
632     if (x < alloc.x + alloc.width) {
633       bias[0] = Position.Bias.Forward;
634       return getStartOffset();
635     }
636     bias[0] = Position.Bias.Backward;
637     return getEndOffset();
638   }
639 
640   /***
641    * Set the size of the view. (Ignored.)
642    * @param width the width
643    * @param height the height
644    */
645   public void setSize(final float width, final float height) {
646     // Ignore this -- image size is determined by the tag attrs and
647     // the image itself, not the surrounding layout!
648   }
649 
650   /***
651    * Change the size of this image. This alters the HEIGHT and WIDTH attributes
652    * of the Element and causes a re-layout.
653    */
654   protected void resize(final int width, final int height) {
655     if (width == fWidth && height == fHeight)
656       return;
657 
658     fWidth = width;
659     fHeight = height;
660 
661     // Replace attributes in document:
662     MutableAttributeSet attr = new SimpleAttributeSet();
663     attr.addAttribute(HTML.Attribute.WIDTH, Integer.toString(width));
664     attr.addAttribute(HTML.Attribute.HEIGHT, Integer.toString(height));
665     ((StyledDocument) getDocument()).setCharacterAttributes(fElement
666         .getStartOffset(), fElement.getEndOffset(), attr, false);
667   }
668 
669   // --- Mouse event handling --------------------------------------------
670 
671   /***
672    * Select or grow image when clicked.
673    * @param e Mouse event.
674    */
675   public void mousePressed(final MouseEvent e) {
676     Dimension size = fComponent.getSize();
677     if (e.getX() >= size.width - 7 && e.getY() >= size.height - 7
678         && getSelectionState() == 2) {
679       // Click in selected grow-box:
680       log.debug("ImageView: grow!!! Size=" + fWidth + "x" + fHeight);
681       Point loc = fComponent.getLocationOnScreen();
682       fGrowBase = new Point(loc.x + e.getX() - fWidth, loc.y + e.getY()
683           - fHeight);
684       this.fGrowProportionally = e.isShiftDown();
685     } else {
686       // Else select image:
687       fGrowBase = null;
688       JTextComponent comp = (JTextComponent) fContainer;
689       int start = fElement.getStartOffset();
690       int end = fElement.getEndOffset();
691       int mark = comp.getCaret().getMark();
692       int dot = comp.getCaret().getDot();
693       if (e.isShiftDown()) {
694         // extend selection if shift key down:
695         if (mark <= start)
696           comp.moveCaretPosition(end);
697         else
698           comp.moveCaretPosition(start);
699       } else {
700         // just select image, without shift:
701         if (mark != start)
702           comp.setCaretPosition(start);
703         if (dot != end)
704           comp.moveCaretPosition(end);
705       }
706     }
707   }
708 
709   /***
710    * Resize image if initial click was in grow-box:
711    * @param e Mouse event
712    */
713   public void mouseDragged(final MouseEvent e) {
714     if (fGrowBase != null) {
715       Point loc = fComponent.getLocationOnScreen();
716       int width = Math.max(2, loc.x + e.getX() - fGrowBase.x);
717       int height = Math.max(2, loc.y + e.getY() - fGrowBase.y);
718 
719       if (e.isShiftDown() && fImage != null) {
720         // Make sure size is proportional to actual image size:
721         float imgWidth = fImage.getWidth(this);
722         float imgHeight = fImage.getHeight(this);
723         if (imgWidth > 0 && imgHeight > 0) {
724           float prop = imgHeight / imgWidth;
725           float pwidth = height / prop;
726           float pheight = width * prop;
727           if (pwidth > width)
728             width = (int) pwidth;
729           else
730             height = (int) pheight;
731         }
732       }
733 
734       resize(width, height);
735     }
736   }
737 
738   /***
739    * Mouse event
740    * @param e Mouse event
741    */
742   public void mouseReleased(final MouseEvent e) {
743     fGrowBase = null;
744     //! Should post some command to make the action undo-able
745   }
746 
747   /***
748    * On double-click, open image properties dialog.
749    * @param e Mouse event
750    */
751   public void mouseClicked(final MouseEvent e) {
752     if (e.getClickCount() == 2) {
753       //$ IMPLEMENT
754     }
755   }
756 
757   /***
758    * Mouse event
759    * @param e Mouse event
760    */
761   public void mouseEntered(final MouseEvent e) {
762   }
763 
764   /***
765    * Mouse event
766    * @param e Mouse event
767    */
768   public void mouseMoved(final MouseEvent e) {
769   }
770 
771   /***
772    * Mouse event
773    * @param e Mouse event
774    */
775   public void mouseExited(final MouseEvent e) {
776   }
777 
778   // --- Static icon accessors -------------------------------------------
779 
780   private Icon makeIcon(final String gifFile) throws IOException {
781     /*
782      * Copy resource into a byte array. This is necessary because several
783      * browsers consider Class.getResource a security risk because it can be
784      * used to load additional classes. Class.getResourceAsStream just returns
785      * raw bytes, which we can convert to an image.
786      */
787     InputStream resource = MyImageView.class.getResourceAsStream(gifFile);
788 
789     if (resource == null) {
790       log.error(MyImageView.class.getName() + "/" + gifFile + " not found.");
791       return null;
792     }
793     BufferedInputStream in = new BufferedInputStream(resource);
794     ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
795     byte[] buffer = new byte[1024];
796     int n;
797     while ((n = in.read(buffer)) > 0) {
798       out.write(buffer, 0, n);
799     }
800     in.close();
801     out.flush();
802 
803     buffer = out.toByteArray();
804     if (buffer.length == 0) {
805       log.warn("warning: " + gifFile + " is zero-length");
806       return null;
807     }
808     return new ImageIcon(buffer);
809   }
810 
811   private void loadIcons() {
812     try {
813       if (sPendingImageIcon == null)
814         sPendingImageIcon = makeIcon(PENDING_IMAGE_SRC);
815       if (sMissingImageIcon == null)
816         sMissingImageIcon = makeIcon(MISSING_IMAGE_SRC);
817     } catch (IOException x) {
818       log.error("ImageView: Couldn't load image icons");
819     }
820   }
821 
822   protected StyleSheet getStyleSheet() {
823     HTMLDocument doc = (HTMLDocument) getDocument();
824     return doc.getStyleSheet();
825   }
826 
827   // --- Getter and setters ----------------------------------------------
828 
829   /***
830    * Get the map of the images.
831    * @return Returns the mapImages
832    */
833   public Map getMapImages() {
834     return mapImages;
835   }
836 
837   /***
838    * Set the map of the images.
839    * @param mapImages The mapImages to set
840    */
841   public void setMapImages(final Map mapImages) {
842     this.mapImages = mapImages;
843   }
844 
845   // --- member variables ------------------------------------------------
846 
847   private Map mapImages;
848 
849   private AttributeSet attr;
850   private Element fElement;
851   private Image fImage;
852   private int fHeight, fWidth;
853   private Container fContainer;
854   private Rectangle fBounds;
855   private Component fComponent;
856   private Point fGrowBase; // base of drag while growing image
857   private boolean fGrowProportionally; // should grow be
858   // proportional?
859   /***
860    * Set to true, while the receiver is locked, to indicate the reciever is
861    * loading the image. This is used in imageUpdate.
862    */
863   private boolean loading;
864 
865   // --- constants and static stuff --------------------------------
866 
867   private static Icon sPendingImageIcon, sMissingImageIcon;
868   private static final String PENDING_IMAGE_SRC = "icons/image-delayed.gif", // both
869       // stolen
870       // from
871       // HotJava
872       MISSING_IMAGE_SRC = "icons/image-failed.gif";
873 
874   private static final boolean DEBUG = false;
875 
876   //$ move this someplace public
877   static final String IMAGE_CACHE_PROPERTY = "imageCache";
878 
879   // Height/width to use before we know the real size:
880   private static final int DEFAULT_WIDTH = 32, DEFAULT_HEIGHT = 32,
881   // Default value of BORDER param: //? possibly move into stylesheet?
882       DEFAULT_BORDER = 2;
883 
884 }
885