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