/*
 * @(#)PaintableButton.java    2.3  3 September 2004
 *
 * Copyright 2004
 * 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.
 */

package edu.neu.ccs.gui;

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>PaintableButton</code> encapsulates a
 * <code>Paintable</code> and uses that paintable to define the
 * icon for the button and to determine the size of the button.</p>
 *
 * @author  Richard Rasala
 * @version 2.3.2
 * @since   2.3.2
 */

public class PaintableButton
    extends JButton
{
    /** Bound property name for set paintable. */
    public static final String SET_PAINTABLE  = "set.paintable";
        
    /** The default button insets if not provided. */
    private static final Insets DEFAULT_INSETS = new Insets(2, 2, 2, 2);
    
    /** The encapsulated paintable that defines this button. */
    private Paintable paintable = null;
    
    /** The icon associated with the encapsulated paintable. */
    private Icon icon = null;
    
    /** The internal refresh listener. */
    private RefreshListener listener = new RefreshListener();
    
    
    /**
     * <p>This constructor provides the paintable needed to define the
     * button.</p>
     *
     * <p>Throws <code>NullPointerException</code> if the given paintable
     * is <code>null</code>.</p>
     *
     * <p>The button behavior must be supplied later.</p>
     *
     * <p>This button uses the default JButton background color.</p>
     *
     * <p>This button uses default insets of 2, 2, 2, 2 which are not the
     * Java default but which work better with most paintable objects.</p>
     *
     * @param paintable the paintable to encapsulate
     * @see #PaintableButton(Paintable, Action)
     * @see #PaintableButton(Paintable, Action, Color)
     * @see #PaintableButton(Paintable, Action, Color, Insets)
     */
    public PaintableButton
        (Paintable paintable)
    {
        this(paintable, null, null, null);
    }
    
    
    /**
     * <p>This constructor provides the paintable needed to define the
     * button,
     * and the action for the button behavior.</p>
     *
     * <p>Throws <code>NullPointerException</code> if the given paintable
     * is <code>null</code>.</p>
     *
     * <p>Determines the button icon from the paintable and ignores any
     * NAME or SMALL_ICON properties that may be set for the action.</p>
     *
     * <p>Ignores the given action if it is <code>null</code>.</p>
     *
     * <p>This button uses the default JButton background color.</p>
     *
     * <p>This button uses default insets of 2, 2, 2, 2 which are not the
     * Java default but which work better with most paintable objects.</p>
     *
     * @param paintable the paintable to encapsulate
     * @param action    the action for the button behavior
     * @see #PaintableButton(Paintable)
     * @see #PaintableButton(Paintable, Action, Color)
     * @see #PaintableButton(Paintable, Action, Color, Insets)
     */
    public PaintableButton
        (Paintable paintable, Action action)
    {
        this(paintable, action, null, null);
    }
    
    
    /**
     * <p>This constructor provides the paintable needed to define the
     * button,
     * the action for the button behavior
     * and the optional background color.</p>
     *
     * <p>Throws <code>NullPointerException</code> if the given paintable
     * is <code>null</code>.</p>
     *
     * <p>Determines the button icon from the paintable and ignores any
     * NAME or SMALL_ICON properties that may be set for the action.</p>
     *
     * <p>Ignores the given action if it is <code>null</code>.</p>
     *
     * <p>Ignores the given color if it is <code>null</code>.</p>
     *
     * <p>This button uses default insets of 2, 2, 2, 2 which are not the
     * Java default but which work better with most paintable objects.</p>
     *
     * @param paintable the paintable to encapsulate
     * @param action    the action for the button behavior
     * @param color     the optional background color
     * @see #PaintableButton(Paintable)
     * @see #PaintableButton(Paintable, Action)
     * @see #PaintableButton(Paintable, Action, Color, Insets)
     */
    public PaintableButton
        (Paintable paintable, Action action, Color color)
    {
        this(paintable, action, color, null);
    }
    
    
    /**
     * <p>This constructor provides the paintable needed to define the
     * button,
     * the action for the button behavior
     * the optional background color,
     * and the button insets (margin).</p>
     *
     * <p>Throws <code>NullPointerException</code> if the given paintable
     * is <code>null</code>.</p>
     *
     * <p>Determines the button icon from the paintable and ignores any
     * NAME or SMALL_ICON properties that may be set for the action.</p>
     *
     * <p>Ignores the given action if it is <code>null</code>.</p>
     *
     * <p>Ignores the given color if it is <code>null</code>.</p>
     *
     * <p>If the given insets is <code>null</code>, it is set to 2, 2, 2, 2.</p>
     *
     * @param paintable the paintable to encapsulate
     * @param action    the action for the button behavior
     * @param color     the optional background color
     * @param insets    the button insets or margin 
     * @see #PaintableButton(Paintable)
     * @see #PaintableButton(Paintable, Action)
     * @see #PaintableButton(Paintable, Action, Color)
     */
    public PaintableButton
        (Paintable paintable, Action action, Color color, Insets insets)
    {
        if (paintable == null)
            throw new NullPointerException
                ("Null Paintable passed to PaintableButton constructor");
        
        setPaintable(paintable);
        
        if (action != null)
            setAction(action);
        
        if (color != null)
            setBackground(color);
        
        if (insets == null)
            insets = DEFAULT_INSETS;
        
        setMargin(insets);
        
        refresh();
        
        addComponentRefreshListener();
    }
    
    
    /**
     * <p>Sets the encapsulated paintable to the given paintable and refreshes
     * the screen provided the button is installed in a GUI.</p>
     *
     * <p>Does nothing if the given paintable is <code>null</code> or if it is
     * the same object as the encapsulated paintable.</p>
     *
     * <P>Fires property change: SET_PAINTABLE.</P>
     * 
     * @param paintable the paintable to encapsulate
     */
    public void setPaintable(Paintable paintable) {
        if ((paintable == null) || (paintable == this.paintable))
            return;
        
        this.paintable = paintable;
        this.icon = new PaintableComponent(paintable);
        
        setIcon(icon);
        
        firePropertyChange(SET_PAINTABLE, null, null);
    }
    
    
    /**
     * <p>Returns the encapsulated paintable.</p>
     *
     * @return the encapsulated paintable
     */
    public Paintable getPaintable() {
        return paintable;
    }
    
    
    /**
     * <p>Overrides the inherited method to make sure that the icon for the
     * button is the one associated with the paintable and the text for the
     * button is <code>null</code>.</p>
     *
     * @param action the action for the button, or null
     */
    public void setAction(Action a) {
        super.setAction(a);
        setText(null);
        setIcon(icon);
    }
    
    
    /**
     * <p>Returns the default bounds for the button using
     * the information in the encapsulated paintable
     * and the insets.</p>
     *
     * <p>A LayoutManager may change the actual bounds of
     * the button in a GUI to satisfy layout decisions.</p>
     *
     * @return the default bounds for the button
     */
    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 button
     * using the information in the encapsulated paintable
     * and the insets.</p>
     *
     * <p>A LayoutManager may change the actual location of
     * the button in a GUI to satisfy layout decisions.</p>
     *
     * @return the default location for the button
     */
    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 button based on the bounds
     * of the encapsulated paintable and the button insets.</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>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()
    {
        setDefaultSize();
        setDefaultLocation();
        
        refreshComponent();
    }
    
    
    /** Refreshes the component by repacking the parent window. */
    public void refreshComponent() {
        Refresh.packParentWindow(this);
    }
    
    
    /**
     * <p>Adds a refresh listener for:</p>
     * <ul>
     *   <li>ordinary and property changes of the button</li>
     *   <li>property changes of the paintable if possible</li>
     * </ul>
     */
    private void addComponentRefreshListener()
    {
        addChangeListener(listener);
        addPropertyChangeListener(listener);
        
        if (paintable instanceof SupportsPropertyChange) {
            SupportsPropertyChange producer = (SupportsPropertyChange) paintable;
            producer.addPropertyChangeListener(listener);
        }
    }
    
    
    /**
     * <p>Sets the button size using the preferred size.</p>
     */
    private void setDefaultSize()
    {
        setSize(getPreferredSize());
    }
    
    
    /**
     * <p>Sets the button location using the default button location.</p>
     */
    private void setDefaultLocation()
    {
        setLocation(getDefaultLocation());
    }
    
    
    /** <p>The internal refresh listener class.</p> */
    private class RefreshListener extends SimpleAction {
        public void perform() {
            refresh();
        }
    }
    
}
