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} 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}. 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} 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} attribute. This is overriden to return 268 * {@code getAltText}. 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 >= 0 575 * @param height the height >= 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} or 676 * {@code updateImageSize} 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}. This should 705 * only be invoked from {@code refreshImage}. 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}. 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} 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 }