/*
* @(#)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());
}
}
}