// BlogBridge -- RSS feed reader, manager, and web based service
// Copyright (C) 2002-2006 by R. Pito Salas
//
// This program is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software Foundation;
// either version 2 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program;
// if not, write to the Free Software Foundation, Inc., 59 Temple Place,
// Suite 330, Boston, MA 02111-1307 USA
//
// Contact: R. Pito Salas
// mailto:pitosalas@users.sourceforge.net
// More information: about BlogBridge
// http://www.blogbridge.com
// http://sourceforge.net/projects/blogbridge
//
// $Id: CustomImageView.java,v 1.24 2008/04/09 06:07:11 spyromus Exp $
//
package com.salas.bb.utils.uif.html;
import com.jgoodies.uif.util.ResourceUtils;
import com.salas.bb.imageblocker.ImageBlocker;
import com.salas.bb.utils.i18n.Strings;
import com.salas.bb.utils.uif.images.ImageFetcher;
import javax.swing.*;
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.net.MalformedURLException;
import java.net.URL;
import java.net.URLStreamHandler;
import java.text.MessageFormat;
import java.util.Dictionary;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Customized version of image view. We assign tweaked URLStreamHandler to image URL to unblock
* the GUI during image loading.
*
* Author of original class taken as a base from Sun SDK is Scott Violet.
*/
public class CustomImageView extends View
{
private static final Logger LOG = Logger.getLogger(CustomImageView.class.getName());
/**
* 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;
/**
* Repaint delay when some of the bits are available.
*/
private static int sIncRate = 100;
/**
* 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;
/**
* 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 = 0;
private static final int DEFAULT_HEIGHT = 0;
// Show no images for pending / missing pictures
private static final boolean NO_STATE_IMAGES = true;
/**
* Link border to use if other is not specified.
*/
private static final int LINK_BORDER = 0;
// 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 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.
protected short leftInset;
protected short rightInset;
protected short topInset;
protected 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;
private URLStreamHandler handler;
/**
* Creates a new view that represents an IMG element.
*
* @param elem the element to create a view for
*/
public CustomImageView(Element elem)
{
super(elem);
fBounds = new Rectangle();
imageObserver = new ImageHandler();
state = RELOAD_FLAG | RELOAD_IMAGE_FLAG;
handler = new CustomHtmlUrlStreamHandler();
}
/**
* 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>.
*
* @return alternative text.
*/
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.
*
* @return URL of image.
*/
public URL getImageURL()
{
URL imageURL = null;
String src = (String)getElement().getAttributes().getAttribute(HTML.Attribute.SRC);
if (src != null)
{
src = src.replaceAll(" ", "%20");
URL reference = ((HTMLDocument)getDocument()).getBase();
try
{
imageURL = new URL(reference, src, handler);
} catch (MalformedURLException e)
{
imageURL = null;
}
// If image URL is blocked, never return it.
if (ImageBlocker.isBlocked(imageURL)) imageURL = null;
}
return imageURL;
}
/**
* Returns the icon to use if the image couldn't be found.
*
* @return icon of absent image.
*/
public Icon getNoImageIcon()
{
Icon icon = null;
if (!NO_STATE_IMAGES)
{
synchronized (CustomImageView.class)
{
if (sMissingImageIcon == null) sMissingImageIcon = ResourceUtils.getIcon("html.image.missing.icon");
icon = sMissingImageIcon;
}
}
return icon;
}
/**
* Returns the icon to use while in the process of loading the image.
*
* @return icon of loading image.
*/
public Icon getLoadingImageIcon()
{
Icon icon = null;
if (!NO_STATE_IMAGES)
{
synchronized (CustomImageView.class)
{
if (sPendingImageIcon == null) sPendingImageIcon = ResourceUtils.getIcon("html.image.pending.icon");
icon = sPendingImageIcon;
}
}
return icon;
}
/**
* Returns the image to render.
*
* @return image.
*/
public Image getImage()
{
sync();
return image;
}
/**
* 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.
*
* @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 ...
*/
public boolean getLoadsSynchronously()
{
return ((state & SYNC_LOAD_FLAG) != 0);
}
/**
* Convenience method to get the StyleSheet.
* @return 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.
*
* @return atrributes.
*/
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 javax.swing.text.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() ? LINK_BORDER : 0);
if (borderSize == 0 && image == null) borderSize = 1;
rightInset = (short)(getIntAttr(HTML.Attribute.HSPACE, 0) + borderSize);
leftInset = rightInset;
bottomInset = (short)(getIntAttr(HTML.Attribute.VSPACE, 0) + borderSize);
topInset = bottomInset;
borderColor = ((StyledDocument)getDocument()).getForeground(getAttributes());
AttributeSet attrs = 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 = attrs.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)attrs.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.
*
* @param parent parent.
*/
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.
*
* @param e ...
* @param a ...
* @param f ...
*/
public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f)
{
super.changedUpdate(e, a, f);
/*
// This is part of the original implementation and it resets the image every time some
// attribute of the element (?) has changed.
// We don't need this as we don't change the attributes of image IMG tags at run-time.
//
// The negative side of this call is that it produces weird effects in conjunction with
// caching. Empty images with correct dimensions are written because of two consequential
// caching attempts (yes, I can't explain the details).
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 javax.swing.text.View#paint
*/
public void paint(Graphics g, Shape a)
{
sync();
Rectangle rect = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds();
Image anImage = getImage();
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);
}
boolean brokenImage = true;
if (anImage != null)
{
try
{
if (!hasPixels(anImage))
{
// No pixels yet, use the default
Icon icon = getLoadingImageIcon();
if (icon != null)
{
icon.paintIcon(getContainer(), g, rect.x + leftInset, rect.y + topInset);
}
} else
{
// Draw the anImage
g.drawImage(anImage, rect.x + leftInset, rect.y + topInset,
width, height, imageObserver);
}
brokenImage = false;
} catch (RuntimeException e)
{
image = null;
LOG.log(Level.WARNING, Strings.error("img.failed.to.paint.image"), e);
}
}
if (brokenImage)
{
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);
}
}
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 && 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);
}
}
}
/**
* 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 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(MessageFormat.format(
Strings.error("img.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(MessageFormat.format(
Strings.error("img.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
* @param b position bias.
*
* @return the bounding box of the given position
*
* @throws javax.swing.text.BadLocationException if the given position does not represent a
* valid location in the associated document
*
* @see javax.swing.text.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
* @param bias position bias.
*
* @return the location within the model that best represents the
* given point of view
* @see javax.swing.text.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 aWidth the aWidth >= 0
* @param aHeight the aHeight >= 0
*/
public void setSize(float aWidth, float aHeight)
{
sync();
if (getImage() == null)
{
View view = getAltView();
if (view != null)
{
view.setSize(Math.max(0f, aWidth - (float)(DEFAULT_WIDTH + leftInset + rightInset)),
Math.max(0f, aHeight - (float)(topInset + bottomInset)));
}
}
}
/**
* Returns true if this image within a link?
* @return <code>TRUE</code> if image is within a link.
*/
private boolean isLink()
{
return ((state & LINK_FLAG) == LINK_FLAG);
}
/**
* Returns true if the passed in anImage has a non-zero width and height.
* @param anImage image.
* @return <code>TRUE</code> if has non-zero width and height.
*/
private boolean hasPixels(Image anImage)
{
return anImage != null &&
(anImage.getHeight(imageObserver) > 0) &&
(anImage.getWidth(imageObserver) > 0);
}
/**
* Returns the preferred span of the View used to display the alt text,
* or 0 if the view does not exist.
*
* @param axis axis.
*
* @return preferred span.
*/
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.
*
* @param delay maximum delay before repaint.
*/
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. Automatically strips off 'px' suffix of attribute value,
* if present.
*
* @param name attribute name.
* @param deflt default value.
*
* @return value.
*/
protected int getIntAttr(Object name, int deflt)
{
AttributeSet attrs = getElement().getAttributes();
if (attrs.isDefined(name))
{
int i;
String val = attrs.getAttribute(name).toString();
if (val == null)
{
i = deflt;
} else
{
try
{
val = val.endsWith("px") ? val.substring(0, val.length() - 2) : val;
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 = 0;
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
{
if (System.getProperty(Strings.error("img.no.images")) == null)
{
newImage = ImageFetcher.load(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;
int newHeight;
int newState = 0;
Image newImage = getImage();
if (newImage != null)
{
// 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 = DEFAULT_HEIGHT;
height = DEFAULT_HEIGHT;
updateBorderForNoImage();
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.
* @return alternative view.
*/
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();
try
{
if (doc instanceof AbstractDocument)
{
((AbstractDocument)doc).readLock();
}
preferenceChanged(null, true, true);
} finally
{
if (doc instanceof AbstractDocument)
{
((AbstractDocument)doc).readUnlock();
}
}
} else
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
safePreferenceChanged();
}
});
}
}
/**
* 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;
}
}
/**
* 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 (image == null || image != img)
{
return false;
}
// Bail out if there was an error:
if ((flags & (ABORT | ERROR)) != 0)
{
repaint(0);
synchronized (CustomImageView.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;
// 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 ((flags & ImageObserver.HEIGHT) != 0 &&
!getElement().getAttributes().isDefined(HTML.Attribute.HEIGHT))
{
changed |= ImageObserver.HEIGHT;
}
if ((flags & ImageObserver.WIDTH) != 0 &&
!getElement().getAttributes().isDefined(HTML.Attribute.WIDTH))
{
changed |= ImageObserver.WIDTH;
}
synchronized (CustomImageView.this)
{
if (image != img) return false;
if ((changed & ImageObserver.WIDTH) != 0 &&
(state & WIDTH_FLAG) == 0)
{
width = newWidth;
}
if ((changed & ImageObserver.HEIGHT) != 0 &&
(state & HEIGHT_FLAG) == 0)
{
height = newHeight;
}
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)
{
// Make a static copy of image
image = prepareStaticCopy(image);
repaint(0);
} else if ((flags & SOMEBITS) != 0 && sIsInc)
{
repaint(sIncRate);
}
return (flags & (ALLBITS | FRAMEBITS)) == 0;
}
/**
* Prepares static copy of image to avoid continuous loading requests on repaint().
*
* @param anImage source image.
*
* @return copy or original image.
*/
private Image prepareStaticCopy(Image anImage)
{
Image result = anImage;
int width = anImage.getWidth(null);
int height = anImage.getHeight(null);
if (width != -1 && height != -1)
{
/*
BufferedImage copy = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics g = copy.createGraphics();
g.drawImage(anImage, 0, 0, null);
result = copy;
*/
}
return result;
}
}
/**
* 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 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(Strings.error("img.imageview.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 attrs = parent.getAttributes();
if (attrs != null && (doc instanceof StyledDocument))
{
fg = ((StyledDocument)doc).getForeground(attrs);
}
}
return fg;
}
}
}