/* * @(#)ImageView.java 1.50 01/12/03 * * Copyright 2002 Sun Microsystems, Inc. All rights reserved. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */ package gmgen.gui; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.text.*; import javax.swing.text.html.HTML; import javax.swing.text.html.HTMLDocument; import javax.swing.text.html.InlineView; import javax.swing.text.html.StyleSheet; import java.awt.*; import java.awt.image.ImageObserver; import java.io.*; import java.net.MalformedURLException; import java.net.URL; import java.util.Dictionary; /** * View of an Image, intended to support the HTML <IMG> tag. * Supports scaling via the HEIGHT and WIDTH attributes of the tag. * If the image is unable to be loaded any text specified via the * {@code ALT} attribute will be rendered. * <p> * While this class has been part of swing for a while now, it is public * as of 1.4. * * @author Scott Violet * @see javax.swing.text.IconView */ public class RelativeImageView extends View implements ImageObserver { /** * Icon used while the image is being loaded. */ private static Icon sPendingImageIcon; /** * Icon used if the image could not be found. */ private static Icon sMissingImageIcon; /** * File name for {@code sPendingImageIcon}. */ private static final String PENDING_IMAGE_SRC = "icons/image-delayed.gif"; /** * File name for {@code sMissingImageIcon}. */ private static final String MISSING_IMAGE_SRC = "icons/image-failed.gif"; /** * Document property for image cache. */ private static final String IMAGE_CACHE_PROPERTY = "imageCache"; // Height/width to use before we know the real size, these should at least // the size of <code>sMissingImageIcon</code> and // <code>sPendingImageIcon</code> private static final int DEFAULT_WIDTH = 38; private static final int DEFAULT_HEIGHT = 38; /** * Default border to use if one is not specified. */ private static final int DEFAULT_BORDER = 2; // Bitmask values private static final int LOADING_FLAG = 1; private static final int LINK_FLAG = 2; private static final int WIDTH_FLAG = 4; private static final int HEIGHT_FLAG = 8; private static final int RELOAD_FLAG = 16; private static final int RELOAD_IMAGE_FLAG = 32; private static final int SYNC_LOAD_FLAG = 64; private AttributeSet attr; private Color borderColor; private Container container; private Image image; /** * We don't directly implement ImageObserver, instead we use an instance * that calls back to us. */ private ImageObserver imageObserver; private Rectangle fBounds; /** * Used for alt text. Will be non-null if the image couldn't be found, * and there is valid alt text. */ private View altView; /** Alignment along the vertical (Y) axis. */ private float vAlign; private int height; /** Bitmask containing some of the above bitmask values. Because the * image loading notification can happen on another thread access to * this is synchronized (at least for modifying it). */ private int state; private int width; // Size of the border, the insets contains this valid. For example, if // the HSPACE attribute was 4 and BORDER 2, leftInset would be 6. private short borderSize; private short bottomInset; // Insets, obtained from the painter. private short leftInset; private short rightInset; private short topInset; /** * Creates a new view that represents an IMG element. * * @param elem the element to create a view for */ public RelativeImageView(Element elem) { super(elem); fBounds = new Rectangle(); imageObserver = new ImageHandler(); state = RELOAD_FLAG | RELOAD_IMAGE_FLAG; } /** * Determines the desired alignment for this view along an * axis. This is implemented to give the alignment to the * bottom of the icon along the y axis, and the default * along the x axis. * * @param axis may be either X_AXIS or Y_AXIS * @return the desired alignment; this should be a value * between 0.0 and 1.0 where 0 indicates alignment at the * origin and 1.0 indicates alignment to the full span * away from the origin; an alignment of 0.5 would be the * center of the view */ @Override public float getAlignment(int axis) { switch (axis) { case View.Y_AXIS: return vAlign; default: return super.getAlignment(axis); } } /** * Returns the text to display if the image can't be loaded. This is * obtained from the Elements attribute set with the attribute name * {@code HTML.Attribute.ALT}. * @return alt text */ public String getAltText() { return (String) getElement().getAttributes().getAttribute(HTML.Attribute.ALT); } /** * Fetches the attributes to use when rendering. This is * implemented to multiplex the attributes specified in the * model with a StyleSheet. * @return Attribute Set */ @Override public AttributeSet getAttributes() { sync(); return attr; } /** * Returns the image to render. * @return Image */ public Image getImage() { sync(); return image; } /** * Return a URL for the image source, * or null if it could not be determined. * @return image URL */ public URL getImageURL() { String src = (String) getElement().getAttributes().getAttribute(HTML.Attribute.SRC); if (src == null) { return null; } URL reference = ((HTMLDocument) getDocument()).getBase(); try { URL u = new URL(reference, src); return u; } catch (MalformedURLException e) { return null; } } /** * Returns the icon to use while in the process of loading the image. * @return Icon */ public Icon getLoadingImageIcon() { loadDefaultIconsIfNecessary(); return sPendingImageIcon; } /** * Sets how the image is loaded. If {@code newValue} is true, * the image we be loaded when first asked for, otherwise it will * be loaded asynchronously. The default is to not load synchronously, * that is to load the image asynchronously. * @param newValue */ public void setLoadsSynchronously(boolean newValue) { synchronized (this) { if (newValue) { state |= SYNC_LOAD_FLAG; } else { state = (state | SYNC_LOAD_FLAG) ^ SYNC_LOAD_FLAG; } } } /** * Returns true if the image should be loaded when first asked for. * @return true or false */ public boolean getLoadsSynchronously() { return ((state & SYNC_LOAD_FLAG) != 0); } /** * Returns the icon to use if the image couldn't be found. * @return Icon */ public Icon getNoImageIcon() { loadDefaultIconsIfNecessary(); return sMissingImageIcon; } /** * Establishes the parent view for this view. * Seize this moment to cache the AWT Container I'm in. * @param parent */ @Override public void setParent(View parent) { View oldParent = getParent(); super.setParent(parent); container = (parent != null) ? getContainer() : null; if (oldParent != parent) { synchronized (this) { state |= RELOAD_FLAG; } } } /** * Determines the preferred span for this view along an * axis. * * @param axis may be either X_AXIS or Y_AXIS * @return the span the view would like to be rendered into; * typically the view is told to render into the span * that is returned, although there is no guarantee; * the parent may choose to resize or break the view */ @Override public float getPreferredSpan(int axis) { sync(); // If the attributes specified a width/height, always use it! if ((axis == View.X_AXIS) && ((state & WIDTH_FLAG) == WIDTH_FLAG)) { getPreferredSpanFromAltView(axis); return width + leftInset + rightInset; } if ((axis == View.Y_AXIS) && ((state & HEIGHT_FLAG) == HEIGHT_FLAG)) { getPreferredSpanFromAltView(axis); return height + topInset + bottomInset; } Image anImage = getImage(); if (anImage != null) { switch (axis) { case View.X_AXIS: return width + leftInset + rightInset; case View.Y_AXIS: return height + topInset + bottomInset; default: throw new IllegalArgumentException("Invalid axis: " + axis); } } View view = getAltView(); float retValue = 0.0f; if (view != null) { retValue = view.getPreferredSpan(axis); } switch (axis) { case View.X_AXIS: return retValue + (width + leftInset + rightInset); case View.Y_AXIS: return retValue + (height + topInset + bottomInset); default: throw new IllegalArgumentException("Invalid axis: " + axis); } } /** * Sets the size of the view. This should cause * layout of the view if it has any layout duties. * * @param width the width >= 0 * @param height the height >= 0 */ @Override public void setSize(float width, float height) { sync(); if (getImage() == null) { View view = getAltView(); if (view != null) { view.setSize(Math.max(0.0f, width - (DEFAULT_WIDTH + leftInset + rightInset)), Math.max(0.0f, height - (topInset + bottomInset))); } } } /** * For images the tooltip text comes from text specified with the * {@code ALT} attribute. This is overriden to return * {@code getAltText}. * @param x * @param y * @param allocation * @return tooltip text * * @see JTextComponent#getToolTipText */ @Override public String getToolTipText(float x, float y, Shape allocation) { return getAltText(); } /** * Invoked when the Elements attributes have changed. Recreates the image. * @param e * @param a * @param f */ @Override public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { super.changedUpdate(e, a, f); synchronized (this) { state |= (RELOAD_FLAG | RELOAD_IMAGE_FLAG); } // Assume the worst. preferenceChanged(null, true, true); } // --- Progressive display --------------------------------------------- // This can come on any thread. If we are in the process of reloading // the image and determining our state (loading == true) we don't fire // preference changed, or repaint, we just reset the fWidth/fHeight as // necessary and return. This is ok as we know when loading finishes // it will pick up the new height/width, if necessary. @Override public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { if ((image == null) || (image != img)) { return false; } // Bail out if there was an error: if ((infoflags & (ABORT | ERROR)) != 0) { image = null; repaint(0); return false; } // Resize image if necessary: short changed = 0; if ((infoflags & ImageObserver.HEIGHT) != 0) { if (!getElement().getAttributes().isDefined(HTML.Attribute.HEIGHT)) { changed |= 1; } } if ((infoflags & ImageObserver.WIDTH) != 0) { if (!getElement().getAttributes().isDefined(HTML.Attribute.WIDTH)) { changed |= 2; } } synchronized (this) { if ((changed & 1) == 1) { this.width = width; } if ((changed & 2) == 2) { this.height = height; } if ((state & LOADING_FLAG) == LOADING_FLAG) { // No need to resize or repaint, still in the process of // loading. return true; } } if (changed != 0) { Document doc = getDocument(); try { if (doc instanceof AbstractDocument) { ((AbstractDocument) doc).readLock(); } preferenceChanged(this, true, true); } finally { if (doc instanceof AbstractDocument) { ((AbstractDocument) doc).readUnlock(); } } return true; } // Repaint when done or when new pixels arrive: if ((infoflags & (FRAMEBITS | ALLBITS)) != 0) { repaint(0); } return ((infoflags & ALLBITS) == 0); } /** * Provides a mapping from the document model coordinate space * to the coordinate space of the view mapped to it. * * @param pos the position to convert * @param a the allocated region to render into * @param b * @return the bounding box of the given position */ @Override public Shape modelToView(int pos, Shape a, Position.Bias b) { int p0 = getStartOffset(); int p1 = getEndOffset(); if ((pos >= p0) && (pos <= p1)) { Rectangle r = a.getBounds(); if (pos == p1) { r.x += r.width; } r.width = 0; return r; } return null; } /** * Paints the View. * * @param g the rendering surface to use * @param allocation the allocated region to render into */ @Override public void paint(Graphics g, Shape allocation) { sync(); Rectangle rect = (allocation instanceof Rectangle) ? (Rectangle) allocation : allocation.getBounds(); Image anImage = getImage(); Rectangle clip = g.getClipBounds(); fBounds.setBounds(rect); paintHighlights(g, allocation); paintBorder(g, rect); if (clip != null) { g.clipRect(rect.x + leftInset, rect.y + topInset, rect.width - leftInset - rightInset, rect.height - topInset - bottomInset); } if (anImage != null) { if (hasPixels(anImage)) { // Draw the image g.drawImage(anImage, rect.x + leftInset, rect.y + topInset, width, height, imageObserver); } else { // No pixels yet, use the default Icon icon = getLoadingImageIcon(); if (icon != null) { icon.paintIcon(getContainer(), g, rect.x + leftInset, rect.y + topInset); } } } else { Icon icon = getNoImageIcon(); if (icon != null) { icon.paintIcon(getContainer(), g, rect.x + leftInset, rect.y + topInset); } View view = getAltView(); // Paint the view representing the alt text, if its non-null if ((view != null) && (((state & WIDTH_FLAG) == 0) || (width > DEFAULT_WIDTH))) { // Assume layout along the y direction Rectangle altRect = new Rectangle(rect.x + leftInset + DEFAULT_WIDTH, rect.y + topInset, rect.width - leftInset - rightInset - DEFAULT_WIDTH, rect.height - topInset - bottomInset); view.paint(g, altRect); } } if (clip != null) { // Reset clip. g.setClip(clip.x, clip.y, clip.width, clip.height); } } /** * Provides a mapping from the view coordinate space to the logical * coordinate space of the model. * * @param x the X coordinate * @param y the Y coordinate * @param a the allocated region to render into * @param biasReturn * @return the location within the model that best represents the * given point of view */ @Override public int viewToModel(float x, float y, Shape a, Position.Bias[] biasReturn) { Rectangle alloc = (Rectangle) a; if (x < (alloc.x + alloc.width)) { biasReturn[0] = Position.Bias.Forward; return getStartOffset(); } biasReturn[0] = Position.Bias.Backward; return getEndOffset(); } /** * Update any cached values that come from attributes. */ protected void setPropertiesFromAttributes() { StyleSheet sheet = getStyleSheet(); this.attr = sheet.getViewAttributes(this); // Gutters borderSize = (short) getIntAttr(HTML.Attribute.BORDER, isLink() ? DEFAULT_BORDER : 0); if ((borderSize == 0) && (image == null)) { borderSize = 1; } leftInset = rightInset = (short) (getIntAttr(HTML.Attribute.HSPACE, 0) + borderSize); topInset = bottomInset = (short) (getIntAttr(HTML.Attribute.VSPACE, 0) + borderSize); borderColor = ((StyledDocument) getDocument()).getForeground(getAttributes()); AttributeSet anAttr = getElement().getAttributes(); // Alignment. // PENDING: This needs to be changed to support the CSS versions // when conversion from ALIGN to VERTICAL_ALIGN is complete. Object alignment = anAttr.getAttribute(HTML.Attribute.ALIGN); vAlign = 1.0f; if (alignment != null) { alignment = alignment.toString(); if ("top".equals(alignment)) { vAlign = 0.0f; } else if ("middle".equals(alignment)) { vAlign = 0.5f; } } AttributeSet anchorAttr = (AttributeSet) anAttr.getAttribute(HTML.Tag.A); if ((anchorAttr != null) && anchorAttr.isDefined(HTML.Attribute.HREF)) { synchronized (this) { state |= LINK_FLAG; } } else { synchronized (this) { state = (state | LINK_FLAG) ^ LINK_FLAG; } } } /** * Convenience method to get the StyleSheet. * @return StyleSheet */ protected StyleSheet getStyleSheet() { HTMLDocument doc = (HTMLDocument) getDocument(); return doc.getStyleSheet(); } /** * Returns the view to use for alternate text. This may be null. * @return view for alt text */ private View getAltView() { View view; synchronized (this) { view = altView; } if ((view != null) && (view.getParent() == null)) { view.setParent(getParent()); } return view; } /** * Convenience method for getting an integer attribute from the elements * AttributeSet. * @param name * @param deflt * @return int */ private int getIntAttr(HTML.Attribute name, int deflt) { AttributeSet anAttr = getElement().getAttributes(); if (anAttr.isDefined(name)) { // does not check parents! int i; String val = (String) anAttr.getAttribute(name); if (val == null) { i = deflt; } else { try { i = Math.max(0, Integer.parseInt(val)); } catch (NumberFormatException x) { i = deflt; } } return i; } return deflt; } /** * Returns true if this image within a link? * @return true if it is a link */ private boolean isLink() { return ((state & LINK_FLAG) == LINK_FLAG); } /** * Returns the preferred span of the View used to display the alt text, * or 0 if the view does not exist. * @param axis * @return float */ private float getPreferredSpanFromAltView(int axis) { if (getImage() == null) { View view = getAltView(); if (view != null) { return view.getPreferredSpan(axis); } } return 0.0f; } /** Determines if path is in the form of a URL * @return true if it is a URL */ private boolean isURL() { String src = (String) getElement().getAttributes().getAttribute(HTML.Attribute.SRC); return src.toLowerCase().startsWith("file") || src.toLowerCase().startsWith("http"); } /** * Returns true if the passed in image has a non-zero width and height. * @param anImage * @return true if it has pixels */ private boolean hasPixels(Image anImage) { return (anImage != null) && (anImage.getHeight(imageObserver) > 0) && (anImage.getWidth(imageObserver) > 0); } private static void loadDefaultIconsIfNecessary() { try { if (sPendingImageIcon == null) { sPendingImageIcon = makeIcon(PENDING_IMAGE_SRC); } if (sMissingImageIcon == null) { sMissingImageIcon = makeIcon(MISSING_IMAGE_SRC); } } catch (IOException x) { System.err.println("RelativeImageView: Couldn't load image icons"); } } /** * Loads the image from the URL {@code getImageURL}. This should * only be invoked from {@code refreshImage}. */ private void loadImage() { if (isURL()) { Image newImage = null; URL src = getImageURL(); if (src != null) { Dictionary<?, ?> cache = (Dictionary) getDocument().getProperty(RelativeImageView.IMAGE_CACHE_PROPERTY); if (cache != null) { newImage = (Image) cache.get(src); } else { newImage = Toolkit.getDefaultToolkit().getImage(src); if ((newImage != null) && getLoadsSynchronously()) { // Force the image to be loaded by using an ImageIcon. ImageIcon ii = new ImageIcon(); ii.setImage(newImage); } } } image = newImage; } else { String src = (String) getElement().getAttributes().getAttribute(HTML.Attribute.SRC); src = processSrcPath(src); image = Toolkit.getDefaultToolkit().createImage(src); try { waitForImage(); } catch (final InterruptedException e) { image = null; } } } private static Icon makeIcon(final String gifFile) throws IOException { /* * Copy resource into a byte array. This is * necessary because several browsers consider * Class.getResource a security risk because it * can be used to load additional classes. * Class.getResourceAsStream just returns raw * bytes, which we can convert to an image. */ InputStream resource = ExtendedHTMLEditorKit.getResourceAsStream(gifFile); if (resource == null) { System.err.println(RelativeImageView.class.getName() + "/" + gifFile + " not found."); return null; } byte[] buffer; try ( ByteArrayOutputStream out = new ByteArrayOutputStream(1024); BufferedInputStream in = new BufferedInputStream(resource); ) { buffer = new byte[1024]; int n; while ((n = in.read(buffer)) > 0) { out.write(buffer, 0, n); } in.close(); out.flush(); buffer = out.toByteArray(); if (buffer.length == 0) { System.err.println("warning: " + gifFile + " is zero-length"); return null; } return new ImageIcon(buffer); } } private void paintBorder(Graphics g, Rectangle rect) { Color color = borderColor; if ((borderSize > 0) && (color != null)) { int xOffset = leftInset - borderSize; int yOffset = topInset - borderSize; g.setColor(color); for (int counter = 0; counter < borderSize; counter++) { g.drawRect(rect.x + xOffset + counter, rect.y + yOffset + counter, rect.width - counter - counter - xOffset - xOffset - 1, rect.height - counter - counter - yOffset - yOffset - 1); } } } private void paintHighlights(Graphics g, Shape shape) { if (container instanceof JTextComponent) { JTextComponent tc = (JTextComponent) container; Highlighter h = tc.getHighlighter(); if (h instanceof LayeredHighlighter) { ((LayeredHighlighter) h).paintLayeredHighlights(g, getStartOffset(), getEndOffset(), shape, tc, this); } } } /** Checks to see if the absolute path is availabe thru an application * global static variable or thru a system variable. If so, appends * the relative path to the absolute path and returns the String. * @param src * @return String */ private String processSrcPath(String src) { String val = src; File imageFile = new File(src); if (imageFile.isAbsolute()) { return src; } boolean found = false; Document doc = getDocument(); if (doc != null) { String pv = (String) doc.getProperty("docroot"); if (pv != null) { File f = new File(pv); val = new File(f.getParent(), imageFile.getPath()).toString(); found = true; } } if (!found) { String imagePath = System.getProperty("system.image.path.key"); if (imagePath != null) { val = new File(imagePath, imageFile.getPath()).toString(); } } return val; } /** * Loads the image and updates the size accordingly. This should be * invoked instead of invoking {@code loadImage} or * {@code updateImageSize} directly. */ private void refreshImage() { synchronized (this) { // clear out width/height/realoadimage flag and set loading flag state = (state | LOADING_FLAG | RELOAD_IMAGE_FLAG | WIDTH_FLAG | HEIGHT_FLAG) ^ (WIDTH_FLAG | HEIGHT_FLAG | RELOAD_IMAGE_FLAG); image = null; width = height = 0; } try { // Load the image loadImage(); // And update the size params updateImageSize(); } finally { synchronized (this) { // Clear out state in case someone threw an exception. state = (state | LOADING_FLAG) ^ LOADING_FLAG; } } } /** * Request that this view be repainted. * Assumes the view is still at its last-drawn location. * @param delay */ private void repaint(final long delay) { if ((container != null) && (fBounds != null)) { container.repaint(delay, fBounds.x, fBounds.y, fBounds.width, fBounds.height); } } /** * Invokes {@code preferenceChanged} on the event displatching * thread. */ private void safePreferenceChanged() { if (SwingUtilities.isEventDispatchThread()) { preferenceChanged(null, true, true); } else { SwingUtilities.invokeLater(() -> preferenceChanged(null, true, true)); } } /** * Makes sure the necessary properties and image is loaded. */ private void sync() { int s = state; if ((s & RELOAD_IMAGE_FLAG) != 0) { refreshImage(); } s = state; if ((s & RELOAD_FLAG) != 0) { synchronized (this) { state = (state | RELOAD_FLAG) ^ RELOAD_FLAG; } setPropertiesFromAttributes(); } } /** * Updates the view representing the alt text. */ private void updateAltTextView() { String text = getAltText(); if (text != null) { ImageLabelView newView; newView = new ImageLabelView(getElement(), text); synchronized (this) { altView = newView; } } } /** * Invoked if no image is found, in which case a default border is * used if one isn't specified. */ private void updateBorderForNoImage() { if (borderSize == 0) { borderSize = 1; leftInset += borderSize; rightInset += borderSize; bottomInset += borderSize; topInset += borderSize; } } /** * Recreates and reloads the image. This should * only be invoked from {@code refreshImage}. */ private void updateImageSize() { int newWidth; int newHeight; int newState = 0; Image newImage = getImage(); if (newImage != null) { getElement(); // Get the width/height and set the state ivar before calling // anything that might cause the image to be loaded, and thus the // ImageHandler to be called. newWidth = getIntAttr(HTML.Attribute.WIDTH, -1); if (newWidth > 0) { newState |= WIDTH_FLAG; } newHeight = getIntAttr(HTML.Attribute.HEIGHT, -1); if (newHeight > 0) { newState |= HEIGHT_FLAG; } if (newWidth <= 0) { newWidth = newImage.getWidth(imageObserver); if (newWidth <= 0) { newWidth = DEFAULT_WIDTH; } } if (newHeight <= 0) { newHeight = newImage.getHeight(imageObserver); if (newHeight <= 0) { newHeight = DEFAULT_HEIGHT; } } // Make sure the image starts loading: if ((newState & (WIDTH_FLAG | HEIGHT_FLAG)) != 0) { Toolkit.getDefaultToolkit().prepareImage(newImage, newWidth, newHeight, imageObserver); } else { Toolkit.getDefaultToolkit().prepareImage(newImage, -1, -1, imageObserver); } boolean createText = false; synchronized (this) { // If imageloading failed, other thread may have called // ImageLoader which will null out image, hence we check // for it. if (image != null) { if (((newState & WIDTH_FLAG) == WIDTH_FLAG) || (width == 0)) { width = newWidth; } if (((newState & HEIGHT_FLAG) == HEIGHT_FLAG) || (height == 0)) { height = newHeight; } } else { createText = true; if ((newState & WIDTH_FLAG) == WIDTH_FLAG) { width = newWidth; } if ((newState & HEIGHT_FLAG) == HEIGHT_FLAG) { height = newHeight; } } state |= newState; state = (state | LOADING_FLAG) ^ LOADING_FLAG; } if (createText) { // Only reset if this thread determined image is null updateAltTextView(); } } else { width = height = DEFAULT_HEIGHT; updateBorderForNoImage(); updateAltTextView(); } } /** Added this guy to make sure an image is loaded - ie no broken images. So far its used only for images loaded off the disk (non-URL). It seems to work marvelously. By the way, it does the same thing as MediaTracker, but you dont need to know the component its being rendered on. Rob * @throws InterruptedException*/ private void waitForImage() throws InterruptedException { Thread.sleep(10); int w = image.getWidth(this); int h = image.getHeight(this); while (true) { int flags = Toolkit.getDefaultToolkit().checkImage(image, w, h, this); if (((flags & ERROR) != 0) || ((flags & ABORT) != 0)) { throw new InterruptedException(); } else if ((flags & (ALLBITS | FRAMEBITS)) != 0) { return; } Thread.sleep(10); } } /** * ImageHandler implements the ImageObserver to correctly update the * display as new parts of the image become available. */ private class ImageHandler implements ImageObserver { // This can come on any thread. If we are in the process of reloading // the image and determining our state (loading == true) we don't fire // preference changed, or repaint, we just reset the fWidth/fHeight as // necessary and return. This is ok as we know when loading finishes // it will pick up the new height/width, if necessary. @Override public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { if ((image == null) || (image != img)) { return false; } // Bail out if there was an error: if ((infoflags & (ABORT | ERROR)) != 0) { repaint(0); synchronized (RelativeImageView.this) { if (image == img) { // Be sure image hasn't changed since we don't // initialy synchronize image = null; if ((state & WIDTH_FLAG) != WIDTH_FLAG) { RelativeImageView.this.width = DEFAULT_WIDTH; } if ((state & HEIGHT_FLAG) != HEIGHT_FLAG) { RelativeImageView.this.height = DEFAULT_HEIGHT; } // No image, use a default border. updateBorderForNoImage(); } if ((state & LOADING_FLAG) == LOADING_FLAG) { // No need to resize or repaint, still in the process // of loading. return false; } } updateAltTextView(); safePreferenceChanged(); return false; } // Resize image if necessary: short changed = 0; if (((infoflags & ImageObserver.HEIGHT) != 0) && !getElement().getAttributes().isDefined(HTML.Attribute.HEIGHT)) { changed |= 1; } if (((infoflags & ImageObserver.WIDTH) != 0) && !getElement().getAttributes().isDefined(HTML.Attribute.WIDTH)) { changed |= 2; } synchronized (RelativeImageView.this) { if (image != img) { return false; } if (((changed & 1) == 1) && ((state & WIDTH_FLAG) == 0)) { RelativeImageView.this.width = width; } if (((changed & 2) == 2) && ((state & HEIGHT_FLAG) == 0)) { RelativeImageView.this.height = height; } if ((state & LOADING_FLAG) == LOADING_FLAG) { // No need to resize or repaint, still in the process of // loading. return true; } } if (changed != 0) { // May need to resize myself, asynchronously: Document doc = getDocument(); try { if (doc instanceof AbstractDocument) { ((AbstractDocument) doc).readLock(); } safePreferenceChanged(); } finally { if (doc instanceof AbstractDocument) { ((AbstractDocument) doc).readUnlock(); } } return true; } // Repaint when done or when new pixels arrive: if ((infoflags & (ImageObserver.FRAMEBITS | ImageObserver.ALLBITS)) != 0) { repaint(0); } return (infoflags & ImageObserver.ALLBITS) == 0; } } /** * ImageLabelView is used if the image can't be loaded, and * the attribute specified an alt attribute. It overriden a handle of * methods as the text is hardcoded and does not come from the document. */ private static class ImageLabelView extends InlineView { private Color fg; private Segment segment; ImageLabelView(Element e, String text) { super(e); reset(text); } @Override public int getEndOffset() { return segment.array.length; } @Override public Color getForeground() { View parent; if ((fg == null) && ((parent = getParent()) != null)) { Document doc = getDocument(); AttributeSet anAttr = parent.getAttributes(); if ((anAttr != null) && (doc instanceof StyledDocument)) { fg = ((StyledDocument) doc).getForeground(anAttr); } } return fg; } @Override public int getStartOffset() { return 0; } @Override public Segment getText(int p0, int p1) { if ((p0 < 0) || (p1 > segment.array.length)) { throw new RuntimeException("ImageLabelView: Stale view"); } segment.offset = p0; segment.count = p1 - p0; return segment; } @Override public View breakView(int axis, int offset, float pos, float len) { // Don't allow a break return this; } @Override public void paint(Graphics g, Shape a) { // Don't use supers paint, otherwise selection will be wrong // as our start/end offsets are fake. GlyphPainter painter = getGlyphPainter(); if (painter != null) { g.setColor(getForeground()); painter.paint(this, g, a, getStartOffset(), getEndOffset()); } } /** * Reset the segment * @param text */ public void reset(String text) { segment = new Segment(text.toCharArray(), 0, text.length()); } } }