/*
* @(#)ImageView.java 1.58 05/11/30
*
* Copyright 2006 Sun Microsystems, Inc. All rights reserved.
* SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package javax.swing.text.html;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.ImageObserver;
import java.io.*;
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
* @version 1.58 11/30/05
* @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 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;
}
/**
* 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();
Image image = 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);
}
if (image != null) {
if (!hasPixels(image)) {
// No pixels yet, use the default
Icon icon = (image == null) ? getNoImageIcon() :
getLoadingImageIcon();
if (icon != null) {
icon.paintIcon(getContainer(), g, rect.x + leftInset,
rect.y + topInset);
}
}
else {
// Draw the image
g.drawImage(image, rect.x + leftInset, rect.y + topInset,
width, height, imageObserver);
}
}
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);
}
}
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 (image == null || image != img || 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;
}
}
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 |= 1;
}
if ((flags & ImageObserver.WIDTH) != 0 && !getElement().
getAttributes().isDefined(HTML.Attribute.WIDTH)) {
changed |= 2;
}
synchronized(ImageView.this) {
if (image != img) {
return false;
}
if ((changed & 1) == 1 && (state & WIDTH_FLAG) == 0) {
width = newWidth;
}
if ((changed & 2) == 2 && (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) {
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;
}
}
}