tentative merge of EKIT_0_9H and custom patches
[old-projects.git] / ekit / com / hexidec / ekit / component / RelativeImageView.java
index 6ef7b6c..c2ba48a 100644 (file)
-/*\r
-GNU Lesser General Public License\r
-\r
-RelativeImageView\r
-Copyright (C) 2001-2002  Frits Jalvingh & Howard Kistler\r
-\r
-This library is free software; you can redistribute it and/or\r
-modify it under the terms of the GNU Lesser General Public\r
-License as published by the Free Software Foundation; either\r
-version 2.1 of the License, or (at your option) any later version.\r
-\r
-This library is distributed in the hope that it will be useful,\r
-but WITHOUT ANY WARRANTY; without even the implied warranty of\r
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
-Lesser General Public License for more details.\r
-\r
-You should have received a copy of the GNU Lesser General Public\r
-License along with this library; if not, write to the Free Software\r
-Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
-*/\r
-\r
-package com.hexidec.ekit.component;\r
-\r
-import java.awt.Color;\r
-import java.awt.Component;\r
-import java.awt.Container;\r
-import java.awt.Dimension;\r
-import java.awt.Graphics;\r
-import java.awt.Image;\r
-import java.awt.Point;\r
-import java.awt.Rectangle;\r
-import java.awt.Shape;\r
-import java.awt.Toolkit;\r
-import java.awt.event.MouseEvent;\r
-import java.awt.event.MouseListener;\r
-import java.awt.event.MouseMotionListener;\r
-import java.awt.image.ImageObserver;\r
-import java.io.BufferedInputStream;\r
-import java.io.ByteArrayOutputStream;\r
-import java.io.File;\r
-import java.io.InputStream;\r
-import java.io.IOException;\r
-import java.net.MalformedURLException;\r
-import java.net.URL;\r
-import java.util.Dictionary;\r
-import javax.swing.Icon;\r
-import javax.swing.ImageIcon;\r
-import javax.swing.JEditorPane;\r
-import javax.swing.text.AbstractDocument;\r
-import javax.swing.text.AttributeSet;\r
-import javax.swing.text.BadLocationException;\r
-import javax.swing.text.Document;\r
-import javax.swing.text.Element;\r
-import javax.swing.text.JTextComponent;\r
-import javax.swing.text.MutableAttributeSet;\r
-import javax.swing.text.Position;\r
-import javax.swing.text.SimpleAttributeSet;\r
-import javax.swing.text.StyledDocument;\r
-import javax.swing.text.View;\r
-import javax.swing.text.ViewFactory;\r
-import javax.swing.text.html.HTML;\r
-import javax.swing.text.html.HTMLDocument;\r
-import javax.swing.text.html.StyleSheet;\r
-import javax.swing.event.DocumentEvent;\r
-\r
-/**\r
-  * @author <a href="mailto:jal@grimor.com">Frits Jalvingh</a>\r
-  * @version 1.0\r
-  *\r
-  * This code was modeled after an artice on\r
-  * <a href="http://www.javaworld.com/javaworld/javatips/jw-javatip109.html">\r
-  * JavaWorld</a> by Bob Kenworthy.\r
-  */\r
-\r
-public class RelativeImageView extends View implements ImageObserver, MouseListener, MouseMotionListener\r
-{\r
-       public static final String TOP       = "top";\r
-       public static final String TEXTTOP   = "texttop";\r
-       public static final String MIDDLE    = "middle";\r
-       public static final String ABSMIDDLE = "absmiddle";\r
-       public static final String CENTER    = "center";\r
-       public static final String BOTTOM    = "bottom";\r
-       public static final String IMAGE_CACHE_PROPERTY = "imageCache";\r
-\r
-       private static Icon sPendingImageIcon;\r
-       private static Icon sMissingImageIcon;\r
-       private static final String PENDING_IMAGE_SRC = "icons/ImagePendingHK.gif";\r
-       private static final String MISSING_IMAGE_SRC = "icons/ImageMissingHK.gif";\r
-       private static final int DEFAULT_WIDTH  = 32;\r
-       private static final int DEFAULT_HEIGHT = 32;\r
-       private static final int DEFAULT_BORDER = 1;\r
-\r
-       private AttributeSet attr;\r
-       private Element      fElement;\r
-       private Image        fImage;\r
-       private int          fHeight;\r
-       private int          fWidth;\r
-       private Container    fContainer;\r
-       private Rectangle    fBounds;\r
-       private Component    fComponent;\r
-       private Point        fGrowBase; // base of drag while growing image\r
-       private boolean      fGrowProportionally; // should grow be proportional?\r
-       private String BaseUrl;\r
-       private boolean      bLoading; // set to true while the receiver is locked, to indicate the reciever is loading the image. This is used in imageUpdate.\r
-\r
-       /** Constructor\r
-         * Creates a new view that represents an IMG element.\r
-         * @param elem the element to create a view for\r
-         */\r
-       public RelativeImageView(Element elem,String baseurl)\r
-       {\r
-               super(elem);\r
-               BaseUrl = baseurl;\r
-               initialize(elem);\r
-               StyleSheet sheet = getStyleSheet();\r
-               attr = sheet.getViewAttributes(this);\r
-       }\r
-\r
-       private void initialize(Element elem)\r
-       {\r
-               synchronized(this)\r
-               {\r
-                       bLoading = true;\r
-                       fWidth  = 0;\r
-                       fHeight = 0;\r
-               }\r
-               int width = 0;\r
-               int height = 0;\r
-               boolean customWidth = false;\r
-               boolean customHeight = false;\r
-               try\r
-               {\r
-                       fElement = elem;\r
-                       // request image from document's cache\r
-                       AttributeSet attr = elem.getAttributes();\r
-                       if(true || isURL())\r
-                       {\r
-                               URL src = getSourceURL();\r
-                               if(src != null)\r
-                               {\r
-                                       Dictionary cache = (Dictionary)getDocument().getProperty(IMAGE_CACHE_PROPERTY);\r
-                                       if(cache != null)\r
-                                       {\r
-                                               fImage = (Image)cache.get(src);\r
-                                       }\r
-                                       else\r
-                                       {\r
-                                               fImage = Toolkit.getDefaultToolkit().getImage(src);\r
-                                       }\r
-                               }\r
-                       }\r
-                       else\r
-                       {\r
-                               // load image from relative path\r
-                               String src = (String)fElement.getAttributes().getAttribute(HTML.Attribute.SRC);\r
-                               src = processSrcPath(src);\r
-                               fImage = Toolkit.getDefaultToolkit().createImage(src);\r
-                               try\r
-                               {\r
-                                       waitForImage();\r
-                               }\r
-                               catch(InterruptedException ie)\r
-                               {\r
-                                       fImage = null;\r
-                                       // possibly replace with the ImageBroken icon, if that's what is happening\r
-                               }\r
-                       }\r
-\r
-                       // get height & width from params or image or defaults\r
-                       height = getIntAttr(HTML.Attribute.HEIGHT, -1);\r
-                       customHeight = (height > 0);\r
-                       if(!customHeight && fImage != null)\r
-                       {\r
-                               height = fImage.getHeight(this);\r
-                       }\r
-                       if(height <= 0)\r
-                       {\r
-                               height = DEFAULT_HEIGHT;\r
-                       }\r
-\r
-                       width = getIntAttr(HTML.Attribute.WIDTH, -1);\r
-                       customWidth = (width > 0);\r
-                       if(!customWidth && fImage != null)\r
-                       {\r
-                               width = fImage.getWidth(this);\r
-                       }\r
-                       if(width <= 0)\r
-                       {\r
-                               width = DEFAULT_WIDTH;\r
-                       }\r
-\r
-                       if(fImage != null)\r
-                       {\r
-                               if(customHeight && customWidth)\r
-                               {\r
-                                       Toolkit.getDefaultToolkit().prepareImage(fImage, height, width, this);\r
-                               }\r
-                               else\r
-                               {\r
-                                       Toolkit.getDefaultToolkit().prepareImage(fImage, -1, -1, this);\r
-                               }\r
-                       }\r
-               }\r
-               finally\r
-               {\r
-                       synchronized(this)\r
-                       {\r
-                               bLoading = false;\r
-                               if(customHeight || fHeight == 0)\r
-                               {\r
-                                       fHeight = height;\r
-                               }\r
-                               if(customWidth || fWidth == 0)\r
-                               {\r
-                                       fWidth = width;\r
-                               }\r
-                       }\r
-               }\r
-       }\r
-\r
-       /** Determines if path is in the form of a URL\r
-         */\r
-       private boolean isURL()\r
-       {\r
-               String src = (String)fElement.getAttributes().getAttribute(HTML.Attribute.SRC);\r
-               return src.toLowerCase().startsWith("file") || src.toLowerCase().startsWith("http");\r
-       }\r
-\r
-       /** Checks to see if the absolute path is availabe thru an application\r
-         * global static variable or thru a system variable. If so, appends\r
-         * the relative path to the absolute path and returns the String.\r
-         */\r
-       private String processSrcPath(String src)\r
-       {\r
-               String val = src;\r
-               File imageFile = new File(src);\r
-               if(imageFile.isAbsolute())\r
-               {\r
-                       return src;\r
-               }\r
-               boolean found = false;\r
-               Document doc = getDocument();\r
-               if(doc != null)\r
-               {\r
-                       String pv = (String)doc.getProperty("com.hexidec.ekit.docsource");\r
-                       if(pv != null)\r
-                       {\r
-                               File f = new File(pv);\r
-                               val     = (new File(f.getParent(), imageFile.getPath().toString())).toString();\r
-                               found = true;\r
-                       }\r
-               }\r
-               if(!found)\r
-               {\r
-                       String imagePath = System.getProperty("system.image.path.key");\r
-                       if(imagePath != null)\r
-                       {\r
-                               val = (new File(imagePath, imageFile.getPath())).toString();\r
-                       }\r
-               }\r
-               return val;\r
-       }\r
-\r
-       /** Method insures that the image is loaded and not a broken reference\r
-         */\r
-       private void waitForImage()\r
-       throws InterruptedException\r
-       {\r
-               int w = fImage.getWidth(this);\r
-               int h = fImage.getHeight(this);\r
-               while (true)\r
-               {\r
-                       int flags = Toolkit.getDefaultToolkit().checkImage(fImage, w, h, this);\r
-                       if(((flags & ERROR) != 0) || ((flags & ABORT) != 0 ))\r
-                       {\r
-                               throw new InterruptedException();\r
-                       }\r
-                       else if((flags & (ALLBITS | FRAMEBITS)) != 0)\r
-                       {\r
-                               return;\r
-                       }\r
-                       Thread.sleep(10);\r
-               }\r
-       }\r
-\r
-       /** Fetches the attributes to use when rendering. This is\r
-         * implemented to multiplex the attributes specified in the\r
-         * model with a StyleSheet.\r
-         */\r
-       public AttributeSet getAttributes()\r
-       {\r
-               return attr;\r
-       }\r
-\r
-       /** Method tests whether the image within a link\r
-         */\r
-       boolean isLink()\r
-       {\r
-               AttributeSet anchorAttr = (AttributeSet)fElement.getAttributes().getAttribute(HTML.Tag.A);\r
-               if(anchorAttr != null)\r
-               {\r
-                       return anchorAttr.isDefined(HTML.Attribute.HREF);\r
-               }\r
-               return false;\r
-       }\r
-\r
-       /** Method returns the size of the border to use\r
-         */\r
-       int getBorder()\r
-       {\r
-               return getIntAttr(HTML.Attribute.BORDER, isLink() ? DEFAULT_BORDER : 0);\r
-       }\r
-\r
-       /** Method returns the amount of extra space to add along an axis\r
-               */\r
-       int getSpace(int axis)\r
-       {\r
-               return getIntAttr((axis == X_AXIS) ? HTML.Attribute.HSPACE : HTML.Attribute.VSPACE, 0);\r
-       }\r
-\r
-       /** Method returns the border's color, or null if this is not a link\r
-       */\r
-       Color getBorderColor()\r
-       {\r
-               StyledDocument doc = (StyledDocument)getDocument();\r
-               return doc.getForeground(getAttributes());\r
-       }\r
-\r
-       /** Method returns the image's vertical alignment\r
-         */\r
-       float getVerticalAlignment()\r
-       {\r
-               String align = (String)fElement.getAttributes().getAttribute(HTML.Attribute.ALIGN);\r
-               if(align != null)\r
-               {\r
-                       align = align.toLowerCase();\r
-                       if(align.equals(TOP) || align.equals(TEXTTOP))\r
-                       {\r
-                               return 0.0f;\r
-                       }\r
-                       else if(align.equals(this.CENTER) || align.equals(MIDDLE) || align.equals(ABSMIDDLE))\r
-                       {\r
-                               return 0.5f;\r
-                       }\r
-               }\r
-               return 1.0f; // default alignment is bottom\r
-       }\r
-\r
-       boolean hasPixels(ImageObserver obs)\r
-       {\r
-               return ((fImage != null) && (fImage.getHeight(obs) > 0) && (fImage.getWidth(obs) > 0));\r
-       }\r
-\r
-       /** Method returns a URL for the image source, or null if it could not be determined\r
-         */\r
-       private URL getSourceURL()\r
-       {\r
-               String src = (String)fElement.getAttributes().getAttribute(HTML.Attribute.SRC);\r
-               if(src == null)\r
-               {\r
-                       return null;\r
-               }\r
-               //URL reference = ((HTMLDocument)getDocument()).getBase();\r
-               try\r
-               {\r
-                       URL reference = new URL(BaseUrl);\r
-                       URL u = new URL(reference,src);\r
-                       return u;\r
-               }\r
-               catch(MalformedURLException mue)\r
-               {\r
-                       return null;\r
-               }\r
-       }\r
-\r
-       /** Method looks up an integer-valued attribute (not recursive!)\r
-         */\r
-       private int getIntAttr(HTML.Attribute name, int iDefault)\r
-       {\r
-               AttributeSet attr = fElement.getAttributes();\r
-               if(attr.isDefined(name))\r
-               {\r
-                       int i;\r
-                       String val = (String)attr.getAttribute(name);\r
-                       if(val == null)\r
-                       {\r
-                               i = iDefault;\r
-                       }\r
-                       else\r
-                       {\r
-                               try\r
-                               {\r
-                                       i = Math.max(0, Integer.parseInt(val));\r
-                               }\r
-                               catch(NumberFormatException nfe)\r
-                               {\r
-                                       i = iDefault;\r
-                               }\r
-                       }\r
-                       return i;\r
-               }\r
-               else\r
-               {\r
-                       return iDefault;\r
-               }\r
-       }\r
-\r
-       /**\r
-       * Establishes the parent view for this view.\r
-       * Seize this moment to cache the AWT Container I'm in.\r
-       */\r
-       public void setParent(View parent)\r
-       {\r
-               super.setParent(parent);\r
-               fContainer = ((parent != null) ? getContainer() : null);\r
-               if((parent == null) && (fComponent != null))\r
-               {\r
-                       fComponent.getParent().remove(fComponent);\r
-                       fComponent = null;\r
-               }\r
-       }\r
-\r
-       /** My attributes may have changed. */\r
-       public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f)\r
-       {\r
-               super.changedUpdate(e, a, f);\r
-               float align = getVerticalAlignment();\r
-\r
-               int height = fHeight;\r
-               int width  = fWidth;\r
-\r
-               initialize(getElement());\r
-\r
-               boolean hChanged = fHeight != height;\r
-               boolean wChanged = fWidth != width;\r
-               if(hChanged || wChanged || getVerticalAlignment() != align)\r
-               {\r
-                       getParent().preferenceChanged(this, hChanged, wChanged);\r
-               }\r
-       }\r
-\r
-\r
-       /**\r
-         * Paints the image.\r
-         *\r
-         * @param g the rendering surface to use\r
-         * @param a the allocated region to render into\r
-         * @see View#paint\r
-         */\r
-       public void paint(Graphics g, Shape a)\r
-       {\r
-               Color oldColor = g.getColor();\r
-               fBounds = a.getBounds();\r
-               int border = getBorder();\r
-               int x = fBounds.x + border + getSpace(X_AXIS);\r
-               int y = fBounds.y + border + getSpace(Y_AXIS);\r
-               int width = fWidth;\r
-               int height = fHeight;\r
-               int sel = getSelectionState();\r
-\r
-               // If no pixels yet, draw gray outline and icon\r
-               if(!hasPixels(this))\r
-               {\r
-                       g.setColor(Color.lightGray);\r
-                       g.drawRect(x, y, width - 1, height - 1);\r
-                       g.setColor(oldColor);\r
-                       loadImageStatusIcons();\r
-                       Icon icon = ((fImage == null) ? sMissingImageIcon : sPendingImageIcon);\r
-                       if(icon != null)\r
-                       {\r
-                               icon.paintIcon(getContainer(), g, x, y);\r
-                       }\r
-               }\r
-\r
-               // Draw image\r
-               if(fImage != null)\r
-               {\r
-                       g.drawImage(fImage, x, y, width, height, this);\r
-               }\r
-\r
-               // If selected exactly, we need a black border & grow-box\r
-               Color bc = getBorderColor();\r
-               if(sel == 2)\r
-               {\r
-                       // Make sure there's room for a border\r
-                       int delta = 2 - border;\r
-                       if(delta > 0)\r
-                       {\r
-                               x += delta;\r
-                               y += delta;\r
-                               width -= delta << 1;\r
-                               height -= delta << 1;\r
-                               border = 2;\r
-                       }\r
-                       bc = null;\r
-                       g.setColor(Color.black);\r
-                       // Draw grow box\r
-                       g.fillRect(x + width - 5, y + height - 5, 5, 5);\r
-               }\r
-\r
-               // Draw border\r
-               if(border > 0)\r
-               {\r
-                       if(bc != null)\r
-                       {\r
-                               g.setColor(bc);\r
-                       }\r
-                       // Draw a thick rectangle:\r
-                       for(int i = 1; i <= border; i++)\r
-                       {\r
-                               g.drawRect(x - i, y - i, width - 1 + i + i, height - 1 + i + i);\r
-                       }\r
-                       g.setColor(oldColor);\r
-               }\r
-       }\r
-\r
-       /** Request that this view be repainted. Assumes the view is still at its last-drawn location.\r
-         */\r
-       protected void repaint(long delay)\r
-       {\r
-               if((fContainer != null) && (fBounds != null))\r
-               {\r
-                       fContainer.repaint(delay, fBounds.x, fBounds.y, fBounds.width, fBounds.height);\r
-               }\r
-       }\r
-\r
-       /**\r
-         * Determines whether the image is selected, and if it's the only thing selected.\r
-         * @return  0 if not selected, 1 if selected, 2 if exclusively selected.\r
-         * "Exclusive" selection is only returned when editable.\r
-         */\r
-       protected int getSelectionState()\r
-       {\r
-               int p0 = fElement.getStartOffset();\r
-               int p1 = fElement.getEndOffset();\r
-               if(fContainer instanceof JTextComponent)\r
-               {\r
-                       JTextComponent textComp = (JTextComponent)fContainer;\r
-                       int start = textComp.getSelectionStart();\r
-                       int end = textComp.getSelectionEnd();\r
-                       if((start <= p0) && (end >= p1))\r
-                       {\r
-                               if((start == p0) && (end == p1) && isEditable())\r
-                               {\r
-                                       return 2;\r
-                               }\r
-                               else\r
-                               {\r
-                                       return 1;\r
-                               }\r
-                       }\r
-               }\r
-               return 0;\r
-       }\r
-\r
-       protected boolean isEditable()\r
-       {\r
-               return ((fContainer instanceof JEditorPane) && ((JEditorPane)fContainer).isEditable());\r
-       }\r
-\r
-       /** Returns the text editor's highlight color.\r
-         */\r
-       protected Color getHighlightColor()\r
-       {\r
-               JTextComponent textComp = (JTextComponent)fContainer;\r
-               return textComp.getSelectionColor();\r
-       }\r
-\r
-       // Progressive display -------------------------------------------------\r
-\r
-       // This can come on any thread. If we are in the process of reloading\r
-       // the image and determining our state (loading == true) we don't fire\r
-       // preference changed, or repaint, we just reset the fWidth/fHeight as\r
-       // necessary and return. This is ok as we know when loading finishes\r
-       // it will pick up the new height/width, if necessary.\r
-\r
-       private static boolean sIsInc = true;\r
-       private static int sIncRate = 100;\r
-\r
-       public boolean imageUpdate(Image img, int flags, int x, int y, int width, int height)\r
-       {\r
-               if((fImage == null) || (fImage != img))\r
-               {\r
-                       return false;\r
-               }\r
-\r
-               // Bail out if there was an error\r
-               if((flags & (ABORT|ERROR)) != 0)\r
-               {\r
-                       fImage = null;\r
-                       repaint(0);\r
-                       return false;\r
-               }\r
-\r
-               // Resize image if necessary\r
-               short changed = 0;\r
-               if((flags & ImageObserver.HEIGHT) != 0)\r
-               {\r
-                       if(!getElement().getAttributes().isDefined(HTML.Attribute.HEIGHT))\r
-                       {\r
-                               changed |= 1;\r
-                       }\r
-               }\r
-               if((flags & ImageObserver.WIDTH) != 0)\r
-               {\r
-                       if(!getElement().getAttributes().isDefined(HTML.Attribute.WIDTH))\r
-                       {\r
-                               changed |= 2;\r
-                       }\r
-               }\r
-\r
-               synchronized(this)\r
-               {\r
-                       if((changed & 1) == 1)\r
-                       {\r
-                               fWidth = width;\r
-                       }\r
-                       if((changed & 2) == 2)\r
-                       {\r
-                               fHeight = height;\r
-                       }\r
-                       if(bLoading)\r
-                       {\r
-                               // No need to resize or repaint, still in the process of loading\r
-                               return true;\r
-                       }\r
-               }\r
-\r
-               if(changed != 0)\r
-               {\r
-                       // May need to resize myself, asynchronously\r
-                       Document doc = getDocument();\r
-                       try\r
-                       {\r
-                               if(doc instanceof AbstractDocument)\r
-                               {\r
-                                       ((AbstractDocument)doc).readLock();\r
-                               }\r
-                               preferenceChanged(this, true, true);\r
-                       }\r
-                       finally\r
-                       {\r
-                               if(doc instanceof AbstractDocument)\r
-                               {\r
-                                       ((AbstractDocument)doc).readUnlock();\r
-                               }\r
-                       }\r
-                       return true;\r
-               }\r
-\r
-               // Repaint when done or when new pixels arrive\r
-               if((flags & (FRAMEBITS|ALLBITS)) != 0)\r
-               {\r
-                       repaint(0);\r
-               }\r
-               else if((flags & SOMEBITS) != 0)\r
-               {\r
-                       if(sIsInc)\r
-                       {\r
-                               repaint(sIncRate);\r
-                       }\r
-               }\r
-               return ((flags & ALLBITS) == 0);\r
-       }\r
-\r
-       // Layout --------------------------------------------------------------\r
-\r
-       /** Determines the preferred span for this view along an axis.\r
-         *\r
-         * @param axis may be either X_AXIS or Y_AXIS\r
-         * @returns  the span the view would like to be rendered into.\r
-         *           Typically the view is told to render into the span\r
-         *           that is returned, although there is no guarantee.\r
-         *           The parent may choose to resize or break the view.\r
-         */\r
-       public float getPreferredSpan(int axis)\r
-       {\r
-               int extra = 2 * (getBorder() + getSpace(axis));\r
-               switch(axis)\r
-               {\r
-                       case View.X_AXIS:\r
-                               return fWidth+extra;\r
-                       case View.Y_AXIS:\r
-                               return fHeight+extra;\r
-                       default:\r
-                               throw new IllegalArgumentException("Invalid axis in getPreferredSpan() : " + axis);\r
-               }\r
-       }\r
-\r
-       /** Determines the desired alignment for this view along an\r
-         * axis. This is implemented to give the alignment to the\r
-         * bottom of the icon along the y axis, and the default\r
-         * along the x axis.\r
-         *\r
-         * @param axis may be either X_AXIS or Y_AXIS\r
-         * @returns the desired alignment. This should be a value\r
-         *   between 0.0 and 1.0 where 0 indicates alignment at the\r
-         *   origin and 1.0 indicates alignment to the full span\r
-         *   away from the origin. An alignment of 0.5 would be the\r
-         *   center of the view.\r
-         */\r
-       public float getAlignment(int axis)\r
-       {\r
-               switch(axis)\r
-               {\r
-                       case View.Y_AXIS:\r
-                               return getVerticalAlignment();\r
-                       default:\r
-                               return super.getAlignment(axis);\r
-               }\r
-       }\r
-\r
-       /** Provides a mapping from the document model coordinate space\r
-         * to the coordinate space of the view mapped to it.\r
-         *\r
-         * @param pos the position to convert\r
-         * @param a the allocated region to render into\r
-         * @return the bounding box of the given position\r
-         * @exception BadLocationException if the given position does not represent a\r
-         *   valid location in the associated document\r
-         * @see View#modelToView\r
-         */\r
-       public Shape modelToView(int pos, Shape a, Position.Bias b)\r
-       throws BadLocationException\r
-       {\r
-               int p0 = getStartOffset();\r
-               int p1 = getEndOffset();\r
-               if((pos >= p0) && (pos <= p1))\r
-               {\r
-                       Rectangle r = a.getBounds();\r
-                       if(pos == p1)\r
-                       {\r
-                               r.x += r.width;\r
-                       }\r
-                       r.width = 0;\r
-                       return r;\r
-               }\r
-               return null;\r
-       }\r
-\r
-       /** Provides a mapping from the view coordinate space to the logical\r
-         * coordinate space of the model.\r
-         *\r
-         * @param x the X coordinate\r
-         * @param y the Y coordinate\r
-         * @param a the allocated region to render into\r
-         * @return the location within the model that best represents the\r
-         *         given point of view\r
-         * @see View#viewToModel\r
-         */\r
-       public int viewToModel(float x, float y, Shape a, Position.Bias[] bias)\r
-       {\r
-               Rectangle alloc = (Rectangle) a;\r
-               if(x < (alloc.x + alloc.width))\r
-               {\r
-                       bias[0] = Position.Bias.Forward;\r
-                       return getStartOffset();\r
-               }\r
-               bias[0] = Position.Bias.Backward;\r
-               return getEndOffset();\r
-       }\r
-\r
-       /** Change the size of this image. This alters the HEIGHT and WIDTH\r
-         * attributes of the Element and causes a re-layout.\r
-         */\r
-       protected void resize(int width, int height)\r
-       {\r
-               if((width == fWidth) && (height == fHeight))\r
-               {\r
-                       return;\r
-               }\r
-               fWidth = width;\r
-               fHeight= height;\r
-               // Replace attributes in document\r
-               MutableAttributeSet attr = new SimpleAttributeSet();\r
-               attr.addAttribute(HTML.Attribute.WIDTH ,Integer.toString(width));\r
-               attr.addAttribute(HTML.Attribute.HEIGHT,Integer.toString(height));\r
-               ((StyledDocument)getDocument()).setCharacterAttributes(fElement.getStartOffset(), fElement.getEndOffset(), attr, false);\r
-       }\r
-\r
-       // Mouse event handling ------------------------------------------------\r
-\r
-       /** Select or grow image when clicked.\r
-         */\r
-       public void mousePressed(MouseEvent e)\r
-       {\r
-               Dimension size = fComponent.getSize();\r
-               if((e.getX() >= (size.width - 7)) && (e.getY() >= (size.height - 7)) && (getSelectionState() == 2))\r
-               {\r
-                       // Click in selected grow-box:\r
-                       Point loc = fComponent.getLocationOnScreen();\r
-                       fGrowBase = new Point(loc.x + e.getX() - fWidth, loc.y + e.getY() - fHeight);\r
-                       fGrowProportionally = e.isShiftDown();\r
-               }\r
-               else\r
-               {\r
-                       // Else select image:\r
-                       fGrowBase = null;\r
-                       JTextComponent comp = (JTextComponent)fContainer;\r
-                       int start = fElement.getStartOffset();\r
-                       int end = fElement.getEndOffset();\r
-                       int mark = comp.getCaret().getMark();\r
-                       int dot  = comp.getCaret().getDot();\r
-                       if(e.isShiftDown())\r
-                       {\r
-                               // extend selection if shift key down:\r
-                               if(mark <= start)\r
-                               {\r
-                                       comp.moveCaretPosition(end);\r
-                               }\r
-                               else\r
-                               {\r
-                                       comp.moveCaretPosition(start);\r
-                               }\r
-                       }\r
-                       else\r
-                       {\r
-                               // just select image, without shift:\r
-                               if(mark != start)\r
-                               {\r
-                                       comp.setCaretPosition(start);\r
-                               }\r
-                               if(dot != end)\r
-                               {\r
-                                       comp.moveCaretPosition(end);\r
-                               }\r
-                       }\r
-               }\r
-       }\r
-\r
-       /** Resize image if initial click was in grow-box: */\r
-       public void mouseDragged(MouseEvent e)\r
-       {\r
-               if(fGrowBase != null)\r
-               {\r
-                       Point loc = fComponent.getLocationOnScreen();\r
-                       int width = Math.max(2, loc.x + e.getX() - fGrowBase.x);\r
-                       int height= Math.max(2, loc.y + e.getY() - fGrowBase.y);\r
-                       if(e.isShiftDown() && fImage != null)\r
-                       {\r
-                               // Make sure size is proportional to actual image size\r
-                               float imgWidth = fImage.getWidth(this);\r
-                               float imgHeight = fImage.getHeight(this);\r
-                               if((imgWidth > 0) && (imgHeight > 0))\r
-                               {\r
-                                       float prop = imgHeight / imgWidth;\r
-                                       float pwidth = height / prop;\r
-                                       float pheight = width * prop;\r
-                                       if(pwidth > width)\r
-                                       {\r
-                                               width = (int)pwidth;\r
-                                       }\r
-                                       else\r
-                                       {\r
-                                               height = (int)pheight;\r
-                                       }\r
-                               }\r
-                       }\r
-                       resize(width,height);\r
-               }\r
-       }\r
-\r
-       public void mouseReleased(MouseEvent me)\r
-       {\r
-               fGrowBase = null;\r
-               //! Should post some command to make the action undo-able\r
-       }\r
-\r
-       /** On double-click, open image properties dialog.\r
-         */\r
-       public void mouseClicked(MouseEvent me)\r
-       {\r
-               if(me.getClickCount() == 2)\r
-               {\r
-                       //$ IMPLEMENT\r
-               }\r
-       }\r
-\r
-       public void mouseEntered(MouseEvent me) { ; }\r
-       public void mouseMoved(MouseEvent me)   { ; }\r
-       public void mouseExited(MouseEvent me)  { ; }\r
-\r
-       // Static icon accessors -----------------------------------------------\r
-\r
-       private Icon makeIcon(final String gifFile)\r
-       throws IOException\r
-       {\r
-               /* Copy resource into a byte array. This is\r
-                * necessary because several browsers consider\r
-                * Class.getResource a security risk because it\r
-                * can be used to load additional classes.\r
-                * Class.getResourceAsStream just returns raw\r
-                * bytes, which we can convert to an image.\r
-                */\r
-               InputStream resource = RelativeImageView.class.getResourceAsStream(gifFile);\r
-\r
-               if(resource == null)\r
-               {\r
-                       return null;\r
-               }\r
-               BufferedInputStream in = new BufferedInputStream(resource);\r
-               ByteArrayOutputStream out = new ByteArrayOutputStream(1024);\r
-               byte[] buffer = new byte[1024];\r
-               int n;\r
-               while((n = in.read(buffer)) > 0)\r
-               {\r
-                       out.write(buffer, 0, n);\r
-               }\r
-               in.close();\r
-               out.flush();\r
-\r
-               buffer = out.toByteArray();\r
-               if(buffer.length == 0)\r
-               {\r
-                       System.err.println("WARNING : " + gifFile + " is zero-length");\r
-                       return null;\r
-               }\r
-               return new ImageIcon(buffer);\r
-       }\r
-\r
-       private void loadImageStatusIcons()\r
-       {\r
-               try\r
-               {\r
-                       if(sPendingImageIcon == null)\r
-                       {\r
-                               sPendingImageIcon = makeIcon(PENDING_IMAGE_SRC);\r
-                       }\r
-                       if(sMissingImageIcon == null)\r
-                       {\r
-                               sMissingImageIcon = makeIcon(MISSING_IMAGE_SRC);\r
-                       }\r
-               }\r
-               catch(Exception e)\r
-               {\r
-                       System.err.println("ImageView : Couldn't load image icons");\r
-               }\r
-       }\r
-\r
-       protected StyleSheet getStyleSheet()\r
-       {\r
-               HTMLDocument doc = (HTMLDocument)getDocument();\r
-               return doc.getStyleSheet();\r
-       }\r
-\r
-}\r
+/*
+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();
+       }
+
+}