// HTMLParser Library $Name: v1_6 $ - A java-based parser for HTML
// http://sourceforge.org/projects/htmlparser
// Copyright (C) 2003 Derrick Oswald
//
// Revision Control Information
//
// $Source: /cvsroot/htmlparser/htmlparser/src/org/htmlparser/lexerapplications/thumbelina/PicturePanel.java,v $
// $Author: derrickoswald $
// $Date: 2005/04/12 11:27:41 $
// $Revision: 1.2 $
//
// 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 org.htmlparser.lexerapplications.thumbelina;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Enumeration;
import java.util.HashSet;
import javax.swing.JPanel;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.border.BevelBorder;

/**
 * Hold and display a group of pictures.
 * @author  derrick
 */
public class PicturePanel
    extends
        JPanel
    implements
        MouseListener,
        Scrollable,
        ComponentListener,
        HierarchyListener
{
    /**
     * Scrolling unit increment (both directions).
     */
    protected static final int UNIT_INCREMENT = 10;

    /**
     * Scrolling block increment (both directions).
     */
    protected static final int BLOCK_INCREMENT = 100;

    /**
     * The thumbelina object in use.
     */
    protected Thumbelina mThumbelina;

    /**
     * The display mosaic.
     */
    protected TileSet mMosaic;

    /**
     * The preferred size of this component.
     * <code>null</code> initially, caches the results of
     * <code>calculatePreferredSize ()</code>.
     */
    protected Dimension mPreferredSize;

    /**
     * Creates a new instance of PicturePanel
     * @param thumbelina The <code>Thumeblina</code> this panel is associated
     * with.
     */
    public PicturePanel (final Thumbelina thumbelina)
    {
        mThumbelina = thumbelina;
        mMosaic = new TileSet ();
        mPreferredSize = null;
        setBorder (new BevelBorder (BevelBorder.LOWERED));
        addMouseListener (this);
        addHierarchyListener (this);
    }

    /**
     * Clears the panel, discarding any existing images.
     */
    public void reset ()
    {
        mMosaic = new TileSet ();
        repaint ();
    }

    /**
     * Move the given picture to the top of the Z order.
     * Adds it, even it if it doesn't exist.
     * Also puts the URL in the url text of the status bar.
     * @param picture The picture being brought forward.
     */
    public void bringToTop (final Picture picture)
    {
        picture.reset ();
        mMosaic.bringToTop (picture);
        repaint (picture.x, picture.y, picture.width, picture.height);
        mThumbelina.mUrlText.setText (picture.getURL ().toExternalForm ());
    }

    /**
     * Find a picture with the given URL in the panel.
     * This should really only be used to discover if the picture is still
     * visible. There could be more than one picture with the given URL
     * because it may be partially obscured by another picture, in which
     * case the pieces are each given their own picture object, but all
     * point at the same <code>URL</code> and <code>Image</code>.
     * @param url The url to locate.
     * @return The first picture encountered in the panel,
     * or null if the picture was not found.
     */
    public Picture find (final String url)
    {
        Enumeration enumeration;
        Picture picture;
        Picture ret;

        ret = null;
        enumeration = mMosaic.getPictures ();
        while ((null == ret) && enumeration.hasMoreElements ())
        {
            picture = (Picture)enumeration.nextElement ();
            if (url.equals (picture.getURL ().toExternalForm ()))
                ret = picture;
        }

        return (ret);
    }

    /**
     * Draw an image on screen.
     * @param picture The picture to draw.
     * @param add If <code>true</code>, the picture is added to the history.
     */
    protected void draw (final Picture picture, final boolean add)
    {
        Component parent;
        boolean dolayout;
        Dimension before;
        Dimension after;

        parent = getParent ();
        dolayout = false;
        synchronized (mMosaic)
        {
            if (parent instanceof JViewport)
            {
                before = getPreferredSize ();
                mMosaic.add (picture);
                after = calculatePreferredSize ();
                if (after.width > before.width)
                    dolayout = true;
                else
                    after.width = before.width;
                if (after.height > before.height)
                    dolayout = true;
                else
                    after.height = before.height;
                if (dolayout)
                    mPreferredSize = after;
            }
            else
                mMosaic.add (picture);
        }
        if (dolayout)
            revalidate ();
        repaint (picture.x, picture.y, picture.width, picture.height);
        if (add)
            mThumbelina.addHistory (picture.getURL ().toExternalForm ());
    }

    /**
     * Updates this component.
     * @param graphics The graphics context in which to update the component.
     */
    public void update (final Graphics graphics)
    {
        paint (graphics);
    }

    /**
     * Adjust the graphics clip region to account for insets.
     * @param graphics The graphics object to set the clip region for.
     */
    public void adjustClipForInsets (final Graphics graphics)
    {
        Dimension dim;
        Insets insets;
        Rectangle clip;

        dim = getSize ();
        insets = getInsets ();
        clip = graphics.getClipBounds ();
        if (clip.x < insets.left)
            clip.x = insets.left;
        if (clip.y < insets.top)
            clip.y = insets.top;
        if (clip.x + clip.width > dim.width - insets.right)
            clip.width = dim.width - insets.right - clip.x;
        if (clip.y + clip.height > dim.height - insets.bottom)
            clip.height = dim.height - insets.bottom - clip.y;
        graphics.setClip (clip.x, clip.y, clip.width, clip.height);
    }

    /**
     * Paints this component.
     * Runs through the list of tiles and for every one that intersects
     * the clip region performs a <code>drawImage()</code>.
     * @param graphics The graphics context used to paint with.
     */
    public void paint (final Graphics graphics)
    {
        Rectangle clip;
        Enumeration enumeration;
        HashSet set; // just so we don't draw things twice
        Picture picture;
        Image image;
        Point origin;
        int width;
        int height;

        adjustClipForInsets (graphics);
        clip = graphics.getClipBounds ();
        synchronized (mMosaic)
        {
            if (0 == mMosaic.getSize ())
                super.paint (graphics);
            else
            {
                super.paint (graphics);
                enumeration = mMosaic.getPictures ();
                set = new HashSet ();
                while (enumeration.hasMoreElements ())
                {
                    picture = (Picture)enumeration.nextElement ();
                    if ((null == clip) || (clip.intersects (picture)))
                    {
                        image = picture.getImage ();
                        if (!set.contains (image))
                        {
                            origin = picture.getOrigin ();
                            width = image.getWidth (this);
                            height = image.getHeight (this);
                            graphics.drawImage (picture.getImage (),
                                origin.x, origin.y,
                                origin.x + width, origin.y + height,
                                0, 0, width, height,
                                this);
                            set.add (image);
                        }
                    }
                }
            }
        }
    }


    /**
     * Get the preferred size of the component.
     * @return The dimension of this component.
     */
    public Dimension getPreferredSize ()
    {
        if (null == mPreferredSize)
            setPreferredSize (calculatePreferredSize ());
        else
            if ((0 == mPreferredSize.width) || (0 == mPreferredSize.height))
                setPreferredSize (calculatePreferredSize ());
        return (mPreferredSize);
    }

    /**
     * Sets the preferred size of this component.
     * @param dimension The new value to use for
     * <code>getPreferredSize()</code> until recalculated.
     */
    public void setPreferredSize (final Dimension dimension)
    {
        mPreferredSize = dimension;
    }

    /**
     * Compute the preferred size of the component.
     * Computes the minimum bounding rectangle covering all the pictures in
     * the panel. It then does some funky stuff to handle
     * embedding in the view port of a scroll pane, basically asking
     * up the ancestor heirarchy what size is available, and filling it.
     * @return The optimal dimension for this component.
     */
    protected Dimension calculatePreferredSize ()
    {
        Enumeration enumeration;
        int x;
        int y;
        Picture picture;
        Component parent;
        Insets insets;
        Dimension ret;

        enumeration = mMosaic.getPictures ();
        x = 0;
        y = 0;
        picture = null;
        while (enumeration.hasMoreElements ())
        {
            picture = (Picture)enumeration.nextElement ();
            if (picture.x + picture.width > x)
                x = picture.x + picture.width;
            if (picture.y + picture.height > y)
                y = picture.y + picture.height;
        }
        parent = getParent ();
        if (parent instanceof JViewport)
        {
            ret = parent.getSize ();
            insets = ((JViewport)parent).getInsets ();
            ret.width -= insets.left + insets.right;
            ret.height -= insets.top + insets.bottom;
            if ((0 != ret.width) || (0 != ret.height))
                ret.width -= 2; // ... I dunno why, it just needs it
            if (ret.width < x)
                ret.width = x;
            if (ret.height < y)
                ret.height = y;
        }
        else
        {
            insets = getInsets ();
            x += insets.left + insets.right;
            y += insets.top + insets.bottom;
            ret = new Dimension (x, y);
        }

        return (ret);
    }

    //
    // MouseListener Interface
    //

    /**
     * Invoked when the mouse button has been clicked
     * (pressed and released) on a component.
     * <i>Not used.</i>
     * @param event The object providing details of the mouse event.
     */
    public void mouseClicked (final MouseEvent event)
    {
    }

    /**
     *Invoked when a mouse button has been released on a component.
     * <i>Not used.</i>
     * @param event The object providing details of the mouse event.
     */
    public void mouseReleased (final MouseEvent event)
    {
    }

    /**
     * Invoked when the mouse enters a component.
     * <i>Not used.</i>
     * @param event The object providing details of the mouse event.
     */
    public void mouseEntered (final MouseEvent event)
    {
    }

    /**
     * Invoked when the mouse exits a component.
     * <i>Not used.</i>
     * @param event The object providing details of the mouse event.
     */
    public void mouseExited (final MouseEvent event)
    {
    }

    /**
     * Handle left click on a picture by bringing it to the top.
     * @param event The object providing details of the mouse event.
     */
    public void mousePressed (final MouseEvent event)
    {
        Picture picture;

        if (!event.isMetaDown ())
        {
            picture = mMosaic.pictureAt (event.getX (), event.getY ());
            if (null != picture)
                bringToTop (picture);
        }
    }

    //
    // Scrollable interface
    //

    /**
     * Returns the preferred size of the viewport for a view component.
     * For example the preferredSize of a JList component is the size
     * required to accommodate all of the cells in its list however the
     * value of preferredScrollableViewportSize is the size required for
     * JList.getVisibleRowCount() rows.   A component without any properties
     * that would effect the viewport size should just return
     * getPreferredSize() here.
     *
     * @return The preferredSize of a JViewport whose view is this Scrollable.
     * @see JViewport#getPreferredSize
     */
    public Dimension getPreferredScrollableViewportSize ()
    {
        return (getPreferredSize ());
    }


    /**
     * Components that display logical rows or columns should compute
     * the scroll increment that will completely expose one new row
     * or column, depending on the value of orientation.  Ideally,
     * components should handle a partially exposed row or column by
     * returning the distance required to completely expose the item.
     * <p>
     * Scrolling containers, like JScrollPane, will use this method
     * each time the user requests a unit scroll.
     *
     * @param visibleRect The view area visible within the viewport
     * @param orientation Either SwingConstants.VERTICAL or
     * SwingConstants.HORIZONTAL.
     * @param direction Less than zero to scroll up/left,
     * greater than zero for down/right.
     * @return The "unit" increment for scrolling in the specified direction.
     *         This value should always be positive.
     */
    public int getScrollableUnitIncrement (
        final Rectangle visibleRect,
        final int orientation,
        final int direction)
    {
        return (UNIT_INCREMENT);
    }


    /**
     * Components that display logical rows or columns should compute
     * the scroll increment that will completely expose one block
     * of rows or columns, depending on the value of orientation.
     * <p>
     * Scrolling containers, like JScrollPane, will use this method
     * each time the user requests a block scroll.
     *
     * @param visibleRect The view area visible within the viewport
     * @param orientation Either SwingConstants.VERTICAL or
     * SwingConstants.HORIZONTAL.
     * @param direction Less than zero to scroll up/left,
     * greater than zero for down/right.
     * @return The "block" increment for scrolling in the specified direction.
     *         This value should always be positive.
     */
    public int getScrollableBlockIncrement (
        final Rectangle visibleRect,
        final int orientation,
        final int direction)
    {
        return (BLOCK_INCREMENT);
    }


    /**
     * Return true if a viewport should always force the width of this
     * <code>Scrollable</code> to match the width of the viewport.
     * For example a normal
     * text view that supported line wrapping would return true here, since it
     * would be undesirable for wrapped lines to disappear beyond the right
     * edge of the viewport.  Note that returning true for a Scrollable
     * whose ancestor is a JScrollPane effectively disables horizontal
     * scrolling.
     * <p>
     * Scrolling containers, like JViewport, will use this method each
     * time they are validated.
     *
     * @return <code>true</code> if a viewport should force the Scrollables
     * width to match its own.
     */
    public boolean getScrollableTracksViewportWidth ()
    {
        return (false);
    }

    /**
     * Return true if a viewport should always force the height of this
     * Scrollable to match the height of the viewport.  For example a
     * columnar text view that flowed text in left to right columns
     * could effectively disable vertical scrolling by returning
     * true here.
     * <p>
     * Scrolling containers, like JViewport, will use this method each
     * time they are validated.
     *
     * @return <code>true</code> if a viewport should force the Scrollables
     * height to match its own.
     */
    public boolean getScrollableTracksViewportHeight ()
    {
        return (false);
    }

    //
    // ComponentListener interface
    //

    /**
     * Invoked when the container's size changes.
     * Un-caches the preferred size.
     * @param event The resize event.
     */
    public void componentResized (final ComponentEvent event)
    {
        setPreferredSize (null);
    }

    /**
     * Invoked when the component's position changes.
     * <i>Not used.</I>
     * @param event The component event.
     */
    public void componentMoved (final ComponentEvent event)
    {
    }

    /**
     * Invoked when the component has been made visible.
     * <i>Not used.</I>
     * @param event The component event.
     */
    public void componentShown (final ComponentEvent event)
    {
    }

    /**
     * Invoked when the component has been made invisible.
     * <i>Not used.</I>
     * @param event The component event.
     */
    public void componentHidden (final ComponentEvent event)
    {
    }

    //
    // HierarchyListener interface
    //

    /**
     * Handles this components ancestor being added to a container.
     * Registers this component as a listener for size changes on the
     * ancestor so that we may un-cache the prefereed size and force
     * a recalculation.
     * @param event The heirarchy event.
     */
    public void hierarchyChanged (final HierarchyEvent event)
    {
        if (0 != (event.getChangeFlags () & HierarchyEvent.PARENT_CHANGED))
        {
            Component dad = event.getChanged ();
            Component parent = getParent ();
            if ((null != parent) && (parent.getParent () == dad))
                dad.addComponentListener (this);
        }
    }
}

/*
 * Revision Control Modification History
 *
 * $Log: PicturePanel.java,v $
 * Revision 1.2  2005/04/12 11:27:41  derrickoswald
 * Documentation revamp part two.
 *
 * Revision 1.1  2003/09/21 18:20:56  derrickoswald
 * Thumbelina
 * Created a lexer GUI application to extract images behind thumbnails.
 * Added a task in the ant build script - thumbelina - to create the jar file.
 * You need JDK 1.4.x to build it.  It can be run on JDK 1.3.x in crippled mode.
 * Usage: java -Xmx256M thumbelina.jar [URL]
 *
 *
 */
