/*******************************************************************************
* Copyright (c) 2006 Chris Gross. 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:
* schtoo@schtoo.com (Chris Gross) - initial API and implementation
* Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation
*******************************************************************************/
package org.eclipse.nebula.widgets.pgroup;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
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.ExpandListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.TypedListener;
import org.eclipse.swt.widgets.Widget;
/**
* Instances of this class provide a decorated border as well as an optional toggle to expand and
* collapse the control.
* <p>
* This widget is customizable through alternative <code>AbstractGroupStrategy</code>s. Each
* strategy determines the size and appearance of the widget.
* <p>
* <dl>
* <dt><b>Styles:</b></dt>
* <dd>SMOOTH</dd>
* <dt><b>Events:</b></dt>
* <dd>Expand, Collapse</dd>
* </dl>
*
* @author cgross
*/
public class PGroup extends Canvas
{
private AbstractGroupStrategy strategy;
private Image image;
private String text = "";
private Font initialFont;
private int imagePosition = SWT.LEAD;
private int togglePosition = SWT.TRAIL;
private int linePosition = SWT.CENTER;
private boolean expanded = true;
private boolean overToggle = false;
private AbstractRenderer toggleRenderer;
private AbstractToolItemRenderer toolItemRenderer;
private Color backgroundColor;
private List toolitems = new ArrayList();
private PGroupToolItem activeToolItem;
private static int checkStyle(int style)
{
int mask = SWT.LEFT_TO_RIGHT | SWT.RIGHT_TO_LEFT | SWT.SMOOTH;
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>
* <p>
* To ensure that the color of corners is equal to one of the underlying control
* invoke the parent composites {@link Composite#setBackgroundMode(int)}
* with {@link SWT#INHERIT_DEFAULT} or {@link SWT#INHERIT_DEFAULT}
* </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>
*
* @see SWT
* @see Widget#checkSubclass
* @see Widget#getStyle
*/
public PGroup(Composite parent, int style)
{
super(parent, checkStyle(style));
setStrategy(new RectangleGroupStrategy());
setToggleRenderer(new ChevronsToggleRenderer());
setToolItemRenderer(new SimpleToolItemRenderer());
initialFont = new Font(getDisplay(), getFont().getFontData()[0].getName(), getFont()
.getFontData()[0].getHeight(), SWT.BOLD);
super.setFont(initialFont);
strategy.initialize(this);
initListeners();
}
/**
* {@inheritDoc}
*/
public Color getBackground()
{
checkWidget();
if (backgroundColor == null)
return super.getBackground();
return backgroundColor;
}
Color internalGetBackground()
{
return backgroundColor;
}
/**
* Sets the receiver's background color to the color specified
* by the argument, or to the default system color for the control
* if the argument is null.
* <p>
* Note: This operation is a hint and may be overridden by the platform.
* For example, on Windows the background of a Button cannot be changed.
* </p>
* <p>
* To ensure that the color of corners is equal to one of the underlying control
* invoke the parent composites {@link Composite#setBackgroundMode(int)}
* with {@link SWT#INHERIT_DEFAULT} or {@link SWT#INHERIT_DEFAULT}
* </p>
* <p>
* Note: If a new strategy is set on the receiver it may overwrite the existing
* background color.
* </p>
* @param color the new color (or null)
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_INVALID_ARGUMENT - if the argument has been disposed</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 setBackground(Color color)
{
checkWidget();
backgroundColor = color;
redraw();
}
/**
* Returns the toggle renderer or <code>null</code>.
*
* @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 getToggleRenderer()
{
checkWidget();
return toggleRenderer;
}
/**
* Sets the toggle renderer. If the toggle renderer is set to <code>null</code> the control
* will not show a toggle or allow the user to expand/collapse the group by clicking on the title.
*
* @param toggleRenderer the toggleRenderer to set or <code>null</code>
*
* @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 setToggleRenderer(AbstractRenderer toggleRenderer)
{
checkWidget();
this.toggleRenderer = toggleRenderer;
strategy.update();
redraw();
}
private void initListeners()
{
this.addDisposeListener(new DisposeListener()
{
public void widgetDisposed(DisposeEvent e)
{
onDispose();
}
});
addPaintListener(new PaintListener()
{
public void paintControl(PaintEvent e)
{
onPaint(e);
}
});
addListener(SWT.Traverse, new Listener()
{
public void handleEvent(Event e)
{
switch (e.detail)
{
/* Do tab group traversal */
case SWT.TRAVERSE_ESCAPE :
case SWT.TRAVERSE_RETURN :
case SWT.TRAVERSE_TAB_NEXT :
case SWT.TRAVERSE_TAB_PREVIOUS :
case SWT.TRAVERSE_PAGE_NEXT :
case SWT.TRAVERSE_PAGE_PREVIOUS :
e.doit = true;
break;
}
}
});
addListener(SWT.FocusIn, new Listener()
{
public void handleEvent(Event e)
{
redraw();
}
});
addListener(SWT.FocusOut, new Listener()
{
public void handleEvent(Event e)
{
redraw();
}
});
addListener(SWT.KeyDown, new Listener()
{
public void handleEvent(Event e)
{
onKeyDown(e);
}
});
addListener(SWT.MouseExit, new Listener()
{
public void handleEvent(Event e)
{
onMouseExit(e);
}
});
addListener(SWT.MouseMove, new Listener()
{
public void handleEvent(Event e)
{
onMouseMove(e);
}
});
addListener(SWT.MouseDown, new Listener()
{
public void handleEvent(Event e)
{
onMouseDown(e);
}
});
}
private void onPaint(PaintEvent e)
{
Color back = e.gc.getBackground();
Color fore = e.gc.getForeground();
strategy.paint(e.gc);
e.gc.setBackground(back);
e.gc.setForeground(fore);
if (toggleRenderer != null)
{
toggleRenderer.setExpanded(expanded);
toggleRenderer.setFocus(isFocusControl());
toggleRenderer.setHover(overToggle);
toggleRenderer.paint(e.gc, this);
}
if( toolItemRenderer != null && toolitems.size() > 0 ) {
paintToolItems(e.gc);
}
}
private void paintToolItems(GC gc) {
Rectangle itemArea = strategy.getToolItemArea();
if( itemArea != null ) {
int spacing = 3;
Iterator it = toolitems.iterator();
AbstractToolItemRenderer toolitemRenderer = getToolItemRenderer();
Point[] sizes = new Point[toolitems.size()];
boolean min = false;
int width = 0;
int i = 0;
while( it.hasNext() ) {
PGroupToolItem item = (PGroupToolItem) it.next();
Point p = toolitemRenderer.computeSize(gc, item, AbstractToolItemRenderer.DEFAULT);
sizes[i++] = p;
if( width + spacing + p.x > itemArea.width ) {
min = true;
break;
} else {
width += p.x + spacing;
}
}
if( min ) {
toolitemRenderer.setSizeType(AbstractToolItemRenderer.MIN);
} else {
toolitemRenderer.setSizeType(AbstractToolItemRenderer.DEFAULT);
}
if( min ) {
it = toolitems.iterator();
i = 0;
while( it.hasNext() ) {
PGroupToolItem item = (PGroupToolItem) it.next();
sizes[i++] = toolitemRenderer.computeSize(gc, item, AbstractToolItemRenderer.MIN);
}
}
it = toolitems.iterator();
int x = itemArea.x;
i = 0;
while( it.hasNext() ) {
PGroupToolItem item = (PGroupToolItem) it.next();
Point p = sizes[i++];
Rectangle rect = new Rectangle(x, itemArea.y, p.x, itemArea.height);
item.setBounds(rect);
toolitemRenderer.setBounds(rect);
x += p.x + spacing;
if( (item.getStyle() & SWT.DROP_DOWN) != 0 ) {
item.setDropDownArea(toolitemRenderer.computeDropDownArea(item.getBounds()));
}
toolitemRenderer.setHover(activeToolItem == item);
toolitemRenderer.paint(gc, item);
}
}
}
private void onKeyDown(Event e)
{
if (e.character == ' ' || e.character == '\r')
{
setExpanded(!getExpanded());
if (expanded)
{
notifyListeners(SWT.Expand, new Event());
}
else
{
notifyListeners(SWT.Collapse, new Event());
}
}
}
private void onMouseMove(Event e)
{
boolean newOverToggle = PGroup.this.strategy.isToggleLocation(e.x, e.y);
boolean redraw = false;
if (newOverToggle != overToggle)
{
if (newOverToggle)
{
setCursor(getDisplay().getSystemCursor(SWT.CURSOR_HAND));
}
else
{
setCursor(null);
}
overToggle = newOverToggle;
redraw = true;
}
if( toolitems.size() > 0 ) {
Iterator it = toolitems.iterator();
PGroupToolItem newItem = null;
while( it.hasNext() ) {
PGroupToolItem item = (PGroupToolItem) it.next();
if( item.getBounds().contains(e.x, e.y) ) {
newItem = item;
break;
}
}
if( newItem != activeToolItem ) {
activeToolItem = newItem;
setToolTipText(null);
if( newItem != null && newItem.getToolTipText () != null ) {
setToolTipText(newItem.getToolTipText ());
}
redraw = true;
}
}
if( redraw ) {
redraw();
}
}
private void onMouseExit(Event e)
{
setCursor(null);
overToggle = false;
redraw();
}
private void onMouseDown(Event e)
{
if (overToggle && e.button == 1)
{
setExpanded(!expanded);
setFocus();
if (expanded)
{
notifyListeners(SWT.Expand, new Event());
}
else
{
notifyListeners(SWT.Collapse, new Event());
}
}
else if( activeToolItem != null )
{
activeToolItem.onMouseDown(e);
}
}
/**
* Returns the strategy.
*
* @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 AbstractGroupStrategy getStrategy()
{
checkWidget();
return strategy;
}
/**
* Sets the strategy.
*
* @param strategy the strategy to set
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the strategy 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 setStrategy(AbstractGroupStrategy strategy)
{
checkWidget();
if (strategy == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
this.strategy = strategy;
setForeground(null);
strategy.initialize(this);
}
/**
* Returns the image.
*
* @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 Image getImage()
{
checkWidget();
return image;
}
/**
* Sets the image.
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the image is disposed</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 setImage(Image image)
{
checkWidget();
if (image != null && image.isDisposed())
SWT.error(SWT.ERROR_INVALID_ARGUMENT);
this.image = image;
strategy.update();
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.widgets.Control#setFont(org.eclipse.swt.graphics.Font)
*/
public void setFont(Font font)
{
checkWidget();
super.setFont(font);
strategy.update();
redraw();
}
/**
* Returns the text.
*
* @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 String getText()
{
checkWidget();
return text;
}
/**
* Sets the text.
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the text 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 setText(String text)
{
checkWidget();
if (text == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
this.text = text;
strategy.update();
redraw();
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.widgets.Control#computeSize(int, int, boolean)
*/
public Point computeSize(int arg0, int arg1, boolean arg2)
{
checkWidget();
if (getExpanded())
return super.computeSize(arg0, arg1, arg2);
Rectangle trim = strategy.computeTrim(0, 0, 0, 0);
trim.width = super.computeSize(arg0, arg1, arg2).x;
return new Point(trim.width, Math.max(trim.height, arg1));
}
/**
* Returns the expanded/collapsed state.
*
* @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 boolean getExpanded()
{
checkWidget();
return expanded;
}
/**
* Sets the expanded state of the group.
*
* @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 setExpanded(boolean expanded)
{
checkWidget();
Control[] kids = getChildren();
for (int i = 0; i < kids.length; i++)
{
kids[i].setVisible(expanded);
}
this.expanded = expanded;
getParent().layout();
redraw();
}
private void onDispose()
{
strategy.dispose();
if (initialFont != null)
initialFont.dispose();
}
/**
* Adds the listener to the collection of listeners who will
* be notified when the receiver is expanded or collapsed
* by sending it one of the messages defined in the <code>ExpandListener</code>
* interface.
*
* @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 ExpandListener
* @see #removeExpandListener
*/
public void addExpandListener(ExpandListener listener)
{
checkWidget();
if (listener == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
TypedListener typedListener = new TypedListener(listener);
addListener(SWT.Expand, typedListener);
addListener(SWT.Collapse, typedListener);
}
/**
* Removes the listener from the collection of listeners who will
* be notified when the receiver are expanded or collapsed.
*
* @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 ExpandListener
* @see #addExpandListener
*/
public void removeExpandListener(ExpandListener listener)
{
checkWidget();
if (listener == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
removeListener(SWT.Expand, listener);
removeListener(SWT.Collapse, listener);
}
/**
* Returns the image position. The image position is a combination of integer constants OR'ed
* together.
*
* @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 int getImagePosition()
{
checkWidget();
return imagePosition;
}
/**
* Sets the image position. The image position is a combination of integer constants OR'ed
* together. Valid values are <code>SWT.LEFT</code> and <code>SWT.RIGHT</code> (mutually exclusive).
* <code>SWT.TOP</code> is hint interpreted by some strategies.
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the strategy 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 setImagePosition(int imagePosition)
{
checkWidget();
this.imagePosition = imagePosition;
strategy.update();
redraw();
}
/**
* Returns the toggle position. The toggle position is a combination of integer constants OR'ed
* together.
*
* @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 int getTogglePosition()
{
checkWidget();
return togglePosition;
}
/**
* Sets the toggle position. The toggle position is a combination of integer constants OR'ed
* together. Valid values are <code>SWT.LEFT</code> and <code>SWT.RIGHT</code> (mutually exclusive).
*
* @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 setTogglePosition(int togglePosition)
{
checkWidget();
this.togglePosition = togglePosition;
strategy.update();
redraw();
}
/**
* Returns the line position. The line position is a combination of integer constants OR'ed
* together.
*
* @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 int getLinePosition()
{
checkWidget();
return linePosition;
}
/**
* Sets the line position. The line position is a combination of integer constants OR'ed
* together. Valid values are <code>SWT.BOTTOM</code> and <code>SWT.CENTER</code> (mutually exclusive).
*
* @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 setLinePosition(int linePosition)
{
checkWidget();
this.linePosition = linePosition;
strategy.update();
redraw();
}
/**
* {@inheritDoc}
*/
public Rectangle computeTrim(int x, int y, int width, int height)
{
checkWidget();
return strategy.computeTrim(x, y, width, height);
}
/**
* {@inheritDoc}
*/
public Rectangle getClientArea()
{
checkWidget();
if (getExpanded())
return strategy.getClientArea();
return new Rectangle(-10, 0, 0, 0);
}
/**
* Sets the receiver's foreground color to the color specified
* by the argument, or to the default system color for the control
* if the argument is null.
* <p>
* Note: This operation is a hint and may be overridden by the platform.
* </p>
* <p>
* Note: If a new strategy is set on the receiver it may overwrite the existing
* foreground color.
* </p>
* @param color the new color (or null)
*
* @exception IllegalArgumentException <ul>
* <li>ERROR_INVALID_ARGUMENT - if the argument has been disposed</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 setForeground(Color color) {
super.setForeground(color);
}
void addToolItem(PGroupToolItem toolitem) {
toolitems.add(toolitem);
}
void removeToolItem(PGroupToolItem toolitem) {
toolitems.remove(toolitem);
}
public AbstractToolItemRenderer getToolItemRenderer() {
return toolItemRenderer;
}
public void setToolItemRenderer(AbstractToolItemRenderer toolItemRenderer)
{
checkWidget();
this.toolItemRenderer = toolItemRenderer;
strategy.update();
redraw();
}
public PGroupToolItem[] getToolItems() {
PGroupToolItem[] rv = new PGroupToolItem[toolitems.size()];
toolitems.toArray(rv);
return rv;
}
}