Java tutorial
/* 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.IOException; import java.io.InputStream; 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.event.DocumentEvent; 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 org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * @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 { private static final Log log = LogFactory.getLog(RelativeImageView.class); 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 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) { super(elem); 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 (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 } catch (Exception ex) { fImage = null; // trap a null exception or other exception that puts the image pointer into an empty or ambiguous state } } // 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; } URL reference = ((HTMLDocument) getDocument()).getBase(); try { 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) { log.warn(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) { log.error("ImageView : Couldn't load image icons", e); } } protected StyleSheet getStyleSheet() { HTMLDocument doc = (HTMLDocument) getDocument(); return doc.getStyleSheet(); } }