/*
 * @(#)PaintableComponentLite.java    2.4.1  12 November 2005
 *
 * Copyright 2005
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

import edu.neu.ccs.gui.*;

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import javax.swing.*;
import javax.swing.border.*;
import java.beans.*;

/**
 * <p>Class <code>PaintableComponentLite</code> encapsulates a
 * <code>Paintable</code> and uses that paintable to define the
 * <code>PaintComponent</code> method and to determine the size
 * of the component.</p>
 *
 * <p>This class also implements the <code>Icon</code> interface.</p>
 *
 * <p>This class calls <code>repaint()</code> when it detects a
 * property change in itself or in its paintable.  This is a
 * more light-weight action than calling <code>refresh()</code>
 * which forces the parent window to be repacked.  This is the
 * main distinction between this class and its relative
 * <code>PaintableComponent</code> which always does a complete
 * <code>refresh()</code>.</p>
 * 
 * <p>The effect of calling <code>repaint()</code> rather than
 * <code>refresh()</code> is that the component will not
 * automatically resize itself if its paintable resizes.  Thus,
 * it is the responsibility of the user of this class either to
 * avoid resizing the paintable or to call <code>refresh()</code>
 * manually if its paintable resizes.</p>
 * 
 * <p>Unlike <code>PaintableComponent</code>, this class does
 * not support a <code>setPaintable</code> method.  Hence, once
 * the paintable object has been set at construction, it cannot
 * be changed to a different paintable object.</p>
 * 
 * @author  Richard Rasala
 * @version 2.4.1
 * @since   2.4.1
 */

public class PaintableComponentLite
    extends JPTComponent
    implements Icon
{
    /** The encapsulated paintable that defines this component. */
    private Paintable paintable;
    
    /** The internal repaint listener. */
    private RepaintListener listener = new RepaintListener();
    
    
    /**
     * <p>This constructor provides the paintable needed to define the
     * component.</p>
     *
     * <p>Throws <code>NullPointerException</code> if the given paintable
     * is <code>null</code>.</p>
     *
     * @param paintable the paintable to encapsulate
     */
    public PaintableComponentLite(Paintable paintable) {
        this(paintable, null, false, null);
    }
    
    
    /**
     * <p>This constructor provides the paintable needed to define the
     * component,
     * and the optional background color.</p>
     *
     * <p>Throws <code>NullPointerException</code> if the given paintable
     * is <code>null</code>.</p>
     *
     * <p>Ignores the given color if it is <code>null</code>.</p>
     *
     * <p>The background will not be painted unless the opaque property is
     * also set to true.</p>
     *
     * @param paintable the paintable to encapsulate
     * @param color     the optional background color
     */
    public PaintableComponentLite(Paintable paintable, Color color) {
        this(paintable, color, false, null);
    }
    
    
    /**
     * <p>This constructor provides the paintable needed to define the
     * component,
     * the optional background color,
     * and the opacity setting.</p>
     *
     * <p>Throws <code>NullPointerException</code> if the given paintable
     * is <code>null</code>.</p>
     *
     * <p>Ignores the given color if it is <code>null</code>.</p>
     *
     * <p>If the given value of opaque is true, then the background color
     * is painted on the component area before painting the paintable.</p>
     *
     * @param paintable the paintable to encapsulate
     * @param color     the optional background color
     * @param opaque    whether or not to paint the component background
     */
    public PaintableComponentLite(Paintable paintable, Color color, boolean opaque) {
        this(paintable, color, opaque, null);
    }
    
    
    /**
     * <p>This constructor provides the paintable needed to define the
     * component,
     * the optional background color,
     * the opacity setting,
     * and the component border.</p>
     *
     * <p>Throws <code>NullPointerException</code> if the given paintable
     * is <code>null</code>.</p>
     *
     * <p>Ignores the given color if it is <code>null</code>.</p>
     *
     * <p>If the given value of opaque is true, then the background color
     * is painted on the component area before painting the paintable.</p>
     *
     * <p>Ignores the given border if it is <code>null</code>.</p>
     *
     * @param paintable the paintable to encapsulate
     * @param color     the optional background color
     * @param opaque    whether or not to paint the component background
     * @param border    the border for the component
     */
    public PaintableComponentLite
        (Paintable paintable, Color color, boolean opaque, Border border)
    {
        if (paintable == null)
            throw new NullPointerException
                ("Null Paintable passed to PaintableComponentLite constructor");
        
        this.paintable = paintable;
        
        if (color != null)
            setBackground(color);
        
        setOpaque(opaque);
        
        if (border != null)
            setBorder(border);
        
        refresh();
        
        addComponentRepaintListener();
    }
    
    
    /**
     * <p>Returns the encapsulated paintable.</p>
     *
     * @return the encapsulated paintable
     */
    public Paintable getPaintable() {
        return paintable;
    }
    
    
    /**
     * <p>Returns the default bounds for the component using
     * the information in the encapsulated paintable
     * and the insets if any.</p>
     *
     * <p>A LayoutManager may change the actual bounds of
     * the component in a GUI to satisfy layout decisions.</p>
     *
     * @return the default bounds for the component
     */
    public Rectangle2D getDefaultBounds2D()
    {
        Rectangle2D bounds = paintable.getBounds2D();
        
        int x = (int) bounds.getMinX();
        int y = (int) bounds.getMinY();
        int w = (int) bounds.getWidth();
        int h = (int) bounds.getHeight();
        
        Insets insets = getInsets();
        
        if (insets != null) {
            x -= insets.left;
            y -= insets.top;
            w += insets.left + insets.right;
            h += insets.top + insets.bottom;
        }
        
        bounds.setRect(x, y, w, h);
        
        return bounds;
    }
    
    
    /**
     * <p>Returns the default location for the component
     * using the information in the encapsulated paintable
     * and the insets if any.</p>
     *
     * <p>A LayoutManager may change the actual location of
     * the component in a GUI to satisfy layout decisions.</p>
     *
     * @return the default location for the component
     */
    public Point getDefaultLocation()
    {
        Rectangle2D bounds = getDefaultBounds2D();
        
        int x = (int) bounds.getMinX();
        int y = (int) bounds.getMinY();
        
        return new Point(x, y);
    }
    
    
    /**
     * <p>Returns the preferred size of the component based on the bounds
     * of the encapsulated paintable and the component insets if any.</p>
     *
     * @return the preferred size
     */
    public Dimension getPreferredSize() {
        Rectangle2D bounds = getDefaultBounds2D();
        
        int w = (int) bounds.getWidth();
        int h = (int) bounds.getHeight();
        
        return new Dimension(w, h);
    }
    
    
    /**
     * <p>Returns the same dimension as <code>getPreferredSize</code>.</p>
     *
     * @return the minimum size
     */
    public Dimension getMinimumSize() {
        return getPreferredSize();
    }
    
    
    /**
     * <p>Returns the same dimension as <code>getPreferredSize</code>.</p>
     *
     * @return the maximum size
     */
    public Dimension getMaximumSize() {
        return getPreferredSize();
    }
    
    
    /**
     * <p>Returns the same dimension as <code>getPreferredSize</code>.</p>
     *
     * @return the size
     */
    public Dimension getSize() {
        return getPreferredSize();
    }
    
    
    /**
     * <p>Returns the bounds for the component viewed as an icon
     * using the information in the encapsulated paintable.</p>
     *
     * <p>These bounds ignore the insets of the object viewed as
     * a component since the border is not painted in the icon.</p>
     *
     * @return the icon bounds for the component
     */
    public Rectangle2D getIconBounds2D()
    {
        return paintable.getBounds2D();
    }
    
    
    /**
     * <p>Returns the icon width which is the width of the bounds
     * region of the encapsulated paintable.</p>
     *
     * <p>This width ignores the insets of the object viewed as a
     * component since the border is not painted in the icon.</p>
     *
     * @return the icon width
     */
    public int getIconWidth() {
        return (int) getIconBounds2D().getWidth();
    }
    
    
    /**
     * <p>Returns the icon height which is the height of the bounds
     * region of the encapsulated paintable.</p>
     *
     * <p>This height ignores the insets of the object viewed as a
     * component since the border is not painted in the icon.</p>
     *
     * @return the icon height
     */
    public int getIconHeight() {
        return (int) getIconBounds2D().getHeight();
    }
    
    
    /**
     * <p>Draw this object as an icon at the specified location
     * using the paintable to paint the icon details.</p>
     *
     * <p>If the component parameter c is non-<code>null</code>,
     * if it is set to opaque, and if its background color is
     * non-<code>null</code>, then the background color will be
     * painted before the icon is painted.</p>
     *
     * <p>Does nothing if the graphics context parameter g is
     * <code>null</code>.</p>
     *
     * @param c the component on which the icon will be painted
     * @param g the graphics context on which to paint
     * @param x the x-location
     * @param y the y-location
     */
    public void paintIcon(Component c, Graphics g, int x, int y) {
        if (g == null)
            return;
        
        Graphics2D h = (Graphics2D)g;
        Paint older = h.getPaint();
        
        Rectangle2D bounds = paintable.getBounds2D();
        
        int x0 = x - (int) bounds.getMinX();
        int y0 = y - (int) bounds.getMinY();
        
        h.translate( x0,  y0);
        
        if (c != null) {
            if (c.isOpaque()) {
                Color color = c.getBackground();
                
                if (color != null) {
                    h.setPaint(color);
                    h.fill(bounds);
                }
            }
        }
        
        paintable.paint(h);
        
        h.setPaint(older);
        h.translate(-x0, -y0);
    }
    
    
    /**
     * <p>Overrides the inherited <code>paintComponent</code> method to
     * paint the background if the component is set to be opaque and
     * then to use the paintable to paint the component details.</p>
     *
     * <p>Note: The javadocs for <code>JComponent</code> claim that the
     * the component background is painted if the opaque setting is true.
     * This is not always the case in practice.  In this class, we take
     * care to see that the opaque setting is always respected.</p>
     *
     * @param g the graphics context of the component that will be copied
     *          prior to any paint operations
     */
    protected void paintComponent(Graphics g) {
        if (g == null)
            return;
        
        int x = 0;
        int y = 0;
        
        Insets insets = getInsets();
        
        if (insets != null) {
            x = insets.left;
            y = insets.top;
        }
        
        paintIcon(this, g, x, y);
    }
    
    
    /**
     * <p>Sets the component location and size using the information
     * in the given paintable and
     * then refreshes the component by repacking the parent window.</p>
     */
    public void refresh()
    {
        setSize(getPreferredSize());
        setLocation(getDefaultLocation());
        
        refreshComponent();
    }
    
    
    /**
     * <p>Adds a refresh listener for:</p>
     * <ul>
     *   <li>property changes of the component</li>
     *   <li>property changes of the paintable</li>
     * </ul>
     */
    private void addComponentRepaintListener()
    {
        addPropertyChangeListener(listener);
        paintable.addPropertyChangeListener(listener);
    }
    
    
    /** <p>The internal repaint listener class.</p> */
    private class RepaintListener extends SimpleAction {
        public void perform() {
            repaint();
        }
    }
}
