/*
GNU Lesser General Public License

RelativeImageView
Copyright (C) 2001  Frits Jalvingh & Howard Kistler

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library 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
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

package com.hexidec.ekit.component;

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.image.ImageObserver;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Dictionary;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JEditorPane;
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.JTextComponent;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.Position;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyledDocument;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.StyleSheet;
import javax.swing.event.DocumentEvent;

/**
  * @author <a href="mailto:jal@grimor.com">Frits Jalvingh</a>
  * @version 1.0
  *
  * This code was modeled after an artice on
  * <a href="http://www.javaworld.com/javaworld/javatips/jw-javatip109.html">
  * JavaWorld</a> by Bob Kenworthy.
  */

public class RelativeImageView extends View implements ImageObserver, MouseListener, MouseMotionListener
{
	public static final String TOP       = "top";
	public static final String TEXTTOP   = "texttop";
	public static final String MIDDLE    = "middle";
	public static final String ABSMIDDLE = "absmiddle";
	public static final String CENTER    = "center";
	public static final String BOTTOM    = "bottom";
	public static final String IMAGE_CACHE_PROPERTY = "imageCache";

	private static Icon sPendingImageIcon;
	private static Icon sMissingImageIcon;
	private static final String PENDING_IMAGE_SRC = "icons/ImagePendingHK.gif";
	private static final String MISSING_IMAGE_SRC = "icons/ImageMissingHK.gif";
	private static final int DEFAULT_WIDTH  = 32;
	private static final int DEFAULT_HEIGHT = 32;
	private static final int DEFAULT_BORDER = 1;

	private AttributeSet attr;
	private Element      fElement;
	private Image        fImage;
	private int          fHeight;
	private int          fWidth;
	private Container    fContainer;
	private Rectangle    fBounds;
	private Component    fComponent;
	private Point        fGrowBase; // base of drag while growing image
	private boolean      fGrowProportionally; // should grow be proportional?
	private String BaseUrl;
	private boolean      bLoading; // set to true while the receiver is locked, to indicate the reciever is loading the image. This is used in imageUpdate.

	/** Constructor
	  * Creates a new view that represents an IMG element.
	  * @param elem the element to create a view for
	  */
	public RelativeImageView(Element elem,String baseurl)
	{
		super(elem);
		BaseUrl = baseurl;
		initialize(elem);
		StyleSheet sheet = getStyleSheet();
		attr = sheet.getViewAttributes(this);
	}

	private void initialize(Element elem)
	{
		synchronized(this)
		{
			bLoading = true;
			fWidth  = 0;
			fHeight = 0;
		}
		int width = 0;
		int height = 0;
		boolean customWidth = false;
		boolean customHeight = false;
		try
		{
			fElement = elem;
			// request image from document's cache
			AttributeSet attr = elem.getAttributes();
			if(true || isURL())
			{
				URL src = getSourceURL();
				if(src != null)
				{
					Dictionary cache = (Dictionary)getDocument().getProperty(IMAGE_CACHE_PROPERTY);
					if(cache != null)
					{
						fImage = (Image)cache.get(src);
					}
					else
					{
						fImage = Toolkit.getDefaultToolkit().getImage(src);
					}
				}
			}
			else
			{
				// load image from relative path
				String src = (String)fElement.getAttributes().getAttribute(HTML.Attribute.SRC);
				src = processSrcPath(src);
				fImage = Toolkit.getDefaultToolkit().createImage(src);
				try
				{
					waitForImage();
				}
				catch(InterruptedException ie)
				{
					fImage = null;
					// possibly replace with the ImageBroken icon, if that's what is happening
				}
			}

			// get height & width from params or image or defaults
			height = getIntAttr(HTML.Attribute.HEIGHT, -1);
			customHeight = (height > 0);
			if(!customHeight && fImage != null)
			{
				height = fImage.getHeight(this);
			}
			if(height <= 0)
			{
				height = DEFAULT_HEIGHT;
			}

			width = getIntAttr(HTML.Attribute.WIDTH, -1);
			customWidth = (width > 0);
			if(!customWidth && fImage != null)
			{
				width = fImage.getWidth(this);
			}
			if(width <= 0)
			{
				width = DEFAULT_WIDTH;
			}

			if(fImage != null)
			{
				if(customHeight && customWidth)
				{
					Toolkit.getDefaultToolkit().prepareImage(fImage, height, width, this);
				}
				else
				{
					Toolkit.getDefaultToolkit().prepareImage(fImage, -1, -1, this);
				}
			}
		}
		finally
		{
			synchronized(this)
			{
				bLoading = false;
				if(customHeight || fHeight == 0)
				{
					fHeight = height;
				}
				if(customWidth || fWidth == 0)
				{
					fWidth = width;
				}
			}
		}
	}

	/** Determines if path is in the form of a URL
	  */
	private boolean isURL()
	{
		String src = (String)fElement.getAttributes().getAttribute(HTML.Attribute.SRC);
		return src.toLowerCase().startsWith("file") || src.toLowerCase().startsWith("http");
	}

	/** Checks to see if the absolute path is availabe thru an application
	  * global static variable or thru a system variable. If so, appends
	  * the relative path to the absolute path and returns the String.
	  */
	private String processSrcPath(String src)
	{
		String val = src;
		File imageFile = new File(src);
		if(imageFile.isAbsolute())
		{
			return src;
		}
		boolean	found = false;
		Document doc = getDocument();
		if(doc != null)
		{
			String pv = (String)doc.getProperty("com.hexidec.ekit.docsource");
			if(pv != null)
			{
				File f = new File(pv);
				val	= (new File(f.getParent(), imageFile.getPath().toString())).toString();
				found = true;
			}
		}
		if(!found)
		{
			String imagePath = System.getProperty("system.image.path.key");
			if(imagePath != null)
			{
				val = (new File(imagePath, imageFile.getPath())).toString();
			}
		}
		return val;
	}

	/** Method insures that the image is loaded and not a broken reference
	  */
	private void waitForImage()
	throws InterruptedException
	{
		int w = fImage.getWidth(this);
		int h = fImage.getHeight(this);
		while (true)
		{
			int flags = Toolkit.getDefaultToolkit().checkImage(fImage, w, h, this);
			if(((flags & ERROR) != 0) || ((flags & ABORT) != 0 ))
			{
				throw new InterruptedException();
			}
			else if((flags & (ALLBITS | FRAMEBITS)) != 0)
			{
				return;
			}
			Thread.sleep(10);
		}
	}

	/** Fetches the attributes to use when rendering. This is
	  * implemented to multiplex the attributes specified in the
	  * model with a StyleSheet.
	  */
	public AttributeSet getAttributes()
	{
		return attr;
	}

	/** Method tests whether the image within a link
	  */
	boolean isLink()
	{
		AttributeSet anchorAttr = (AttributeSet)fElement.getAttributes().getAttribute(HTML.Tag.A);
		if(anchorAttr != null)
		{
			return anchorAttr.isDefined(HTML.Attribute.HREF);
		}
		return false;
	}

	/** Method returns the size of the border to use
	  */
	int getBorder()
	{
		return getIntAttr(HTML.Attribute.BORDER, isLink() ? DEFAULT_BORDER : 0);
	}

	/** Method returns the amount of extra space to add along an axis
		*/
	int getSpace(int axis)
	{
		return getIntAttr((axis == X_AXIS) ? HTML.Attribute.HSPACE : HTML.Attribute.VSPACE, 0);
	}

	/** Method returns the border's color, or null if this is not a link
	*/
	Color getBorderColor()
	{
		StyledDocument doc = (StyledDocument)getDocument();
		return doc.getForeground(getAttributes());
	}

	/** Method returns the image's vertical alignment
	  */
	float getVerticalAlignment()
	{
		String align = (String)fElement.getAttributes().getAttribute(HTML.Attribute.ALIGN);
		if(align != null)
		{
			align = align.toLowerCase();
			if(align.equals(TOP) || align.equals(TEXTTOP))
			{
				return 0.0f;
			}
			else if(align.equals(this.CENTER) || align.equals(MIDDLE) || align.equals(ABSMIDDLE))
			{
				return 0.5f;
			}
		}
		return 1.0f; // default alignment is bottom
	}

	boolean hasPixels(ImageObserver obs)
	{
		return ((fImage != null) && (fImage.getHeight(obs) > 0) && (fImage.getWidth(obs) > 0));
	}

	/** Method returns a URL for the image source, or null if it could not be determined
	  */
	private URL getSourceURL()
	{
		String src = (String)fElement.getAttributes().getAttribute(HTML.Attribute.SRC);
		if(src == null)
		{
			return null;
		}
		try
		{
			URL reference = new URL(BaseUrl);
			URL u = new URL(reference,src);
			return u;
		}
		catch(MalformedURLException mue)
		{
			return null;
		}
	}

	/** Method looks up an integer-valued attribute (not recursive!)
	  */
	private int getIntAttr(HTML.Attribute name, int iDefault)
	{
		AttributeSet attr = fElement.getAttributes();
		if(attr.isDefined(name))
		{
			int i;
			String val = (String)attr.getAttribute(name);
			if(val == null)
			{
				i = iDefault;
			}
			else
			{
				try
				{
					i = Math.max(0, Integer.parseInt(val));
				}
				catch(NumberFormatException nfe)
				{
					i = iDefault;
				}
			}
			return i;
		}
		else
		{
			return iDefault;
		}
	}

	/**
	* Establishes the parent view for this view.
	* Seize this moment to cache the AWT Container I'm in.
	*/
	public void setParent(View parent)
	{
		super.setParent(parent);
		fContainer = ((parent != null) ? getContainer() : null);
		if((parent == null) && (fComponent != null))
		{
			fComponent.getParent().remove(fComponent);
			fComponent = null;
		}
	}

	/** My attributes may have changed. */
	public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f)
	{
		super.changedUpdate(e, a, f);
		float align = getVerticalAlignment();

		int height = fHeight;
		int width  = fWidth;

		initialize(getElement());

		boolean hChanged = fHeight != height;
		boolean wChanged = fWidth != width;
		if(hChanged || wChanged || getVerticalAlignment() != align)
		{
			getParent().preferenceChanged(this, hChanged, wChanged);
		}
	}


	/**
	  * Paints the image.
	  *
	  * @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)
	{
		Color oldColor = g.getColor();
		fBounds = a.getBounds();
		int border = getBorder();
		int x = fBounds.x + border + getSpace(X_AXIS);
		int y = fBounds.y + border + getSpace(Y_AXIS);
		int width = fWidth;
		int height = fHeight;
		int sel = getSelectionState();

		// If no pixels yet, draw gray outline and icon
		if(!hasPixels(this))
		{
			g.setColor(Color.lightGray);
			g.drawRect(x, y, width - 1, height - 1);
			g.setColor(oldColor);
			loadImageStatusIcons();
			Icon icon = ((fImage == null) ? sMissingImageIcon : sPendingImageIcon);
			if(icon != null)
			{
				icon.paintIcon(getContainer(), g, x, y);
			}
		}

		// Draw image
		if(fImage != null)
		{
			g.drawImage(fImage, x, y, width, height, this);
		}

		// If selected exactly, we need a black border & grow-box
		Color bc = getBorderColor();
		if(sel == 2)
		{
			// Make sure there's room for a border
			int delta = 2 - border;
			if(delta > 0)
			{
				x += delta;
				y += delta;
				width -= delta << 1;
				height -= delta << 1;
				border = 2;
			}
			bc = null;
			g.setColor(Color.black);
			// Draw grow box
			g.fillRect(x + width - 5, y + height - 5, 5, 5);
		}

		// Draw border
		if(border > 0)
		{
			if(bc != null)
			{
				g.setColor(bc);
			}
			// Draw a thick rectangle:
			for(int i = 1; i <= border; i++)
			{
				g.drawRect(x - i, y - i, width - 1 + i + i, height - 1 + i + i);
			}
			g.setColor(oldColor);
		}
	}

	/** Request that this view be repainted. Assumes the view is still at its last-drawn location.
	  */
	protected void repaint(long delay)
	{
		if((fContainer != null) && (fBounds != null))
		{
			fContainer.repaint(delay, fBounds.x, fBounds.y, fBounds.width, fBounds.height);
		}
	}

	/**
	  * Determines whether the image is selected, and if it's the only thing selected.
	  * @return  0 if not selected, 1 if selected, 2 if exclusively selected.
	  * "Exclusive" selection is only returned when editable.
	  */
	protected int getSelectionState()
	{
		int p0 = fElement.getStartOffset();
		int p1 = fElement.getEndOffset();
		if(fContainer instanceof JTextComponent)
		{
			JTextComponent textComp = (JTextComponent)fContainer;
			int start = textComp.getSelectionStart();
			int end = textComp.getSelectionEnd();
			if((start <= p0) && (end >= p1))
			{
				if((start == p0) && (end == p1) && isEditable())
				{
					return 2;
				}
				else
				{
					return 1;
				}
			}
		}
		return 0;
	}

	protected boolean isEditable()
	{
		return ((fContainer instanceof JEditorPane) && ((JEditorPane)fContainer).isEditable());
	}

	/** Returns the text editor's highlight color.
	  */
	protected Color getHighlightColor()
	{
		JTextComponent textComp = (JTextComponent)fContainer;
		return textComp.getSelectionColor();
	}

	// Progressive display -------------------------------------------------

	// This can come on any thread. If we are in the process of reloading
	// the image and determining our state (loading == true) we don't fire
	// preference changed, or repaint, we just reset the fWidth/fHeight as
	// necessary and return. This is ok as we know when loading finishes
	// it will pick up the new height/width, if necessary.

	private static boolean sIsInc = true;
	private static int sIncRate = 100;

	public boolean imageUpdate(Image img, int flags, int x, int y, int width, int height)
	{
		if((fImage == null) || (fImage != img))
		{
			return false;
		}

		// Bail out if there was an error
		if((flags & (ABORT|ERROR)) != 0)
		{
			fImage = null;
			repaint(0);
			return false;
		}

		// Resize image if necessary
		short changed = 0;
		if((flags & ImageObserver.HEIGHT) != 0)
		{
			if(!getElement().getAttributes().isDefined(HTML.Attribute.HEIGHT))
			{
				changed |= 1;
			}
		}
		if((flags & ImageObserver.WIDTH) != 0)
		{
			if(!getElement().getAttributes().isDefined(HTML.Attribute.WIDTH))
			{
				changed |= 2;
			}
		}

		synchronized(this)
		{
			if((changed & 1) == 1)
			{
				fWidth = width;
			}
			if((changed & 2) == 2)
			{
				fHeight = height;
			}
			if(bLoading)
			{
				// No need to resize or repaint, still in the process of loading
				return true;
			}
		}

		if(changed != 0)
		{
			// May need to resize myself, asynchronously
			Document doc = getDocument();
			try
			{
				if(doc instanceof AbstractDocument)
				{
					((AbstractDocument)doc).readLock();
				}
				preferenceChanged(this, true, true);
			}
			finally
			{
				if(doc instanceof AbstractDocument)
				{
					((AbstractDocument)doc).readUnlock();
				}
			}
			return true;
		}

		// Repaint when done or when new pixels arrive
		if((flags & (FRAMEBITS|ALLBITS)) != 0)
		{
			repaint(0);
		}
		else if((flags & SOMEBITS) != 0)
		{
			if(sIsInc)
			{
				repaint(sIncRate);
			}
		}
		return ((flags & ALLBITS) == 0);
	}

	// Layout --------------------------------------------------------------

	/** Determines the preferred span for this view along an axis.
	  *
	  * @param axis may be either X_AXIS or Y_AXIS
	  * @returns  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)
	{
		int extra = 2 * (getBorder() + getSpace(axis));
		switch(axis)
		{
			case View.X_AXIS:
				return fWidth+extra;
			case View.Y_AXIS:
				return fHeight+extra;
			default:
				throw new IllegalArgumentException("Invalid axis in getPreferredSpan() : " + 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
	  * @returns 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 getVerticalAlignment();
			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();
	}

	/** Change the size of this image. This alters the HEIGHT and WIDTH
	  * attributes of the Element and causes a re-layout.
	  */
	protected void resize(int width, int height)
	{
		if((width == fWidth) && (height == fHeight))
		{
			return;
		}
		fWidth = width;
		fHeight= height;
		// Replace attributes in document
		MutableAttributeSet attr = new SimpleAttributeSet();
		attr.addAttribute(HTML.Attribute.WIDTH ,Integer.toString(width));
		attr.addAttribute(HTML.Attribute.HEIGHT,Integer.toString(height));
		((StyledDocument)getDocument()).setCharacterAttributes(fElement.getStartOffset(), fElement.getEndOffset(), attr, false);
	}

	// Mouse event handling ------------------------------------------------

	/** Select or grow image when clicked.
	  */
	public void mousePressed(MouseEvent e)
	{
		Dimension size = fComponent.getSize();
		if((e.getX() >= (size.width - 7)) && (e.getY() >= (size.height - 7)) && (getSelectionState() == 2))
		{
			// Click in selected grow-box:
			Point loc = fComponent.getLocationOnScreen();
			fGrowBase = new Point(loc.x + e.getX() - fWidth, loc.y + e.getY() - fHeight);
			fGrowProportionally = e.isShiftDown();
		}
		else
		{
			// Else select image:
			fGrowBase = null;
			JTextComponent comp = (JTextComponent)fContainer;
			int start = fElement.getStartOffset();
			int end = fElement.getEndOffset();
			int mark = comp.getCaret().getMark();
			int dot  = comp.getCaret().getDot();
			if(e.isShiftDown())
			{
				// extend selection if shift key down:
				if(mark <= start)
				{
					comp.moveCaretPosition(end);
				}
				else
				{
					comp.moveCaretPosition(start);
				}
			}
			else
			{
				// just select image, without shift:
				if(mark != start)
				{
					comp.setCaretPosition(start);
				}
				if(dot != end)
				{
					comp.moveCaretPosition(end);
				}
			}
		}
	}

	/** Resize image if initial click was in grow-box: */
	public void mouseDragged(MouseEvent e)
	{
		if(fGrowBase != null)
		{
			Point loc = fComponent.getLocationOnScreen();
			int width = Math.max(2, loc.x + e.getX() - fGrowBase.x);
			int height= Math.max(2, loc.y + e.getY() - fGrowBase.y);
			if(e.isShiftDown() && fImage != null)
			{
				// Make sure size is proportional to actual image size
				float imgWidth = fImage.getWidth(this);
				float imgHeight = fImage.getHeight(this);
				if((imgWidth > 0) && (imgHeight > 0))
				{
					float prop = imgHeight / imgWidth;
					float pwidth = height / prop;
					float pheight = width * prop;
					if(pwidth > width)
					{
						width = (int)pwidth;
					}
					else
					{
						height = (int)pheight;
					}
				}
			}
			resize(width,height);
		}
	}

	public void mouseReleased(MouseEvent me)
	{
		fGrowBase = null;
		//! Should post some command to make the action undo-able
	}

	/** On double-click, open image properties dialog.
	  */
	public void mouseClicked(MouseEvent me)
	{
		if(me.getClickCount() == 2)
		{
			//$ IMPLEMENT
		}
	}

	public void mouseEntered(MouseEvent me) { ; }
	public void mouseMoved(MouseEvent me)   { ; }
	public void mouseExited(MouseEvent me)  { ; }

	// Static icon accessors -----------------------------------------------

	private Icon makeIcon(final String gifFile)
	throws IOException
	{
		/* Copy resource into a byte array. This is
		 * necessary because several browsers consider
		 * Class.getResource a security risk because it
		 * can be used to load additional classes.
		 * Class.getResourceAsStream just returns raw
		 * bytes, which we can convert to an image.
		 */
		InputStream resource = RelativeImageView.class.getResourceAsStream(gifFile);

		if(resource == null)
		{
			return null;
		}
		BufferedInputStream in = new BufferedInputStream(resource);
		ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
		byte[] buffer = new byte[1024];
		int n;
		while((n = in.read(buffer)) > 0)
		{
			out.write(buffer, 0, n);
		}
		in.close();
		out.flush();

		buffer = out.toByteArray();
		if(buffer.length == 0)
		{
			System.err.println("WARNING : " + gifFile + " is zero-length");
			return null;
		}
		return new ImageIcon(buffer);
	}

	private void loadImageStatusIcons()
	{
		try
		{
			if(sPendingImageIcon == null)
			{
				sPendingImageIcon = makeIcon(PENDING_IMAGE_SRC);
			}
			if(sMissingImageIcon == null)
			{
				sMissingImageIcon = makeIcon(MISSING_IMAGE_SRC);
			}
		}
		catch(Exception e)
		{
			System.err.println("ImageView : Couldn't load image icons");
		}
	}

	protected StyleSheet getStyleSheet()
	{
		HTMLDocument doc = (HTMLDocument)getDocument();
		return doc.getStyleSheet();
	}

}
