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