package javax.swing.text.html;
import gnu.javax.swing.text.html.ImageViewIconFactory;
import gnu.javax.swing.text.html.css.Length;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.image.ImageObserver;
import java.net.MalformedURLException;
import java.net.URL;
import javax.swing.Icon;
import javax.swing.SwingUtilities;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.View;
import javax.swing.text.Position.Bias;
import javax.swing.text.html.HTML.Attribute;
/**
* A view, representing a single image, represented by the HTML IMG tag.
*
* @author Audrius Meskauskas (AudriusA@Bioinformatics.org)
*/
public class ImageView extends View
{
/**
* Tracks image loading state and performs the necessary layout updates.
*/
class Observer
implements ImageObserver
{
public boolean imageUpdate(Image image, int flags, int x, int y, int width, int height)
{
boolean widthChanged = false;
if ((flags & ImageObserver.WIDTH) != 0 && spans[X_AXIS] == null)
widthChanged = true;
boolean heightChanged = false;
if ((flags & ImageObserver.HEIGHT) != 0 && spans[Y_AXIS] == null)
heightChanged = true;
if (widthChanged || heightChanged)
safePreferenceChanged(ImageView.this, widthChanged, heightChanged);
boolean ret = (flags & ALLBITS) != 0;
return ret;
}
}
/**
* True if the image loads synchronuosly (on demand). By default, the image
* loads asynchronuosly.
*/
boolean loadOnDemand;
/**
* The image icon, wrapping the image,
*/
Image image;
/**
* The image state.
*/
byte imageState = MediaTracker.LOADING;
/**
* True when the image needs re-loading, false otherwise.
*/
private boolean reloadImage;
/**
* True when the image properties need re-loading, false otherwise.
*/
private boolean reloadProperties;
/**
* True when the width is set as CSS/HTML attribute.
*/
private boolean haveWidth;
/**
* True when the height is set as CSS/HTML attribute.
*/
private boolean haveHeight;
/**
* True when the image is currently loading.
*/
private boolean loading;
/**
* The current width of the image.
*/
private int width;
/**
* The current height of the image.
*/
private int height;
/**
* Our ImageObserver for tracking the loading state.
*/
private ImageObserver observer;
/**
* The CSS width and height.
*
* Package private to avoid synthetic accessor methods.
*/
Length[] spans;
/**
* The cached attributes.
*/
private AttributeSet attributes;
/**
* Creates the image view that represents the given element.
*
* @param element the element, represented by this image view.
*/
public ImageView(Element element)
{
super(element);
spans = new Length[2];
observer = new Observer();
reloadProperties = true;
reloadImage = true;
loadOnDemand = false;
}
/**
* Load or reload the image. This method initiates the image reloading. After
* the image is ready, the repaint event will be scheduled. The current image,
* if it already exists, will be discarded.
*/
private void reloadImage()
{
loading = true;
reloadImage = false;
haveWidth = false;
haveHeight = false;
image = null;
width = 0;
height = 0;
try
{
loadImage();
updateSize();
}
finally
{
loading = false;
}
}
/**
* Get the image alignment. This method works handling standart alignment
* attributes in the HTML IMG tag (align = top bottom middle left right).
* Depending from the parameter, either horizontal or vertical alingment
* information is returned.
*
* @param axis -
* either X_AXIS or Y_AXIS
*/
public float getAlignment(int axis)
{
AttributeSet attrs = getAttributes();
Object al = attrs.getAttribute(Attribute.ALIGN);
// Default is top left aligned.
if (al == null)
return 0.0f;
String align = al.toString();
if (axis == View.X_AXIS)
{
if (align.equals("middle"))
return 0.5f;
else if (align.equals("left"))
return 0.0f;
else if (align.equals("right"))
return 1.0f;
else
return 0.0f;
}
else if (axis == View.Y_AXIS)
{
if (align.equals("middle"))
return 0.5f;
else if (align.equals("top"))
return 0.0f;
else if (align.equals("bottom"))
return 1.0f;
else
return 0.0f;
}
else
throw new IllegalArgumentException("axis " + axis);
}
/**
* Get the text that should be shown as the image replacement and also as the
* image tool tip text. The method returns the value of the attribute, having
* the name {@link Attribute#ALT}. If there is no such attribute, the image
* name from the url is returned. If the URL is not available, the empty
* string is returned.
*/
public String getAltText()
{
Object rt = getAttributes().getAttribute(Attribute.ALT);
if (rt != null)
return rt.toString();
else
{
URL u = getImageURL();
if (u == null)
return "";
else
return u.getFile();
}
}
/**
* Returns the combination of the document and the style sheet attributes.
*/
public AttributeSet getAttributes()
{
if (attributes == null)
attributes = getStyleSheet().getViewAttributes(this);
return attributes;
}
/**
* Get the image to render. May return null if the image is not yet loaded.
*/
public Image getImage()
{
updateState();
return image;
}
/**
* Get the URL location of the image to render. If this method returns null,
* the "no image" icon is rendered instead. By defaul, url must be present as
* the "src" property of the IMG tag. If it is missing, null is returned and
* the "no image" icon is rendered.
*
* @return the URL location of the image to render.
*/
public URL getImageURL()
{
Element el = getElement();
String src = (String) el.getAttributes().getAttribute(Attribute.SRC);
URL url = null;
if (src != null)
{
URL base = ((HTMLDocument) getDocument()).getBase();
try
{
url = new URL(base, src);
}
catch (MalformedURLException ex)
{
// Return null.
}
}
return url;
}
/**
* Get the icon that should be displayed while the image is loading and hence
* not yet available.
*
* @return an icon, showing a non broken sheet of paper with image.
*/
public Icon getLoadingImageIcon()
{
return ImageViewIconFactory.getLoadingImageIcon();
}
/**
* Get the image loading strategy.
*
* @return false (default) if the image is loaded when the view is
* constructed, true if the image is only loaded on demand when
* rendering.
*/
public boolean getLoadsSynchronously()
{
return loadOnDemand;
}
/**
* Get the icon that should be displayed when the image is not available.
*
* @return an icon, showing a broken sheet of paper with image.
*/
public Icon getNoImageIcon()
{
return ImageViewIconFactory.getNoImageIcon();
}
/**
* Get the preferred span of the image along the axis. The image size is first
* requested to the attributes {@link Attribute#WIDTH} and
* {@link Attribute#HEIGHT}. If they are missing, and the image is already
* loaded, the image size is returned. If there are no attributes, and the
* image is not loaded, zero is returned.
*
* @param axis -
* either X_AXIS or Y_AXIS
* @return either width of height of the image, depending on the axis.
*/
public float getPreferredSpan(int axis)
{
Image image = getImage();
if (axis == View.X_AXIS)
{
if (spans[axis] != null)
return spans[axis].getValue();
else if (image != null)
return image.getWidth(getContainer());
else
return getNoImageIcon().getIconWidth();
}
else if (axis == View.Y_AXIS)
{
if (spans[axis] != null)
return spans[axis].getValue();
else if (image != null)
return image.getHeight(getContainer());
else
return getNoImageIcon().getIconHeight();
}
else
throw new IllegalArgumentException("axis " + axis);
}
/**
* Get the associated style sheet from the document.
*
* @return the associated style sheet.
*/
protected StyleSheet getStyleSheet()
{
HTMLDocument doc = (HTMLDocument) getDocument();
return doc.getStyleSheet();
}
/**
* Get the tool tip text. This is overridden to return the value of the
* {@link #getAltText()}. The parameters are ignored.
*
* @return that is returned by getAltText().
*/
public String getToolTipText(float x, float y, Shape shape)
{
return getAltText();
}
/**
* Paints the image or one of the two image state icons. The image is resized
* to the shape bounds. If there is no image available, the alternative text
* is displayed besides the image state icon.
*
* @param g
* the Graphics, used for painting.
* @param bounds
* the bounds of the region where the image or replacing icon must be
* painted.
*/
public void paint(Graphics g, Shape bounds)
{
updateState();
Rectangle r = bounds instanceof Rectangle ? (Rectangle) bounds
: bounds.getBounds();
Image image = getImage();
if (image != null)
{
g.drawImage(image, r.x, r.y, r.width, r.height, observer);
}
else
{
Icon icon = getNoImageIcon();
if (icon != null)
icon.paintIcon(getContainer(), g, r.x, r.y);
}
}
/**
* Set if the image should be loaded only when needed (synchronuosly). By
* default, the image loads asynchronuosly. If the image is not yet ready, the
* icon, returned by the {@link #getLoadingImageIcon()}, is displayed.
*/
public void setLoadsSynchronously(boolean load_on_demand)
{
loadOnDemand = load_on_demand;
}
/**
* Update all cached properties from the attribute set, returned by the
* {@link #getAttributes}.
*/
protected void setPropertiesFromAttributes()
{
AttributeSet atts = getAttributes();
StyleSheet ss = getStyleSheet();
float emBase = ss.getEMBase(atts);
float exBase = ss.getEXBase(atts);
spans[X_AXIS] = (Length) atts.getAttribute(CSS.Attribute.WIDTH);
if (spans[X_AXIS] != null)
{
spans[X_AXIS].setFontBases(emBase, exBase);
}
spans[Y_AXIS] = (Length) atts.getAttribute(CSS.Attribute.HEIGHT);
if (spans[Y_AXIS] != null)
{
spans[Y_AXIS].setFontBases(emBase, exBase);
}
}
/**
* Maps the picture co-ordinates into the image position in the model. As the
* image is not divideable, this is currently implemented always to return the
* start offset.
*/
public int viewToModel(float x, float y, Shape shape, Bias[] bias)
{
return getStartOffset();
}
/**
* This is currently implemented always to return the area of the image view,
* as the image is not divideable by character positions.
*
* @param pos character position
* @param area of the image view
* @param bias bias
*
* @return the shape, where the given character position should be mapped.
*/
public Shape modelToView(int pos, Shape area, Bias bias)
throws BadLocationException
{
return area;
}
/**
* Starts loading the image asynchronuosly. If the image must be loaded
* synchronuosly instead, the {@link #setLoadsSynchronously} must be
* called before calling this method. The passed parameters are not used.
*/
public void setSize(float width, float height)
{
updateState();
// TODO: Implement this when we have an alt view for the alt=... attribute.
}
/**
* This makes sure that the image and properties have been loaded.
*/
private void updateState()
{
if (reloadImage)
reloadImage();
if (reloadProperties)
setPropertiesFromAttributes();
}
/**
* Actually loads the image.
*/
private void loadImage()
{
URL src = getImageURL();
Image newImage = null;
if (src != null)
{
// Call getImage(URL) to allow the toolkit caching of that image URL.
Toolkit tk = Toolkit.getDefaultToolkit();
newImage = tk.getImage(src);
tk.prepareImage(newImage, -1, -1, observer);
if (newImage != null && getLoadsSynchronously())
{
// Load image synchronously.
MediaTracker tracker = new MediaTracker(getContainer());
tracker.addImage(newImage, 0);
try
{
tracker.waitForID(0);
}
catch (InterruptedException ex)
{
Thread.interrupted();
}
}
}
image = newImage;
}
/**
* Updates the size parameters of the image.
*/
private void updateSize()
{
int newW = 0;
int newH = 0;
Image newIm = getImage();
if (newIm != null)
{
// Fetch width.
Length l = spans[X_AXIS];
if (l != null)
{
newW = (int) l.getValue();
haveWidth = true;
}
else
{
newW = newIm.getWidth(observer);
}
// Fetch height.
l = spans[Y_AXIS];
if (l != null)
{
newH = (int) l.getValue();
haveHeight = true;
}
else
{
newW = newIm.getWidth(observer);
}
// Go and trigger loading.
Toolkit tk = Toolkit.getDefaultToolkit();
if (haveWidth || haveHeight)
tk.prepareImage(newIm, width, height, observer);
else
tk.prepareImage(newIm, -1, -1, observer);
}
}
/**
* Calls preferenceChanged from the event dispatch thread and within
* a read lock to protect us from threading issues.
*
* @param v the view
* @param width true when the width changed
* @param height true when the height changed
*/
void safePreferenceChanged(final View v, final boolean width,
final boolean height)
{
if (SwingUtilities.isEventDispatchThread())
{
Document doc = getDocument();
if (doc instanceof AbstractDocument)
((AbstractDocument) doc).readLock();
try
{
preferenceChanged(v, width, height);
}
finally
{
if (doc instanceof AbstractDocument)
((AbstractDocument) doc).readUnlock();
}
}
else
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
safePreferenceChanged(v, width, height);
}
});
}
}
}