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