You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1009 lines
32 KiB
1009 lines
32 KiB
/*
|
|
* Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
|
|
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*/
|
|
package javax.swing.text.html;
|
|
|
|
import java.awt.*;
|
|
import java.awt.image.ImageObserver;
|
|
import java.net.*;
|
|
import java.util.Dictionary;
|
|
import javax.swing.*;
|
|
import javax.swing.text.*;
|
|
import javax.swing.event.*;
|
|
|
|
/**
|
|
* 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</code> 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 IconView
|
|
* @since 1.4
|
|
*/
|
|
public class ImageView extends View {
|
|
/**
|
|
* If true, when some of the bits are available a repaint is done.
|
|
* <p>
|
|
* This is set to false as swing does not offer a repaint that takes a
|
|
* delay. If this were true, a bunch of immediate repaints would get
|
|
* generated that end up significantly delaying the loading of the image
|
|
* (or anything else going on for that matter).
|
|
*/
|
|
private static boolean sIsInc = false;
|
|
/**
|
|
* Repaint delay when some of the bits are available.
|
|
*/
|
|
private static int sIncRate = 100;
|
|
/**
|
|
* Property name for pending image icon
|
|
*/
|
|
private static final String PENDING_IMAGE = "html.pendingImage";
|
|
/**
|
|
* Property name for missing image icon
|
|
*/
|
|
private static final String MISSING_IMAGE = "html.missingImage";
|
|
|
|
/**
|
|
* 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 Image image;
|
|
private Image disabledImage;
|
|
private int width;
|
|
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 Container container;
|
|
private Rectangle fBounds;
|
|
private Color borderColor;
|
|
// 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;
|
|
// Insets, obtained from the painter.
|
|
private short leftInset;
|
|
private short rightInset;
|
|
private short topInset;
|
|
private short bottomInset;
|
|
/**
|
|
* We don't directly implement ImageObserver, instead we use an instance
|
|
* that calls back to us.
|
|
*/
|
|
private ImageObserver imageObserver;
|
|
/**
|
|
* 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;
|
|
|
|
|
|
|
|
/**
|
|
* Creates a new view that represents an IMG element.
|
|
*
|
|
* @param elem the element to create a view for
|
|
*/
|
|
public ImageView(Element elem) {
|
|
super(elem);
|
|
fBounds = new Rectangle();
|
|
imageObserver = new ImageHandler();
|
|
state = RELOAD_FLAG | RELOAD_IMAGE_FLAG;
|
|
}
|
|
|
|
/**
|
|
* 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</code>.
|
|
*/
|
|
public String getAltText() {
|
|
return (String)getElement().getAttributes().getAttribute
|
|
(HTML.Attribute.ALT);
|
|
}
|
|
|
|
/**
|
|
* Return a URL for the image source,
|
|
* or null if it could not be determined.
|
|
*/
|
|
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 if the image couldn't be found.
|
|
*/
|
|
public Icon getNoImageIcon() {
|
|
return (Icon) UIManager.getLookAndFeelDefaults().get(MISSING_IMAGE);
|
|
}
|
|
|
|
/**
|
|
* Returns the icon to use while in the process of loading the image.
|
|
*/
|
|
public Icon getLoadingImageIcon() {
|
|
return (Icon) UIManager.getLookAndFeelDefaults().get(PENDING_IMAGE);
|
|
}
|
|
|
|
/**
|
|
* Returns the image to render.
|
|
*/
|
|
public Image getImage() {
|
|
sync();
|
|
return image;
|
|
}
|
|
|
|
private Image getImage(boolean enabled) {
|
|
Image img = getImage();
|
|
if (! enabled) {
|
|
if (disabledImage == null) {
|
|
disabledImage = GrayFilter.createDisabledImage(img);
|
|
}
|
|
img = disabledImage;
|
|
}
|
|
return img;
|
|
}
|
|
|
|
/**
|
|
* Sets how the image is loaded. If <code>newValue</code> 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.
|
|
*/
|
|
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.
|
|
*/
|
|
public boolean getLoadsSynchronously() {
|
|
return ((state & SYNC_LOAD_FLAG) != 0);
|
|
}
|
|
|
|
/**
|
|
* Convenience method to get the StyleSheet.
|
|
*/
|
|
protected StyleSheet getStyleSheet() {
|
|
HTMLDocument doc = (HTMLDocument) getDocument();
|
|
return doc.getStyleSheet();
|
|
}
|
|
|
|
/**
|
|
* Fetches the attributes to use when rendering. This is
|
|
* implemented to multiplex the attributes specified in the
|
|
* model with a StyleSheet.
|
|
*/
|
|
public AttributeSet getAttributes() {
|
|
sync();
|
|
return attr;
|
|
}
|
|
|
|
/**
|
|
* For images the tooltip text comes from text specified with the
|
|
* <code>ALT</code> attribute. This is overriden to return
|
|
* <code>getAltText</code>.
|
|
*
|
|
* @see JTextComponent#getToolTipText
|
|
*/
|
|
public String getToolTipText(float x, float y, Shape allocation) {
|
|
return getAltText();
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
|
|
leftInset = rightInset = (short)(getIntAttr(HTML.Attribute.HSPACE,
|
|
0) + borderSize);
|
|
topInset = bottomInset = (short)(getIntAttr(HTML.Attribute.VSPACE,
|
|
0) + borderSize);
|
|
|
|
borderColor = ((StyledDocument)getDocument()).getForeground
|
|
(getAttributes());
|
|
|
|
AttributeSet attr = 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 = attr.getAttribute(HTML.Attribute.ALIGN);
|
|
|
|
vAlign = 1.0f;
|
|
if (alignment != null) {
|
|
alignment = alignment.toString();
|
|
if ("top".equals(alignment)) {
|
|
vAlign = 0f;
|
|
}
|
|
else if ("middle".equals(alignment)) {
|
|
vAlign = .5f;
|
|
}
|
|
}
|
|
|
|
AttributeSet anchorAttr = (AttributeSet)attr.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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Establishes the parent view for this view.
|
|
* Seize this moment to cache the AWT Container I'm in.
|
|
*/
|
|
public void setParent(View parent) {
|
|
View oldParent = getParent();
|
|
super.setParent(parent);
|
|
container = (parent != null) ? getContainer() : null;
|
|
if (oldParent != parent) {
|
|
synchronized(this) {
|
|
state |= RELOAD_FLAG;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoked when the Elements attributes have changed. Recreates the image.
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Paints the View.
|
|
*
|
|
* @param g the rendering surface to use
|
|
* @param a the allocated region to render into
|
|
* @see View#paint
|
|
*/
|
|
public void paint(Graphics g, Shape a) {
|
|
sync();
|
|
|
|
Rectangle rect = (a instanceof Rectangle) ? (Rectangle)a :
|
|
a.getBounds();
|
|
Rectangle clip = g.getClipBounds();
|
|
|
|
fBounds.setBounds(rect);
|
|
paintHighlights(g, a);
|
|
paintBorder(g, rect);
|
|
if (clip != null) {
|
|
g.clipRect(rect.x + leftInset, rect.y + topInset,
|
|
rect.width - leftInset - rightInset,
|
|
rect.height - topInset - bottomInset);
|
|
}
|
|
|
|
Container host = getContainer();
|
|
Image img = getImage(host == null || host.isEnabled());
|
|
if (img != null) {
|
|
if (! hasPixels(img)) {
|
|
// No pixels yet, use the default
|
|
Icon icon = getLoadingImageIcon();
|
|
if (icon != null) {
|
|
icon.paintIcon(host, g,
|
|
rect.x + leftInset, rect.y + topInset);
|
|
}
|
|
}
|
|
else {
|
|
// Draw the image
|
|
g.drawImage(img, rect.x + leftInset, rect.y + topInset,
|
|
width, height, imageObserver);
|
|
}
|
|
}
|
|
else {
|
|
Icon icon = getNoImageIcon();
|
|
if (icon != null) {
|
|
icon.paintIcon(host, 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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void paintBorder(Graphics g, Rectangle rect) {
|
|
Color color = borderColor;
|
|
|
|
if ((borderSize > 0 || image == null) && color != null) {
|
|
int xOffset = leftInset - borderSize;
|
|
int yOffset = topInset - borderSize;
|
|
g.setColor(color);
|
|
int n = (image == null) ? 1 : borderSize;
|
|
for (int counter = 0; counter < n; 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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 image = getImage();
|
|
|
|
if (image != 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);
|
|
}
|
|
}
|
|
else {
|
|
View view = getAltView();
|
|
float retValue = 0f;
|
|
|
|
if (view != null) {
|
|
retValue = view.getPreferredSpan(axis);
|
|
}
|
|
switch (axis) {
|
|
case View.X_AXIS:
|
|
return retValue + (float)(width + leftInset + rightInset);
|
|
case View.Y_AXIS:
|
|
return retValue + (float)(height + topInset + bottomInset);
|
|
default:
|
|
throw new IllegalArgumentException("Invalid axis: " + axis);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
public float getAlignment(int axis) {
|
|
switch (axis) {
|
|
case View.Y_AXIS:
|
|
return vAlign;
|
|
default:
|
|
return super.getAlignment(axis);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @return the bounding box of the given position
|
|
* @exception BadLocationException if the given position does not represent a
|
|
* valid location in the associated document
|
|
* @see View#modelToView
|
|
*/
|
|
public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @return the location within the model that best represents the
|
|
* given point of view
|
|
* @see View#viewToModel
|
|
*/
|
|
public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) {
|
|
Rectangle alloc = (Rectangle) a;
|
|
if (x < alloc.x + alloc.width) {
|
|
bias[0] = Position.Bias.Forward;
|
|
return getStartOffset();
|
|
}
|
|
bias[0] = Position.Bias.Backward;
|
|
return getEndOffset();
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
public void setSize(float width, float height) {
|
|
sync();
|
|
|
|
if (getImage() == null) {
|
|
View view = getAltView();
|
|
|
|
if (view != null) {
|
|
view.setSize(Math.max(0f, width - (float)(DEFAULT_WIDTH + leftInset + rightInset)),
|
|
Math.max(0f, height - (float)(topInset + bottomInset)));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if this image within a link?
|
|
*/
|
|
private boolean isLink() {
|
|
return ((state & LINK_FLAG) == LINK_FLAG);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the passed in image has a non-zero width and height.
|
|
*/
|
|
private boolean hasPixels(Image image) {
|
|
return image != null &&
|
|
(image.getHeight(imageObserver) > 0) &&
|
|
(image.getWidth(imageObserver) > 0);
|
|
}
|
|
|
|
/**
|
|
* Returns the preferred span of the View used to display the alt text,
|
|
* or 0 if the view does not exist.
|
|
*/
|
|
private float getPreferredSpanFromAltView(int axis) {
|
|
if (getImage() == null) {
|
|
View view = getAltView();
|
|
|
|
if (view != null) {
|
|
return view.getPreferredSpan(axis);
|
|
}
|
|
}
|
|
return 0f;
|
|
}
|
|
|
|
/**
|
|
* Request that this view be repainted.
|
|
* Assumes the view is still at its last-drawn location.
|
|
*/
|
|
private void repaint(long delay) {
|
|
if (container != null && fBounds != null) {
|
|
container.repaint(delay, fBounds.x, fBounds.y, fBounds.width,
|
|
fBounds.height);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convenience method for getting an integer attribute from the elements
|
|
* AttributeSet.
|
|
*/
|
|
private int getIntAttr(HTML.Attribute name, int deflt) {
|
|
AttributeSet attr = getElement().getAttributes();
|
|
if (attr.isDefined(name)) { // does not check parents!
|
|
int i;
|
|
String val = (String)attr.getAttribute(name);
|
|
if (val == null) {
|
|
i = deflt;
|
|
}
|
|
else {
|
|
try{
|
|
i = Math.max(0, Integer.parseInt(val));
|
|
}catch( NumberFormatException x ) {
|
|
i = deflt;
|
|
}
|
|
}
|
|
return i;
|
|
} else
|
|
return deflt;
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads the image and updates the size accordingly. This should be
|
|
* invoked instead of invoking <code>loadImage</code> or
|
|
* <code>updateImageSize</code> 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads the image from the URL <code>getImageURL</code>. This should
|
|
* only be invoked from <code>refreshImage</code>.
|
|
*/
|
|
private void loadImage() {
|
|
URL src = getImageURL();
|
|
Image newImage = null;
|
|
if (src != null) {
|
|
Dictionary cache = (Dictionary)getDocument().
|
|
getProperty(IMAGE_CACHE_PROPERTY);
|
|
if (cache != null) {
|
|
newImage = (Image)cache.get(src);
|
|
}
|
|
else {
|
|
newImage = Toolkit.getDefaultToolkit().createImage(src);
|
|
if (newImage != null && getLoadsSynchronously()) {
|
|
// Force the image to be loaded by using an ImageIcon.
|
|
ImageIcon ii = new ImageIcon();
|
|
ii.setImage(newImage);
|
|
}
|
|
}
|
|
}
|
|
image = newImage;
|
|
}
|
|
|
|
/**
|
|
* Recreates and reloads the image. This should
|
|
* only be invoked from <code>refreshImage</code>.
|
|
*/
|
|
private void updateImageSize() {
|
|
int newWidth = 0;
|
|
int newHeight = 0;
|
|
int newState = 0;
|
|
Image newImage = getImage();
|
|
|
|
if (newImage != null) {
|
|
Element elem = getElement();
|
|
AttributeSet attr = elem.getAttributes();
|
|
|
|
// 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 = 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;
|
|
updateAltTextView();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the view to use for alternate text. This may be null.
|
|
*/
|
|
private View getAltView() {
|
|
View view;
|
|
|
|
synchronized(this) {
|
|
view = altView;
|
|
}
|
|
if (view != null && view.getParent() == null) {
|
|
view.setParent(getParent());
|
|
}
|
|
return view;
|
|
}
|
|
|
|
/**
|
|
* Invokes <code>preferenceChanged</code> on the event displatching
|
|
* thread.
|
|
*/
|
|
private void safePreferenceChanged() {
|
|
if (SwingUtilities.isEventDispatchThread()) {
|
|
Document doc = getDocument();
|
|
if (doc instanceof AbstractDocument) {
|
|
((AbstractDocument)doc).readLock();
|
|
}
|
|
preferenceChanged(null, true, true);
|
|
if (doc instanceof AbstractDocument) {
|
|
((AbstractDocument)doc).readUnlock();
|
|
}
|
|
}
|
|
else {
|
|
SwingUtilities.invokeLater(new Runnable() {
|
|
public void run() {
|
|
safePreferenceChanged();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
public boolean imageUpdate(Image img, int flags, int x, int y,
|
|
int newWidth, int newHeight ) {
|
|
if (img != image && img != disabledImage ||
|
|
image == null || getParent() == null) {
|
|
|
|
return false;
|
|
}
|
|
|
|
// Bail out if there was an error:
|
|
if ((flags & (ABORT|ERROR)) != 0) {
|
|
repaint(0);
|
|
synchronized(ImageView.this) {
|
|
if (image == img) {
|
|
// Be sure image hasn't changed since we don't
|
|
// initialy synchronize
|
|
image = null;
|
|
if ((state & WIDTH_FLAG) != WIDTH_FLAG) {
|
|
width = DEFAULT_WIDTH;
|
|
}
|
|
if ((state & HEIGHT_FLAG) != HEIGHT_FLAG) {
|
|
height = DEFAULT_HEIGHT;
|
|
}
|
|
} else {
|
|
disabledImage = null;
|
|
}
|
|
if ((state & LOADING_FLAG) == LOADING_FLAG) {
|
|
// No need to resize or repaint, still in the process
|
|
// of loading.
|
|
return false;
|
|
}
|
|
}
|
|
updateAltTextView();
|
|
safePreferenceChanged();
|
|
return false;
|
|
}
|
|
|
|
if (image == img) {
|
|
// Resize image if necessary:
|
|
short changed = 0;
|
|
if ((flags & ImageObserver.HEIGHT) != 0 && !getElement().
|
|
getAttributes().isDefined(HTML.Attribute.HEIGHT)) {
|
|
changed |= 1;
|
|
}
|
|
if ((flags & ImageObserver.WIDTH) != 0 && !getElement().
|
|
getAttributes().isDefined(HTML.Attribute.WIDTH)) {
|
|
changed |= 2;
|
|
}
|
|
|
|
synchronized(ImageView.this) {
|
|
if ((changed & 1) == 1 && (state & HEIGHT_FLAG) == 0) {
|
|
height = newHeight;
|
|
}
|
|
if ((changed & 2) == 2 && (state & WIDTH_FLAG) == 0) {
|
|
width = newWidth;
|
|
}
|
|
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:
|
|
safePreferenceChanged();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Repaint when done or when new pixels arrive:
|
|
if ((flags & (FRAMEBITS|ALLBITS)) != 0) {
|
|
repaint(0);
|
|
}
|
|
else if ((flags & SOMEBITS) != 0 && sIsInc) {
|
|
repaint(sIncRate);
|
|
}
|
|
return ((flags & 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 class ImageLabelView extends InlineView {
|
|
private Segment segment;
|
|
private Color fg;
|
|
|
|
ImageLabelView(Element e, String text) {
|
|
super(e);
|
|
reset(text);
|
|
}
|
|
|
|
public void reset(String text) {
|
|
segment = new Segment(text.toCharArray(), 0, text.length());
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
public int getStartOffset() {
|
|
return 0;
|
|
}
|
|
|
|
public int getEndOffset() {
|
|
return segment.array.length;
|
|
}
|
|
|
|
public View breakView(int axis, int p0, float pos, float len) {
|
|
// Don't allow a break
|
|
return this;
|
|
}
|
|
|
|
public Color getForeground() {
|
|
View parent;
|
|
if (fg == null && (parent = getParent()) != null) {
|
|
Document doc = getDocument();
|
|
AttributeSet attr = parent.getAttributes();
|
|
|
|
if (attr != null && (doc instanceof StyledDocument)) {
|
|
fg = ((StyledDocument)doc).getForeground(attr);
|
|
}
|
|
}
|
|
return fg;
|
|
}
|
|
}
|
|
}
|