com.hexidec.ekit.component.RelativeImageView.java Source code

Java tutorial

Introduction

Here is the source code for com.hexidec.ekit.component.RelativeImageView.java

Source

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

}