-/*\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();
+ }
+
+}