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