/*
 * @(#)PaintableButton.java    2.5.0  2 May 2006
 *
 * Copyright 2006
 * 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>
 *
 * <p>As of 2.5.0, the built-in listener that listens for changes
 * to the paintable or button will normally repaint() but will
 * do a full refresh() if it detects that the bounds of the
 * internal paintable have changed.</p>
 * 
 * @author  Richard Rasala
 * @version 2.5.0
 * @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 paintable component associated with the paintable.
     * 
     * This object satisfies the Icon interface.
     */
    private PaintableComponent component = null;
    
    /** The current default bounds of the component. */
    private Rectangle2D bounds = new Rectangle2D.Double(0, 0, 0, 0);
    
    /** 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>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 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 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 or null
     * @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");
        
        this.paintable = paintable;
        this.component = new PaintableComponent(paintable);
        
        // setAction will also set component as the icon
        setAction(action);
        
        if (color != null)
            setBackground(color);
        
        if (insets == null)
            insets = DEFAULT_INSETS;
        
        setMargin(insets);
        
        refresh();
        
        addChangeListener(listener);
        addPropertyChangeListener(listener);
        this.paintable.addPropertyChangeListener(listener);
    }
    
    
    /**
     * <p>Sets the encapsulated paintable to the given paintable and
     * refreshes the screen.</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.removePropertyChangeListener(listener);
        
        this.paintable = paintable;
        this.component = new PaintableComponent(paintable);
        
        setIcon(component);
        
        this.paintable.addPropertyChangeListener(listener);
        
        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 component associated with the paintable
     * and that the text for the button is <code>null</code>.</p>
     *
     * @param action the action for the button behavior or null
     */
    public void setAction(Action action) {
        super.setAction(action);
        setText(null);
        setIcon(component);
    }
    
    
    /**
     * <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 rect = paintable.getBounds2D();
        
        double x = rect.getMinX();
        double y = rect.getMinY();
        double w = rect.getWidth();
        double h = rect.getHeight();
        
        Insets insets = getInsets();
        
        if (insets != null) {
            x -= insets.left;
            y -= insets.top;
            w += insets.left + insets.right;
            h += insets.top + insets.bottom;
        }
        
        rect.setRect(x, y, w, h);
        
        return rect;
    }
    
    
    /**
     * <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 Point2D getDefaultLocation()
    {
        Rectangle2D rect = getDefaultBounds2D();
        
        double x = rect.getMinX();
        double y = rect.getMinY();
        
        return new Point2D.Double(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 rect = getDefaultBounds2D();
        
        int w = (int) rect.getWidth();
        int h = (int) rect.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>If the default bounds rectangle of this button has
     * not changed then calls <code>repaint()</code>;
     * otherwise sets the button location and size using the
     * default bounds rectangle and refreshes the button by
     * repacking the parent window.</p>
     */
    public void refresh()
    {
        Rectangle2D rect = getDefaultBounds2D();
        
        boolean no_bounds_change =
            (bounds.getMinX() == rect.getMinX())
            && (bounds.getMinY() == rect.getMinY())
            && (bounds.getMaxX() == rect.getMaxX())
            && (bounds.getMaxY() == rect.getMaxY());
        
        if (no_bounds_change) {
            repaint();
        }
        else {
            bounds = rect;
            
            int x = (int) rect.getMinX();
            int y = (int) rect.getMinY();
            
            int w = (int) rect.getWidth();
            int h = (int) rect.getHeight();
            
            setSize(w, h);
            setLocation(x, y);
            
            refreshComponent();
        }
    }
    
    
    /** Refreshes the component by repacking the parent window. */
    public void refreshComponent() {
        Refresh.packParentWindow(this);
    }
    
    
    /** <p>The internal refresh listener class.</p> */
    private class RefreshListener extends SimpleAction {
        public void perform() {
            refresh();
        }
    }
    
}
