/*******************************************************************************
* Copyright (c) 2006 Chris Gross. 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: schtoo@schtoo.com(Chris Gross) - initial API and implementation
******************************************************************************/
package org.eclipse.nebula.widgets.pshelf;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.TypedListener;
import java.util.ArrayList;
import java.util.Iterator;
/**
* <p>
* NOTE: THIS WIDGET AND ITS API ARE STILL UNDER DEVELOPMENT. THIS IS A PRE-RELEASE ALPHA
* VERSION. USERS SHOULD EXPECT API CHANGES IN FUTURE VERSIONS.
* </p>
*
* Instances of this class implement a selectable accordion metaphor, where each shelf contains
* a client area.
* <p>
* The item children that may be added to instances of this class
* must be of type <code>PShelfItem</code>.
* <code>Control</code> children are created on the body composite of each items accessed via
* <code>PShelfItem#getBody</code>.
* </p><p>
* <dl>
* <dt><b>Styles:</b></dt>
* <dd>BORDER, SIMPLE</dd>
* <dt><b>Events:</b></dt>
* <dd>Selection</dd>
* </dl>
* <p>
* IMPORTANT: This class is <em>not</em> intended to be subclassed.
* </p>
*/
public class PShelf extends Canvas {
private ArrayList items = new ArrayList();
private AbstractRenderer renderer;
private PShelfItem openItem;
private PShelfItem focusItem;
private PShelfItem mouseDownItem;
private PShelfItem hoverItem;
private int itemHeight = 0;
private ArrayList yCoordinates = new ArrayList();
private static int checkStyle(int style)
{
int mask = SWT.LEFT_TO_RIGHT | SWT.RIGHT_TO_LEFT | SWT.BORDER | SWT.SIMPLE;
return (style & mask) | SWT.DOUBLE_BUFFERED;
}
/**
* Constructs a new instance of this class given its parent
* and a style value describing its behavior and appearance.
* <p>
* The style value is either one of the style constants defined in
* class <code>SWT</code> which is applicable to instances of this
* class, or must be built by <em>bitwise OR</em>'ing together
* (that is, using the <code>int</code> "|" operator) two or more
* of those <code>SWT</code> style constants. The class description
* lists the style constants that are applicable to the class.
* Style bits are also inherited from superclasses.
* </p>
*
* @param parent a composite control which will be the parent of the new instance (cannot be null)
* @param style the style of control to construct
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the parent is null</li>
* </ul>
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the parent</li>
* <li>ERROR_INVALID_SUBCLASS - if this class is not an allowed subclass</li>
* </ul>
*/
public PShelf(Composite parent, int style) {
super(parent,checkStyle(style));
setRenderer(new PaletteShelfRenderer());
this.addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
onPaint(e.gc);
}
});
this.addListener(SWT.Resize, new Listener()
{
public void handleEvent(Event event)
{
onResize();
}
});
this.addMouseListener(new MouseListener() {
public void mouseUp(MouseEvent e) {
PShelfItem item = getItem(new Point(1,e.y));
if ((item) == null)
return;
if (item == mouseDownItem && item != openItem){
openItem(item,true);
}
}
public void mouseDown(MouseEvent e) {
mouseDownItem = getItem(new Point(1,e.y));
}
public void mouseDoubleClick(MouseEvent arg0) {
}
});
this.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent arg0) {
onDispose();
}
});
this.addMouseTrackListener(new MouseTrackListener() {
public void mouseHover(MouseEvent arg0) {
}
public void mouseExit(MouseEvent arg0) {
hoverItem = null;
redraw();
}
public void mouseEnter(MouseEvent arg0) {
}
});
this.addMouseMoveListener(new MouseMoveListener(){
public void mouseMove(MouseEvent e) {
PShelfItem item = getItem(new Point(1,e.y));
if (item != hoverItem){
hoverItem = item;
redraw();
}
}});
}
/**
* Sets the renderer.
*
* @param renderer the new renderer
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the renderer is null</li>
* </ul>
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver or the renderer has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*/
public void setRenderer(AbstractRenderer renderer)
{
checkWidget();
if (renderer == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
if (renderer.isDisposed())
SWT.error(SWT.ERROR_WIDGET_DISPOSED);
if (this.renderer != null)
this.renderer.dispose();
this.renderer = renderer;
renderer.initialize(this);
computeItemHeight();
onResize();
redraw();
}
/**
* Returns the renderer.
*
* @return the renderer
*
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*/
public AbstractRenderer getRenderer()
{
checkWidget();
return renderer;
}
/**
* {@inheritDoc}
*/
public Point computeSize(int wHint, int hHint, boolean changed)
{
checkWidget();
Point size = new Point(wHint,hHint);
if (wHint == SWT.DEFAULT || hHint == SWT.DEFAULT)
{
if (openItem != null)
{
Point prefSize = openItem.getBody().computeSize(SWT.DEFAULT,SWT.DEFAULT);
if (wHint == SWT.DEFAULT)
size.x = prefSize.x;
if (hHint == SWT.DEFAULT)
{
size.y = prefSize.y + (items.size() * itemHeight);
}
}
else
{
return super.computeSize(wHint,hHint,changed);
}
}
return size;
}
private void onDispose() {
renderer.dispose();
}
private void onPaint(GC gc) {
gc.setAdvanced(true);
if (gc.getAdvanced())
gc.setTextAntialias(SWT.ON);
Color back = getBackground();
Color fore = getForeground();
int index = 0;
for (Iterator iter = items.iterator(); iter.hasNext();) {
PShelfItem item = (PShelfItem) iter.next();
gc.setBackground(back);
gc.setForeground(fore);
Integer y = (Integer) yCoordinates.get(index);
renderer.setBounds(0,y.intValue(),getClientArea().width,itemHeight);
renderer.setSelected(item == openItem);
renderer.setFocus(this.isFocusControl() && focusItem == item);
renderer.setHover(item == hoverItem);
renderer.paint(gc,item);
index ++;
}
}
private void computeItemYCoordinates()
{
yCoordinates.clear();
int y = getClientArea().y;
int i = 0;
for (Iterator iter = items.iterator(); iter.hasNext();)
{
i ++;
PShelfItem item = (PShelfItem)iter.next();
yCoordinates.add(new Integer(y));
y += itemHeight;
if (item == openItem)
y = getClientArea().y + getClientArea().height - (itemHeight * (items.size() - i));
}
}
void createItem(PShelfItem item,int index){
if (index == -1){
items.add(item);
} else {
items.add(index,item);
}
computeItemHeight();
if (openItem == null){
openItem(item,false);
}
//need to recompute ycoords and heights and such
onResize();
}
void removeItem(PShelfItem item){
computeItemHeight();
items.remove(item);
if (openItem == item)
{
openItem = null;
if (items.size() > 0)
{
openItem((PShelfItem) items.get(0),false);
}
}
onResize();
}
private void openItem(PShelfItem item, boolean animation){
PShelfItem previousOpen = openItem;
openItem = item;
focusItem = item;
item.getBodyParent().setBounds(0,0,0,0);
item.getBodyParent().setVisible(true);
item.getBody().layout();
if (animation && (getStyle() & SWT.SIMPLE) == 0){
if (items.indexOf(item) < items.indexOf(previousOpen)){
animateOpenFromTop(previousOpen,item);
} else {
animateOpenFromBottom(previousOpen,item);
}
} else {
if (previousOpen != null && !previousOpen.isDisposed())
previousOpen.getBodyParent().setBounds(0,0,0,0);
if ((getStyle() & SWT.SIMPLE) != 0){
//reorder the items
items.remove(item);
items.add(0,item);
}
onResize();
}
if (previousOpen != null)
previousOpen.getBodyParent().setVisible(false);
redraw();
getDisplay().update();
Event e = new Event();
e.item = openItem;
this.notifyListeners(SWT.Selection,e);
computeItemYCoordinates();
onResize();
}
private void animateOpenFromTop(PShelfItem previousItem, PShelfItem newItem)
{
double percentOfWork = 0;
while (percentOfWork < 1)
{
yCoordinates.clear();
int yTop = getClientArea().y;
int yBottom = getClientArea().y + getClientArea().height - (itemHeight * (items.size() - (items.indexOf(previousItem) + 1)));
int totalGrowingArea = getClientArea().height - (itemHeight * items.size());
int growingSpace = (int)(totalGrowingArea * percentOfWork);
boolean addingGrowingSpace = false;
for (int i = 0; i < items.size(); i++)
{
if (i <= items.indexOf(newItem))
{
//put on top
yCoordinates.add(new Integer(yTop));
yTop += itemHeight;
}
else if (i > items.indexOf(previousItem))
{
//put on bottom
yCoordinates.add(new Integer(yBottom));
yBottom += itemHeight;
}
else
{
if (!addingGrowingSpace)
{
yTop += growingSpace;
addingGrowingSpace = true;
}
yCoordinates.add(new Integer(yTop));
yTop += itemHeight;
}
}
sizeClients();
redraw();
update();
//workaround for SWT bug 193357
/*if (SWT.getPlatform().equals("carbon"))
{
getDisplay().readAndDispatch();
}*/
while(getDisplay().readAndDispatch());
percentOfWork += .02;
}
computeItemYCoordinates();
redraw();
}
private void animateOpenFromBottom(PShelfItem previousItem, PShelfItem newItem)
{
double percentOfWork = 0;
while (percentOfWork < 1)
{
yCoordinates.clear();
int yTop = getClientArea().y;
int yBottom = getClientArea().y + getClientArea().height - (itemHeight * (items.size() - (items.indexOf(newItem) + 1)));
int totalShrinkingArea = getClientArea().height - (itemHeight * items.size());
int collapsingSpace = (int)(totalShrinkingArea * (1 - percentOfWork));
boolean addedCollapsingSpace = false;
for (int i = 0; i < items.size(); i++)
{
if (i <= items.indexOf(previousItem))
{
//put on top
yCoordinates.add(new Integer(yTop));
yTop += itemHeight;
}
else if (i > items.indexOf(newItem))
{
//put on bottom
yCoordinates.add(new Integer(yBottom));
yBottom += itemHeight;
}
else
{
if (!addedCollapsingSpace)
{
yTop += collapsingSpace;
addedCollapsingSpace = true;
}
yCoordinates.add(new Integer(yTop));
yTop += itemHeight;
}
}
sizeClients();
redraw();
update();
//workaround for SWT bug 193357
/*if (SWT.getPlatform().equals("carbon"))
{
getDisplay().readAndDispatch();
}*/
while(getDisplay().readAndDispatch());
percentOfWork += .02;
}
computeItemYCoordinates();
redraw();
}
void onResize(){
computeItemYCoordinates();
sizeClients();
int clientHeight = getClientArea().height - (itemHeight * items.size());
for (Iterator iter = items.iterator(); iter.hasNext();)
{
PShelfItem item = (PShelfItem)iter.next();
item.getBody().setBounds(0,0,getClientArea().width,clientHeight);
}
}
private void sizeClients()
{
if (openItem == null)
return;
if (items.size() == 0)
return;
for(int i = 0; i < items.size(); i ++)
{
PShelfItem item = (PShelfItem)items.get(i);
int y = ((Integer)yCoordinates.get(i)).intValue();
int nextY = 0;
if (i + 1 < items.size())
{
nextY = ((Integer)yCoordinates.get(i + 1)).intValue();
}
else
{
nextY = getClientArea().y + getClientArea().height;
}
int clientHeight = nextY - y - itemHeight;
if (clientHeight > 0)
{
item.getBodyParent().setVisible(true);
item.getBodyParent().setBounds(0,y + itemHeight,getClientArea().width,clientHeight);
}
else
{
item.getBodyParent().setVisible(false);
}
}
}
void computeItemHeight(){
GC gc = new GC(this);
for (Iterator iter = items.iterator(); iter.hasNext();) {
PShelfItem item = (PShelfItem) iter.next();
itemHeight = Math.max(renderer.computeSize(gc, 0, SWT.DEFAULT, item).y,itemHeight);
}
gc.dispose();
}
/**
* Returns the item at the given location.
*
* @param point location
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the point is null</li>
* </ul>
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver or the renderer has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*/
public PShelfItem getItem(Point point){
checkWidget();
int y1 = 0;
int y2 = 0;
int c = 0;
if (point == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
for (Iterator iter = items.iterator(); iter.hasNext();)
{
PShelfItem item = (PShelfItem)iter.next();
y2 += itemHeight;
if (point.y >= y1 && point.y <= y2 -1){
return item;
}
y1 += itemHeight;
if (item == openItem){
y1 += openItem.getBodyParent().getSize().y;
y2 += openItem.getBodyParent().getSize().y;
}
c ++;
}
return null;
}
/**
* Sets the receiver's selection to the given item.
*
* @param item the item to select
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the item is null</li>
* </ul>
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*/
public void setSelection(PShelfItem item){
checkWidget();
if (item == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
if (!items.contains(item))
return;
if (openItem == item)
return;
openItem(item,true);
}
/**
* Returns the <code>PShelfItem</code> that is currently
* selected in the receiver.
*
* @return the currently selected item
*
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*/
public PShelfItem getSelection(){
checkWidget();
return openItem;
}
/**
* Returns an array of <code>PShelfItem</code>s which are the items
* in the receiver.
* <p>
* Note: This is not the actual structure used by the receiver
* to maintain its list of items, so modifying the array will
* not affect the receiver.
* </p>
*
* @return the items in the receiver
*
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*/
public PShelfItem[] getItems()
{
checkWidget();
return (PShelfItem[])items.toArray(new PShelfItem[items.size()]);
}
/**
* Adds the listener to the collection of listeners who will
* be notified when the receiver's selection changes, by sending
* it one of the messages defined in the <code>SelectionListener</code>
* interface.
* <p>
* When <code>widgetSelected</code> is called, the item field of the event object is valid.
* <code>widgetDefaultSelected</code> is not called.
* </p>
*
* @param listener the listener which should be notified
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
* </ul>
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*
* @see SelectionListener
* @see #removeSelectionListener
* @see SelectionEvent
*/
public void addSelectionListener(SelectionListener listener) {
checkWidget ();
if (listener == null) SWT.error (SWT.ERROR_NULL_ARGUMENT);
TypedListener typedListener = new TypedListener(listener);
addListener(SWT.Selection,typedListener);
addListener(SWT.DefaultSelection,typedListener);
}
/**
* Removes the listener from the collection of listeners who will
* be notified when the receiver's selection changes.
*
* @param listener the listener which should no longer be notified
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
* </ul>
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*
* @see SelectionListener
* @see #addSelectionListener
*/
public void removeSelectionListener (SelectionListener listener) {
checkWidget ();
if (listener == null) SWT.error (SWT.ERROR_NULL_ARGUMENT);
removeListener(SWT.Selection, listener);
removeListener(SWT.DefaultSelection,listener);
}
}