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