/*******************************************************************************
 * Copyright (c) 2000, 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.draw2d;

import java.util.Iterator;

import org.eclipse.draw2d.geometry.Rectangle;

/**
 * A Clickable responds to mouse clicks in some way (determined by a ClickBehavior) and
 * fires action events. No visual appearance of feedback is offered. Depends on a model
 * holder and an event handler which understands the model and updates the model
 * accordingly. {@link ButtonModel} is used by default. Any figure can be set as contents
 * to a Clickable. Clickable->EventHandler->Model->ModelObserver->Listeners of actions.
 */
public class Clickable
	extends Figure
{

private static final int
	ROLLOVER_ENABLED_FLAG = Figure.MAX_FLAG << 1,
	STYLE_BUTTON_FLAG = Figure.MAX_FLAG << 2,
	STYLE_TOGGLE_FLAG = Figure.MAX_FLAG << 3;

/**
 * The highest reserved flag used by this class
 */
protected static int
	MAX_FLAG = STYLE_TOGGLE_FLAG;

/**
 * Style constant that defines a push button. The button will be pressed when the mouse is
 * pressed on top of it. The button will be released when the mouse is released or is move
 * off of the button.
 * 
 */
public static final int STYLE_BUTTON = STYLE_BUTTON_FLAG;

/**
 * Style constant that defines a toggle button. The button will toggle between 2 states
 * when the mouse is clicked on the button. 
 */
public static final int STYLE_TOGGLE = STYLE_TOGGLE_FLAG;

/**
 * An action is performed every time the mouse is released.
 */
public static final int DEFAULT_FIRING = 0;

/**
 * Firing starts as soon as the mouse is pressed on this Clickable, and keeps firing at
 * prefixed intervals until the mouse is released.
 */
public static final int REPEAT_FIRING  = 1;

/**
 * Observes the model for action and state changes.
 *
 * @see ActionListener
 * @see ChangeListener 
 */
static interface ModelObserver
	extends ActionListener, ChangeListener
{ }

private ClickableEventHandler eventHandler;

private ButtonModel model;

private ModelObserver modelObserver;

{
	init();
	setRequestFocusEnabled(true);
	setFocusTraversable(true);
}

/**
 * Constructs a Clickable with no contents.
 */
public Clickable() { }

/**
 * Constructs a Clickable whose contents are provided as input. The content figure
 * occupies the entire region of the Clickable.
 * 
 * @param contents The content figure
 */
public Clickable(IFigure contents) {
	this(contents, 0);
}

/**
 * Constructs a Clickable whose contents are provided as input. The content figure
 * occupies the entire region of the Clickable.  Sets the style to the given <i>style</i>
 * (either {@link #STYLE_BUTTON} or {@link #STYLE_TOGGLE}).
 * 
 * @param contents The content figure
 * @param style The button style
 */
public Clickable(IFigure contents, int style) {
	setContents(contents);
	setStyle(style);
}

/**
 * Adds the given listener to the list of action listeners of this Figure. Listener is
 * called whenever an action is performed.
 *
 * @param listener The ActionListener to be added
 * @since 2.0
 */
public void addActionListener(ActionListener listener) {
	addListener(ActionListener.class, listener);
}

/**
 * Adds the given listener to the list of state change listeners of this figure. A
 * ChangeListener is informed if there is any state change in the model requiring action
 * by the listener. 
 *
 * @param listener The ChangeListener to be added
 * @since 2.0
 */
public void addChangeListener(ChangeListener listener) {
	addListener(ChangeListener.class, listener);
}

/**
 * Returns a newly created ButtonModel as the default model to be used by this Clickable
 * based on the button style.
 *
 * @return The model to be used by default
 * @since 2.0
 */
protected ButtonModel createDefaultModel() {
	if (isStyle(STYLE_TOGGLE))
		return new ToggleModel();
	else
		return new ButtonModel();
}

/**
 * Returns a newly created event handler for this Clickable and its model.
 *
 * @return The event handler
 * @since 2.0
 */
protected ClickableEventHandler createEventHandler() {
	return new ClickableEventHandler();
}

/**
 * Returns a newly created model observer which listens to the model, and fires any action
 * or state changes. A ModelObserver holds both an action listener and a state change
 * listener.
 *
 * @return  The newly created model observer
 * @since 2.0
 */
protected ModelObserver createModelObserver() {
	return new ModelObserver() {
		public void actionPerformed(ActionEvent action) {
			fireActionPerformed();
		}
		public void handleStateChanged(ChangeEvent change) {
			fireStateChanged(change);
		}
	};
}

/**
 * Fires an action performed event.
 * 
 * @since 2.0
 */
public void doClick() {
	fireActionPerformed();
}

/**
 * Called when there has been an action performed by this Clickable, which is determined
 * by the model. Notifies all ActionListener type listeners of an action performed.
 * 
 * @since 2.0
 */
protected void fireActionPerformed() {
	ActionEvent action = new ActionEvent(this);
	Iterator listeners = getListeners(ActionListener.class);
	while (listeners.hasNext())
		((ActionListener)listeners.next()) //Leave newline for debug stepping
			.actionPerformed(action); 
}

/**
 * Called when there has been a change of state in the model of this clickable. Notifies
 * all ChangeListener type listeners  of the state change.
 * 
 * @param modelChange The ChangeEvent
 * @since 2.0
 */
protected void fireStateChanged(ChangeEvent modelChange) {
	ChangeEvent change = new ChangeEvent(this, modelChange.getPropertyName());
	Iterator listeners = getListeners(ChangeListener.class);
	while (listeners.hasNext())
		((ChangeListener)listeners.next())  //Leave newline for debug stepping
			.handleStateChanged(change);
}

/**
 * Returns the behavior model used by this Clickable.
 *
 * @return  The model used by this Clickable
 * @since 2.0
 */
public ButtonModel getModel() {
	return model;
}

/**
 * Adds the given ClickableEventHandler to this clickable. A ClickableEventHandler
 * should be a MouseListener, MouseMotionListener, ChangeListener, KeyListener,  and
 * FocusListener.
 *
 * @param handler  The new event handler
 * @since 2.0
 */
protected void hookEventHandler(ClickableEventHandler handler) {
	if (handler == null) 
		return;
	addMouseListener(handler);
	addMouseMotionListener(handler);
	addChangeListener(handler);
	addKeyListener(handler);
	addFocusListener(handler);
}

/**
 * Initializes this Clickable by setting a default model and adding a clickable event
 * handler for that model.
 * 
 * @since 2.0
 */
protected void init() {
	setModel(createDefaultModel());
	setEventHandler(createEventHandler());
}

/**
 * Returns <code>true</code> if rollover feedback is enabled.
 * 
 * @return <code>true</code> rollover feedback is enabled
 * @since 2.0
 */
public boolean isRolloverEnabled() {
	return (flags & ROLLOVER_ENABLED_FLAG) != 0;
}

/**
 * Returns <code>true</code> if this Clickable is in a selected state. The model is the
 * one which holds all this state based information.
 *
 * @return <code>true</code> if this Clickable is in a selected state
 * @since 2.0
 */
public boolean isSelected() {
	return getModel().isSelected();
}

/**
 * Returns <code>true</code> if this Clickable's style is the same as the passed style.
 * 
 * @param style The style to be checked
 * @return <code>true</code> if this Clickable's style is the same as the passed style
 * @since 2.0
 */
public boolean isStyle(int style) {
	return ((style & flags) == style);
}

/**
 * If this Clickable has focus, this method paints a focus rectangle.
 * 
 * @param graphics Graphics handle for painting
 */
protected void paintBorder(Graphics graphics) {
	super.paintBorder(graphics);
	if (hasFocus()) {
		graphics.setForegroundColor(ColorConstants.black);
		graphics.setBackgroundColor(ColorConstants.white);

		Rectangle area = getClientArea();
		if (isStyle(STYLE_BUTTON))
			graphics.drawFocus(area.x, area.y, area.width, area.height);
		else
			graphics.drawFocus(area.x, area.y, area.width - 1, area.height - 1);
	}
}

/**
 * Paints the area of this figure excluded by the borders. Induces a (1,1) pixel shift in
 * the painting if the  mouse is armed, giving it the pressed appearance. 
 *
 * @param graphics  Graphics handle for painting
 * @since 2.0
 */
protected void paintClientArea(Graphics graphics) {
	if (isStyle(STYLE_BUTTON) && (getModel().isArmed() || getModel().isSelected())) {
		graphics.translate(1, 1);
		graphics.pushState();
		super.paintClientArea(graphics);
		graphics.popState();
		graphics.translate(-1, -1);
	} else 
		super.paintClientArea(graphics);
}

/**
 * Removes the given listener from the list of ActionListener's of this Clickable.
 * 
 * @param listener Listener to be removed from this figure
 * @since 2.0
 */
public void removeActionListener(ActionListener listener) {
	removeListener(ActionListener.class, listener);
}

/**
 * Removes the given listener from the list of ChangeListener's of this clickable.
 * 
 * @param listener Listener to be removed from this figure
 * @since 2.0
 */
public void removeChangeListener(ChangeListener listener) {
	removeListener(ChangeListener.class, listener);
}

/**
 * Sets the Figure which is the contents of this Clickable. This Figure occupies the
 * entire clickable region.
 *
 * @param contents Contents of the clickable
 * @since 2.0
 */
protected void setContents(IFigure contents) {
	setLayoutManager(new StackLayout());
	if (getChildren().size() > 0)
		remove((IFigure)getChildren().get(0));
	add(contents);
}

/**
 * @see org.eclipse.draw2d.IFigure#setEnabled(boolean)
 */
public void setEnabled(boolean value) {
	if (isEnabled() == value) 
		return;
	super.setEnabled(value);
	getModel().setEnabled(value);
	setChildrenEnabled(value);
}

/**
 * Sets the event handler which interacts with the model to determine the behavior of this
 * Clickable.
 *
 * @param h  Event handler for this clickable
 * @since 2.0
 */
public void setEventHandler(ClickableEventHandler h) {
	if (eventHandler != null) 
		unhookEventHandler(eventHandler);
	eventHandler = h;
	if (eventHandler != null) 
		hookEventHandler(eventHandler);
}

/**
 * Determines how this clickable is to fire notifications to its listeners. In the default
 * firing method ({@link #DEFAULT_FIRING}), an action is performed  every time the mouse
 * is released. In the repeat firing method ({@link #REPEAT_FIRING}), firing starts as
 * soon as it is pressed on this clickable, and keeps firing at prefixed intervals until
 * the mouse is released.
 *
 * @param type  Type of firing
 * @since 2.0
 */
public void setFiringMethod(int type) {
	getModel().setFiringBehavior(type);
}

/**
 * Sets the model to be used by this clickable for its state and behavior determination.
 * This clickable removes any observers from the previous model before adding new ones to
 * the new model.
 *
 * @param model The new model of this Clickable
 * @since 2.0
 */
public void setModel(ButtonModel model) {
	if (this.model != null) {
		this.model.removeChangeListener(modelObserver);
		this.model.removeActionListener(modelObserver);
		modelObserver = null;
	}
	this.model = model;
	if (model != null) {
		modelObserver = createModelObserver();
		model.addActionListener(modelObserver);
		model.addChangeListener(modelObserver);
	}
}

/**
 * Enables or disables rollover feedback of this figure. Generally used in conjunction
 * with the model to determine if feedback is to be shown. 
 *
 * @param value The rollover state to be set
 * @since 2.0
 */
public void setRolloverEnabled(boolean value) {
	if (isRolloverEnabled() == value)
		return;
	setFlag(ROLLOVER_ENABLED_FLAG, value);
	repaint();
}

/**
 * Sets the selected state of this Clickable. Since the model is responsible for all state
 * based information, it is informed of the state change. Extending classes can choose
 * selection information, if they do not represent any selection.
 *
 * @param value  New selected state of this clickable.
 * @see  #isSelected()
 * @since 2.0
 */
public void setSelected(boolean value) {
	getModel().setSelected(value);
}

/**
 * Sets this Clickable's style to the passed value, either {@link #STYLE_BUTTON} or 
 * {@link #STYLE_TOGGLE}.
 *
 * @param style The button style 
 * @since 2.0
 */
public void setStyle(int style) {
	if ((style & STYLE_BUTTON) != 0) {
		setFlag(STYLE_BUTTON_FLAG, true);
		if (!(getBorder() instanceof ButtonBorder))
			setBorder(new ButtonBorder());
		setOpaque(true);
	} else {
		setFlag(STYLE_BUTTON_FLAG, false);
		setOpaque(false);
	}

	if ((style & STYLE_TOGGLE) != 0) {
		setFlag(STYLE_TOGGLE_FLAG, true);
		setModel(createDefaultModel());
	}
}

/**
 * Removes the given ClickableEventHandler containing listeners from this Clickable.
 *
 * @param handler The event handler to be removed
 * @since 2.0
 */
protected void unhookEventHandler(ClickableEventHandler handler) {
	if (handler == null) 
		return;
	removeMouseListener(handler);
	removeMouseMotionListener(handler);
	removeChangeListener(handler);
}

}
