1 /*
2 * Copyright 1997-2005 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25 package javax.swing.text.html;
26
27 import java.awt;
28 import java.awt.event;
29 import java.awt.image.ImageObserver;
30 import java.io;
31 import java.net;
32 import java.util.Dictionary;
33 import javax.swing;
34 import javax.swing.text;
35 import javax.swing.event;
36
37 /**
38 * View of an Image, intended to support the HTML <IMG> tag.
39 * Supports scaling via the HEIGHT and WIDTH attributes of the tag.
40 * If the image is unable to be loaded any text specified via the
41 * <code>ALT</code> attribute will be rendered.
42 * <p>
43 * While this class has been part of swing for a while now, it is public
44 * as of 1.4.
45 *
46 * @author Scott Violet
47 * @see IconView
48 * @since 1.4
49 */
50 public class ImageView extends View {
51 /**
52 * If true, when some of the bits are available a repaint is done.
53 * <p>
54 * This is set to false as swing does not offer a repaint that takes a
55 * delay. If this were true, a bunch of immediate repaints would get
56 * generated that end up significantly delaying the loading of the image
57 * (or anything else going on for that matter).
58 */
59 private static boolean sIsInc = false;
60 /**
61 * Repaint delay when some of the bits are available.
62 */
63 private static int sIncRate = 100;
64 /**
65 * Property name for pending image icon
66 */
67 private static final String PENDING_IMAGE = "html.pendingImage";
68 /**
69 * Property name for missing image icon
70 */
71 private static final String MISSING_IMAGE = "html.missingImage";
72
73 /**
74 * Document property for image cache.
75 */
76 private static final String IMAGE_CACHE_PROPERTY = "imageCache";
77
78 // Height/width to use before we know the real size, these should at least
79 // the size of <code>sMissingImageIcon</code> and
80 // <code>sPendingImageIcon</code>
81 private static final int DEFAULT_WIDTH = 38;
82 private static final int DEFAULT_HEIGHT= 38;
83
84 /**
85 * Default border to use if one is not specified.
86 */
87 private static final int DEFAULT_BORDER = 2;
88
89 // Bitmask values
90 private static final int LOADING_FLAG = 1;
91 private static final int LINK_FLAG = 2;
92 private static final int WIDTH_FLAG = 4;
93 private static final int HEIGHT_FLAG = 8;
94 private static final int RELOAD_FLAG = 16;
95 private static final int RELOAD_IMAGE_FLAG = 32;
96 private static final int SYNC_LOAD_FLAG = 64;
97
98 private AttributeSet attr;
99 private Image image;
100 private int width;
101 private int height;
102 /** Bitmask containing some of the above bitmask values. Because the
103 * image loading notification can happen on another thread access to
104 * this is synchronized (at least for modifying it). */
105 private int state;
106 private Container container;
107 private Rectangle fBounds;
108 private Color borderColor;
109 // Size of the border, the insets contains this valid. For example, if
110 // the HSPACE attribute was 4 and BORDER 2, leftInset would be 6.
111 private short borderSize;
112 // Insets, obtained from the painter.
113 private short leftInset;
114 private short rightInset;
115 private short topInset;
116 private short bottomInset;
117 /**
118 * We don't directly implement ImageObserver, instead we use an instance
119 * that calls back to us.
120 */
121 private ImageObserver imageObserver;
122 /**
123 * Used for alt text. Will be non-null if the image couldn't be found,
124 * and there is valid alt text.
125 */
126 private View altView;
127 /** Alignment along the vertical (Y) axis. */
128 private float vAlign;
129
130
131
132 /**
133 * Creates a new view that represents an IMG element.
134 *
135 * @param elem the element to create a view for
136 */
137 public ImageView(Element elem) {
138 super(elem);
139 fBounds = new Rectangle();
140 imageObserver = new ImageHandler();
141 state = RELOAD_FLAG | RELOAD_IMAGE_FLAG;
142 }
143
144 /**
145 * Returns the text to display if the image can't be loaded. This is
146 * obtained from the Elements attribute set with the attribute name
147 * <code>HTML.Attribute.ALT</code>.
148 */
149 public String getAltText() {
150 return (String)getElement().getAttributes().getAttribute
151 (HTML.Attribute.ALT);
152 }
153
154 /**
155 * Return a URL for the image source,
156 * or null if it could not be determined.
157 */
158 public URL getImageURL() {
159 String src = (String)getElement().getAttributes().
160 getAttribute(HTML.Attribute.SRC);
161 if (src == null) {
162 return null;
163 }
164
165 URL reference = ((HTMLDocument)getDocument()).getBase();
166 try {
167 URL u = new URL(reference,src);
168 return u;
169 } catch (MalformedURLException e) {
170 return null;
171 }
172 }
173
174 /**
175 * Returns the icon to use if the image couldn't be found.
176 */
177 public Icon getNoImageIcon() {
178 return (Icon) UIManager.getLookAndFeelDefaults().get(MISSING_IMAGE);
179 }
180
181 /**
182 * Returns the icon to use while in the process of loading the image.
183 */
184 public Icon getLoadingImageIcon() {
185 return (Icon) UIManager.getLookAndFeelDefaults().get(PENDING_IMAGE);
186 }
187
188 /**
189 * Returns the image to render.
190 */
191 public Image getImage() {
192 sync();
193 return image;
194 }
195
196 /**
197 * Sets how the image is loaded. If <code>newValue</code> is true,
198 * the image we be loaded when first asked for, otherwise it will
199 * be loaded asynchronously. The default is to not load synchronously,
200 * that is to load the image asynchronously.
201 */
202 public void setLoadsSynchronously(boolean newValue) {
203 synchronized(this) {
204 if (newValue) {
205 state |= SYNC_LOAD_FLAG;
206 }
207 else {
208 state = (state | SYNC_LOAD_FLAG) ^ SYNC_LOAD_FLAG;
209 }
210 }
211 }
212
213 /**
214 * Returns true if the image should be loaded when first asked for.
215 */
216 public boolean getLoadsSynchronously() {
217 return ((state & SYNC_LOAD_FLAG) != 0);
218 }
219
220 /**
221 * Convenience method to get the StyleSheet.
222 */
223 protected StyleSheet getStyleSheet() {
224 HTMLDocument doc = (HTMLDocument) getDocument();
225 return doc.getStyleSheet();
226 }
227
228 /**
229 * Fetches the attributes to use when rendering. This is
230 * implemented to multiplex the attributes specified in the
231 * model with a StyleSheet.
232 */
233 public AttributeSet getAttributes() {
234 sync();
235 return attr;
236 }
237
238 /**
239 * For images the tooltip text comes from text specified with the
240 * <code>ALT</code> attribute. This is overriden to return
241 * <code>getAltText</code>.
242 *
243 * @see JTextComponent#getToolTipText
244 */
245 public String getToolTipText(float x, float y, Shape allocation) {
246 return getAltText();
247 }
248
249 /**
250 * Update any cached values that come from attributes.
251 */
252 protected void setPropertiesFromAttributes() {
253 StyleSheet sheet = getStyleSheet();
254 this.attr = sheet.getViewAttributes(this);
255
256 // Gutters
257 borderSize = (short)getIntAttr(HTML.Attribute.BORDER, isLink() ?
258 DEFAULT_BORDER : 0);
259
260 leftInset = rightInset = (short)(getIntAttr(HTML.Attribute.HSPACE,
261 0) + borderSize);
262 topInset = bottomInset = (short)(getIntAttr(HTML.Attribute.VSPACE,
263 0) + borderSize);
264
265 borderColor = ((StyledDocument)getDocument()).getForeground
266 (getAttributes());
267
268 AttributeSet attr = getElement().getAttributes();
269
270 // Alignment.
271 // PENDING: This needs to be changed to support the CSS versions
272 // when conversion from ALIGN to VERTICAL_ALIGN is complete.
273 Object alignment = attr.getAttribute(HTML.Attribute.ALIGN);
274
275 vAlign = 1.0f;
276 if (alignment != null) {
277 alignment = alignment.toString();
278 if ("top".equals(alignment)) {
279 vAlign = 0f;
280 }
281 else if ("middle".equals(alignment)) {
282 vAlign = .5f;
283 }
284 }
285
286 AttributeSet anchorAttr = (AttributeSet)attr.getAttribute(HTML.Tag.A);
287 if (anchorAttr != null && anchorAttr.isDefined
288 (HTML.Attribute.HREF)) {
289 synchronized(this) {
290 state |= LINK_FLAG;
291 }
292 }
293 else {
294 synchronized(this) {
295 state = (state | LINK_FLAG) ^ LINK_FLAG;
296 }
297 }
298 }
299
300 /**
301 * Establishes the parent view for this view.
302 * Seize this moment to cache the AWT Container I'm in.
303 */
304 public void setParent(View parent) {
305 View oldParent = getParent();
306 super.setParent(parent);
307 container = (parent != null) ? getContainer() : null;
308 if (oldParent != parent) {
309 synchronized(this) {
310 state |= RELOAD_FLAG;
311 }
312 }
313 }
314
315 /**
316 * Invoked when the Elements attributes have changed. Recreates the image.
317 */
318 public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) {
319 super.changedUpdate(e,a,f);
320
321 synchronized(this) {
322 state |= RELOAD_FLAG | RELOAD_IMAGE_FLAG;
323 }
324
325 // Assume the worst.
326 preferenceChanged(null, true, true);
327 }
328
329 /**
330 * Paints the View.
331 *
332 * @param g the rendering surface to use
333 * @param a the allocated region to render into
334 * @see View#paint
335 */
336 public void paint(Graphics g, Shape a) {
337 sync();
338
339 Rectangle rect = (a instanceof Rectangle) ? (Rectangle)a :
340 a.getBounds();
341
342 Image image = getImage();
343 Rectangle clip = g.getClipBounds();
344
345 fBounds.setBounds(rect);
346 paintHighlights(g, a);
347 paintBorder(g, rect);
348 if (clip != null) {
349 g.clipRect(rect.x + leftInset, rect.y + topInset,
350 rect.width - leftInset - rightInset,
351 rect.height - topInset - bottomInset);
352 }
353 if (image != null) {
354 if (!hasPixels(image)) {
355 // No pixels yet, use the default
356 Icon icon = (image == null) ? getNoImageIcon() :
357 getLoadingImageIcon();
358
359 if (icon != null) {
360 icon.paintIcon(getContainer(), g, rect.x + leftInset,
361 rect.y + topInset);
362 }
363 }
364 else {
365 // Draw the image
366 g.drawImage(image, rect.x + leftInset, rect.y + topInset,
367 width, height, imageObserver);
368 }
369 }
370 else {
371 Icon icon = getNoImageIcon();
372
373 if (icon != null) {
374 icon.paintIcon(getContainer(), g, rect.x + leftInset,
375 rect.y + topInset);
376 }
377 View view = getAltView();
378 // Paint the view representing the alt text, if its non-null
379 if (view != null && ((state & WIDTH_FLAG) == 0 ||
380 width > DEFAULT_WIDTH)) {
381 // Assume layout along the y direction
382 Rectangle altRect = new Rectangle
383 (rect.x + leftInset + DEFAULT_WIDTH, rect.y + topInset,
384 rect.width - leftInset - rightInset - DEFAULT_WIDTH,
385 rect.height - topInset - bottomInset);
386
387 view.paint(g, altRect);
388 }
389 }
390 if (clip != null) {
391 // Reset clip.
392 g.setClip(clip.x, clip.y, clip.width, clip.height);
393 }
394 }
395
396 private void paintHighlights(Graphics g, Shape shape) {
397 if (container instanceof JTextComponent) {
398 JTextComponent tc = (JTextComponent)container;
399 Highlighter h = tc.getHighlighter();
400 if (h instanceof LayeredHighlighter) {
401 ((LayeredHighlighter)h).paintLayeredHighlights
402 (g, getStartOffset(), getEndOffset(), shape, tc, this);
403 }
404 }
405 }
406
407 private void paintBorder(Graphics g, Rectangle rect) {
408 Color color = borderColor;
409
410 if ((borderSize > 0 || image == null) && color != null) {
411 int xOffset = leftInset - borderSize;
412 int yOffset = topInset - borderSize;
413 g.setColor(color);
414 int n = (image == null) ? 1 : borderSize;
415 for (int counter = 0; counter < n; counter++) {
416 g.drawRect(rect.x + xOffset + counter,
417 rect.y + yOffset + counter,
418 rect.width - counter - counter - xOffset -xOffset-1,
419 rect.height - counter - counter -yOffset-yOffset-1);
420 }
421 }
422 }
423
424 /**
425 * Determines the preferred span for this view along an
426 * axis.
427 *
428 * @param axis may be either X_AXIS or Y_AXIS
429 * @return the span the view would like to be rendered into;
430 * typically the view is told to render into the span
431 * that is returned, although there is no guarantee;
432 * the parent may choose to resize or break the view
433 */
434 public float getPreferredSpan(int axis) {
435 sync();
436
437 // If the attributes specified a width/height, always use it!
438 if (axis == View.X_AXIS && (state & WIDTH_FLAG) == WIDTH_FLAG) {
439 getPreferredSpanFromAltView(axis);
440 return width + leftInset + rightInset;
441 }
442 if (axis == View.Y_AXIS && (state & HEIGHT_FLAG) == HEIGHT_FLAG) {
443 getPreferredSpanFromAltView(axis);
444 return height + topInset + bottomInset;
445 }
446
447 Image image = getImage();
448
449 if (image != null) {
450 switch (axis) {
451 case View.X_AXIS:
452 return width + leftInset + rightInset;
453 case View.Y_AXIS:
454 return height + topInset + bottomInset;
455 default:
456 throw new IllegalArgumentException("Invalid axis: " + axis);
457 }
458 }
459 else {
460 View view = getAltView();
461 float retValue = 0f;
462
463 if (view != null) {
464 retValue = view.getPreferredSpan(axis);
465 }
466 switch (axis) {
467 case View.X_AXIS:
468 return retValue + (float)(width + leftInset + rightInset);
469 case View.Y_AXIS:
470 return retValue + (float)(height + topInset + bottomInset);
471 default:
472 throw new IllegalArgumentException("Invalid axis: " + axis);
473 }
474 }
475 }
476
477 /**
478 * Determines the desired alignment for this view along an
479 * axis. This is implemented to give the alignment to the
480 * bottom of the icon along the y axis, and the default
481 * along the x axis.
482 *
483 * @param axis may be either X_AXIS or Y_AXIS
484 * @return the desired alignment; this should be a value
485 * between 0.0 and 1.0 where 0 indicates alignment at the
486 * origin and 1.0 indicates alignment to the full span
487 * away from the origin; an alignment of 0.5 would be the
488 * center of the view
489 */
490 public float getAlignment(int axis) {
491 switch (axis) {
492 case View.Y_AXIS:
493 return vAlign;
494 default:
495 return super.getAlignment(axis);
496 }
497 }
498
499 /**
500 * Provides a mapping from the document model coordinate space
501 * to the coordinate space of the view mapped to it.
502 *
503 * @param pos the position to convert
504 * @param a the allocated region to render into
505 * @return the bounding box of the given position
506 * @exception BadLocationException if the given position does not represent a
507 * valid location in the associated document
508 * @see View#modelToView
509 */
510 public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
511 int p0 = getStartOffset();
512 int p1 = getEndOffset();
513 if ((pos >= p0) && (pos <= p1)) {
514 Rectangle r = a.getBounds();
515 if (pos == p1) {
516 r.x += r.width;
517 }
518 r.width = 0;
519 return r;
520 }
521 return null;
522 }
523
524 /**
525 * Provides a mapping from the view coordinate space to the logical
526 * coordinate space of the model.
527 *
528 * @param x the X coordinate
529 * @param y the Y coordinate
530 * @param a the allocated region to render into
531 * @return the location within the model that best represents the
532 * given point of view
533 * @see View#viewToModel
534 */
535 public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) {
536 Rectangle alloc = (Rectangle) a;
537 if (x < alloc.x + alloc.width) {
538 bias[0] = Position.Bias.Forward;
539 return getStartOffset();
540 }
541 bias[0] = Position.Bias.Backward;
542 return getEndOffset();
543 }
544
545 /**
546 * Sets the size of the view. This should cause
547 * layout of the view if it has any layout duties.
548 *
549 * @param width the width >= 0
550 * @param height the height >= 0
551 */
552 public void setSize(float width, float height) {
553 sync();
554
555 if (getImage() == null) {
556 View view = getAltView();
557
558 if (view != null) {
559 view.setSize(Math.max(0f, width - (float)(DEFAULT_WIDTH + leftInset + rightInset)),
560 Math.max(0f, height - (float)(topInset + bottomInset)));
561 }
562 }
563 }
564
565 /**
566 * Returns true if this image within a link?
567 */
568 private boolean isLink() {
569 return ((state & LINK_FLAG) == LINK_FLAG);
570 }
571
572 /**
573 * Returns true if the passed in image has a non-zero width and height.
574 */
575 private boolean hasPixels(Image image) {
576 return image != null &&
577 (image.getHeight(imageObserver) > 0) &&
578 (image.getWidth(imageObserver) > 0);
579 }
580
581 /**
582 * Returns the preferred span of the View used to display the alt text,
583 * or 0 if the view does not exist.
584 */
585 private float getPreferredSpanFromAltView(int axis) {
586 if (getImage() == null) {
587 View view = getAltView();
588
589 if (view != null) {
590 return view.getPreferredSpan(axis);
591 }
592 }
593 return 0f;
594 }
595
596 /**
597 * Request that this view be repainted.
598 * Assumes the view is still at its last-drawn location.
599 */
600 private void repaint(long delay) {
601 if (container != null && fBounds != null) {
602 container.repaint(delay, fBounds.x, fBounds.y, fBounds.width,
603 fBounds.height);
604 }
605 }
606
607 /**
608 * Convenience method for getting an integer attribute from the elements
609 * AttributeSet.
610 */
611 private int getIntAttr(HTML.Attribute name, int deflt) {
612 AttributeSet attr = getElement().getAttributes();
613 if (attr.isDefined(name)) { // does not check parents!
614 int i;
615 String val = (String)attr.getAttribute(name);
616 if (val == null) {
617 i = deflt;
618 }
619 else {
620 try{
621 i = Math.max(0, Integer.parseInt(val));
622 }catch( NumberFormatException x ) {
623 i = deflt;
624 }
625 }
626 return i;
627 } else
628 return deflt;
629 }
630
631 /**
632 * Makes sure the necessary properties and image is loaded.
633 */
634 private void sync() {
635 int s = state;
636 if ((s & RELOAD_IMAGE_FLAG) != 0) {
637 refreshImage();
638 }
639 s = state;
640 if ((s & RELOAD_FLAG) != 0) {
641 synchronized(this) {
642 state = (state | RELOAD_FLAG) ^ RELOAD_FLAG;
643 }
644 setPropertiesFromAttributes();
645 }
646 }
647
648 /**
649 * Loads the image and updates the size accordingly. This should be
650 * invoked instead of invoking <code>loadImage</code> or
651 * <code>updateImageSize</code> directly.
652 */
653 private void refreshImage() {
654 synchronized(this) {
655 // clear out width/height/realoadimage flag and set loading flag
656 state = (state | LOADING_FLAG | RELOAD_IMAGE_FLAG | WIDTH_FLAG |
657 HEIGHT_FLAG) ^ (WIDTH_FLAG | HEIGHT_FLAG |
658 RELOAD_IMAGE_FLAG);
659 image = null;
660 width = height = 0;
661 }
662
663 try {
664 // Load the image
665 loadImage();
666
667 // And update the size params
668 updateImageSize();
669 }
670 finally {
671 synchronized(this) {
672 // Clear out state in case someone threw an exception.
673 state = (state | LOADING_FLAG) ^ LOADING_FLAG;
674 }
675 }
676 }
677
678 /**
679 * Loads the image from the URL <code>getImageURL</code>. This should
680 * only be invoked from <code>refreshImage</code>.
681 */
682 private void loadImage() {
683 URL src = getImageURL();
684 Image newImage = null;
685 if (src != null) {
686 Dictionary cache = (Dictionary)getDocument().
687 getProperty(IMAGE_CACHE_PROPERTY);
688 if (cache != null) {
689 newImage = (Image)cache.get(src);
690 }
691 else {
692 newImage = Toolkit.getDefaultToolkit().createImage(src);
693 if (newImage != null && getLoadsSynchronously()) {
694 // Force the image to be loaded by using an ImageIcon.
695 ImageIcon ii = new ImageIcon();
696 ii.setImage(newImage);
697 }
698 }
699 }
700 image = newImage;
701 }
702
703 /**
704 * Recreates and reloads the image. This should
705 * only be invoked from <code>refreshImage</code>.
706 */
707 private void updateImageSize() {
708 int newWidth = 0;
709 int newHeight = 0;
710 int newState = 0;
711 Image newImage = getImage();
712
713 if (newImage != null) {
714 Element elem = getElement();
715 AttributeSet attr = elem.getAttributes();
716
717 // Get the width/height and set the state ivar before calling
718 // anything that might cause the image to be loaded, and thus the
719 // ImageHandler to be called.
720 newWidth = getIntAttr(HTML.Attribute.WIDTH, -1);
721 if (newWidth > 0) {
722 newState |= WIDTH_FLAG;
723 }
724 newHeight = getIntAttr(HTML.Attribute.HEIGHT, -1);
725 if (newHeight > 0) {
726 newState |= HEIGHT_FLAG;
727 }
728
729 if (newWidth <= 0) {
730 newWidth = newImage.getWidth(imageObserver);
731 if (newWidth <= 0) {
732 newWidth = DEFAULT_WIDTH;
733 }
734 }
735
736 if (newHeight <= 0) {
737 newHeight = newImage.getHeight(imageObserver);
738 if (newHeight <= 0) {
739 newHeight = DEFAULT_HEIGHT;
740 }
741 }
742
743 // Make sure the image starts loading:
744 if ((newState & (WIDTH_FLAG | HEIGHT_FLAG)) != 0) {
745 Toolkit.getDefaultToolkit().prepareImage(newImage, newWidth,
746 newHeight,
747 imageObserver);
748 }
749 else {
750 Toolkit.getDefaultToolkit().prepareImage(newImage, -1, -1,
751 imageObserver);
752 }
753
754 boolean createText = false;
755 synchronized(this) {
756 // If imageloading failed, other thread may have called
757 // ImageLoader which will null out image, hence we check
758 // for it.
759 if (image != null) {
760 if ((newState & WIDTH_FLAG) == WIDTH_FLAG || width == 0) {
761 width = newWidth;
762 }
763 if ((newState & HEIGHT_FLAG) == HEIGHT_FLAG ||
764 height == 0) {
765 height = newHeight;
766 }
767 }
768 else {
769 createText = true;
770 if ((newState & WIDTH_FLAG) == WIDTH_FLAG) {
771 width = newWidth;
772 }
773 if ((newState & HEIGHT_FLAG) == HEIGHT_FLAG) {
774 height = newHeight;
775 }
776 }
777 state = state | newState;
778 state = (state | LOADING_FLAG) ^ LOADING_FLAG;
779 }
780 if (createText) {
781 // Only reset if this thread determined image is null
782 updateAltTextView();
783 }
784 }
785 else {
786 width = height = DEFAULT_HEIGHT;
787 updateAltTextView();
788 }
789 }
790
791 /**
792 * Updates the view representing the alt text.
793 */
794 private void updateAltTextView() {
795 String text = getAltText();
796
797 if (text != null) {
798 ImageLabelView newView;
799
800 newView = new ImageLabelView(getElement(), text);
801 synchronized(this) {
802 altView = newView;
803 }
804 }
805 }
806
807 /**
808 * Returns the view to use for alternate text. This may be null.
809 */
810 private View getAltView() {
811 View view;
812
813 synchronized(this) {
814 view = altView;
815 }
816 if (view != null && view.getParent() == null) {
817 view.setParent(getParent());
818 }
819 return view;
820 }
821
822 /**
823 * Invokes <code>preferenceChanged</code> on the event displatching
824 * thread.
825 */
826 private void safePreferenceChanged() {
827 if (SwingUtilities.isEventDispatchThread()) {
828 Document doc = getDocument();
829 if (doc instanceof AbstractDocument) {
830 ((AbstractDocument)doc).readLock();
831 }
832 preferenceChanged(null, true, true);
833 if (doc instanceof AbstractDocument) {
834 ((AbstractDocument)doc).readUnlock();
835 }
836 }
837 else {
838 SwingUtilities.invokeLater(new Runnable() {
839 public void run() {
840 safePreferenceChanged();
841 }
842 });
843 }
844 }
845
846 /**
847 * ImageHandler implements the ImageObserver to correctly update the
848 * display as new parts of the image become available.
849 */
850 private class ImageHandler implements ImageObserver {
851 // This can come on any thread. If we are in the process of reloading
852 // the image and determining our state (loading == true) we don't fire
853 // preference changed, or repaint, we just reset the fWidth/fHeight as
854 // necessary and return. This is ok as we know when loading finishes
855 // it will pick up the new height/width, if necessary.
856 public boolean imageUpdate(Image img, int flags, int x, int y,
857 int newWidth, int newHeight ) {
858 if (image == null || image != img || getParent() == null) {
859 return false;
860 }
861
862 // Bail out if there was an error:
863 if ((flags & (ABORT|ERROR)) != 0) {
864 repaint(0);
865 synchronized(ImageView.this) {
866 if (image == img) {
867 // Be sure image hasn't changed since we don't
868 // initialy synchronize
869 image = null;
870 if ((state & WIDTH_FLAG) != WIDTH_FLAG) {
871 width = DEFAULT_WIDTH;
872 }
873 if ((state & HEIGHT_FLAG) != HEIGHT_FLAG) {
874 height = DEFAULT_HEIGHT;
875 }
876 }
877 if ((state & LOADING_FLAG) == LOADING_FLAG) {
878 // No need to resize or repaint, still in the process
879 // of loading.
880 return false;
881 }
882 }
883 updateAltTextView();
884 safePreferenceChanged();
885 return false;
886 }
887
888 // Resize image if necessary:
889 short changed = 0;
890 if ((flags & ImageObserver.HEIGHT) != 0 && !getElement().
891 getAttributes().isDefined(HTML.Attribute.HEIGHT)) {
892 changed |= 1;
893 }
894 if ((flags & ImageObserver.WIDTH) != 0 && !getElement().
895 getAttributes().isDefined(HTML.Attribute.WIDTH)) {
896 changed |= 2;
897 }
898
899 synchronized(ImageView.this) {
900 if (image != img) {
901 return false;
902 }
903 if ((changed & 1) == 1 && (state & WIDTH_FLAG) == 0) {
904 width = newWidth;
905 }
906 if ((changed & 2) == 2 && (state & HEIGHT_FLAG) == 0) {
907 height = newHeight;
908 }
909 if ((state & LOADING_FLAG) == LOADING_FLAG) {
910 // No need to resize or repaint, still in the process of
911 // loading.
912 return true;
913 }
914 }
915 if (changed != 0) {
916 // May need to resize myself, asynchronously:
917 safePreferenceChanged();
918 return true;
919 }
920
921 // Repaint when done or when new pixels arrive:
922 if ((flags & (FRAMEBITS|ALLBITS)) != 0) {
923 repaint(0);
924 }
925 else if ((flags & SOMEBITS) != 0 && sIsInc) {
926 repaint(sIncRate);
927 }
928 return ((flags & ALLBITS) == 0);
929 }
930 }
931
932
933 /**
934 * ImageLabelView is used if the image can't be loaded, and
935 * the attribute specified an alt attribute. It overriden a handle of
936 * methods as the text is hardcoded and does not come from the document.
937 */
938 private class ImageLabelView extends InlineView {
939 private Segment segment;
940 private Color fg;
941
942 ImageLabelView(Element e, String text) {
943 super(e);
944 reset(text);
945 }
946
947 public void reset(String text) {
948 segment = new Segment(text.toCharArray(), 0, text.length());
949 }
950
951 public void paint(Graphics g, Shape a) {
952 // Don't use supers paint, otherwise selection will be wrong
953 // as our start/end offsets are fake.
954 GlyphPainter painter = getGlyphPainter();
955
956 if (painter != null) {
957 g.setColor(getForeground());
958 painter.paint(this, g, a, getStartOffset(), getEndOffset());
959 }
960 }
961
962 public Segment getText(int p0, int p1) {
963 if (p0 < 0 || p1 > segment.array.length) {
964 throw new RuntimeException("ImageLabelView: Stale view");
965 }
966 segment.offset = p0;
967 segment.count = p1 - p0;
968 return segment;
969 }
970
971 public int getStartOffset() {
972 return 0;
973 }
974
975 public int getEndOffset() {
976 return segment.array.length;
977 }
978
979 public View breakView(int axis, int p0, float pos, float len) {
980 // Don't allow a break
981 return this;
982 }
983
984 public Color getForeground() {
985 View parent;
986 if (fg == null && (parent = getParent()) != null) {
987 Document doc = getDocument();
988 AttributeSet attr = parent.getAttributes();
989
990 if (attr != null && (doc instanceof StyledDocument)) {
991 fg = ((StyledDocument)doc).getForeground(attr);
992 }
993 }
994 return fg;
995 }
996 }
997 }