View Javadoc

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