blob: ca38f6974ade191f4ca44052693d4032e8c88fcf [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2004 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.ui.forms.widgets;
import java.util.Vector;
import org.eclipse.jface.util.Assert;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.forms.events.*;
import org.eclipse.ui.internal.forms.widgets.*;
/**
* This composite is capable of expanding or collapsing a single client that is
* its direct child. The composite renders an expansion toggle affordance
* (according to the chosen style), and a title that also acts as a hyperlink
* (can be selected and is traversable). The client is layed out below the
* title when expanded, or hidden when collapsed.
* <p>The widget can be instantiated as-is, or subclassed to
* modify some aspects of it.
*
* @see Section
* @since 3.0
*/
public class ExpandableComposite extends Composite {
/**
* If this style is used, a twistie will be used to render the expansion
* toggle.
*/
public static final int TWISTIE = 1 << 1;
/**
* If this style is used, a tree node with either + or - signs will be used
* to render the expansion toggle.
*/
public static final int TREE_NODE = 1 << 2;
/**
* If this style is used, the title text will be rendered as a hyperlink
* that can individually accept focus. Otherwise, it will still act like a
* hyperlink, but only the toggle control will accept focus.
*/
public static final int FOCUS_TITLE = 1 << 3;
/**
* If this style is used, the client origin will be vertically aligned with
* the title text. Otherwise, it will start at x = 0.
*/
public static final int CLIENT_INDENT = 1 << 4;
/**
* If this style is used, computed size of the composite will take the
* client width into consideration only in the expanded state. Otherwise,
* client width will always be taken into acount.
*/
public static final int COMPACT = 1 << 5;
/**
* If this style is used, the control will be created in the expanded
* state. This state can later be changed programmatically or by the user
* if TWISTIE or TREE_NODE style is used.
*/
public static final int EXPANDED = 1 << 6;
/**
* If this style is used, title bar decoration will be painted behind
* the text.
*/
public static final int TITLE_BAR = 1 << 8;
/**
* If this style is used, title will not be rendered.
*/
public static final int NO_TITLE = 1<<12;
/**
* Width of the margin that will be added around the control (default is
* 0).
*/
public int marginWidth = 0;
/**
* Height of the margin that will be added around the control (default is
* 0).
*/
public int marginHeight = 0;
private int VSPACE = 3;
public int clientVerticalSpacing = VSPACE;
protected int GAP = 4;
private int SEPARATOR_HEIGHT = 2;
private int expansionStyle = TWISTIE | FOCUS_TITLE | EXPANDED;
private boolean expanded;
private Control textClient;
private Control client;
private Vector listeners;
protected ToggleHyperlink toggle;
protected Control textLabel;
private class ExpandableLayout extends Layout implements ILayoutExtension {
protected void layout(Composite parent, boolean changed) {
Rectangle clientArea = parent.getClientArea();
int thmargin = 0;
int tvmargin = 0;
if ((expansionStyle & TITLE_BAR)!=0) {
thmargin = GAP;
tvmargin = GAP;
}
int x = marginWidth + thmargin;
int y = marginHeight + tvmargin;
Point tsize = null;
Point tcsize = null;
if (toggle != null)
tsize = toggle.computeSize(SWT.DEFAULT, SWT.DEFAULT, changed);
int twidth = clientArea.width - marginWidth - marginWidth - thmargin - thmargin;
if (tsize != null)
twidth -= tsize.x + GAP;
if (textClient !=null)
tcsize = textClient.computeSize(SWT.DEFAULT, SWT.DEFAULT, changed);
if (tcsize!=null)
twidth -= tcsize.x + GAP;
Point size = null;
if (textLabel!=null)
size = textLabel.computeSize(twidth, SWT.DEFAULT, changed);
if (textLabel instanceof Label) {
Point defSize = textLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT,
changed);
if (defSize.y == size.y) {
// One line - pick the smaller of the two widths
size.x = Math.min(defSize.x, size.x);
}
}
if (toggle != null) {
GC gc = new GC(ExpandableComposite.this);
gc.setFont(getFont());
FontMetrics fm = gc.getFontMetrics();
int fontHeight = fm.getHeight();
gc.dispose();
int ty = fontHeight / 2 - tsize.y / 2 + 1;
ty = Math.max(ty, 0);
ty += marginHeight + tvmargin;
toggle.setLocation(x, ty);
toggle.setSize(tsize);
x += tsize.x + GAP;
}
if (textLabel!=null)
textLabel.setBounds(x, y, size.x, size.y);
if (textClient!=null) {
int tcx = clientArea.width - tcsize.x-thmargin;
textClient.setBounds(tcx, y, tcsize.x, tcsize.y);
}
if (size!=null)
y += size.y;
if ((expansionStyle & TITLE_BAR) != 0)
y += tvmargin;
if (getSeparatorControl() != null) {
y += VSPACE;
getSeparatorControl().setBounds(marginWidth, y,
clientArea.width - marginWidth - marginWidth,
SEPARATOR_HEIGHT);
y += SEPARATOR_HEIGHT;
if (expanded)
y += VSPACE;
}
if (expanded) {
int areaWidth = clientArea.width - marginWidth - marginWidth - thmargin-thmargin;
int cx = marginWidth + thmargin;
if ((expansionStyle & CLIENT_INDENT) != 0) {
cx = x;
areaWidth -= x;
}
if (client != null) {
Point dsize = null;
Control desc = getDescriptionControl();
if (desc != null) {
dsize = desc.computeSize(areaWidth, SWT.DEFAULT,
changed);
desc.setBounds(cx, y, dsize.x, dsize.y);
y += dsize.y + clientVerticalSpacing;
}
else
y += clientVerticalSpacing - VSPACE;
//int cwidth = clientArea.width - marginWidth - marginWidth
//- cx;
int cwidth = areaWidth;
int cheight = clientArea.height - marginHeight
- marginHeight - y;
client.setBounds(cx, y, cwidth, cheight);
}
}
}
protected Point computeSize(Composite parent, int wHint, int hHint,
boolean changed) {
int width = 0, height = 0;
Point tsize = null;
int twidth = 0;
if (toggle != null) {
tsize = toggle.computeSize(SWT.DEFAULT, SWT.DEFAULT, changed);
twidth = tsize.x + GAP;
}
int thmargin = 0;
int tvmargin = 0;
if ((expansionStyle & TITLE_BAR)!=0) {
thmargin = GAP;
tvmargin = GAP;
}
int innerwHint = wHint;
if (innerwHint != SWT.DEFAULT)
innerwHint -= twidth;
int innertHint = innerwHint;
Point tcsize = null;
if (textClient!=null) {
tcsize = textClient.computeSize(SWT.DEFAULT, SWT.DEFAULT, changed);
if (innertHint!=SWT.DEFAULT)
innertHint -= GAP + tcsize.x;
}
Point size = null;
if (textLabel!=null)
size = textLabel.computeSize(innertHint, SWT.DEFAULT, changed);
if (textLabel instanceof Label) {
Point defSize = textLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT,
changed);
if (defSize.y == size.y) {
// One line - pick the smaller of the two widths
size.x = Math.min(defSize.x, size.x);
}
}
if (size!=null)
width = size.x;
int sizey = size!=null?size.y:0;
height = tcsize!=null?Math.max(tcsize.y, sizey):sizey;
if (getSeparatorControl() != null) {
height += VSPACE + SEPARATOR_HEIGHT;
if (expanded && client != null)
height += VSPACE;
}
if ((expansionStyle & TITLE_BAR) != 0)
height += VSPACE;
if ((expanded || (expansionStyle & COMPACT) == 0) && client != null) {
int cwHint = wHint;
if (cwHint!=SWT.DEFAULT)
cwHint -= tvmargin + tvmargin;
if ((expansionStyle & CLIENT_INDENT) != 0)
cwHint = innerwHint;
Point dsize = null;
Point csize = client.computeSize(FormUtil.getWidthHint(cwHint,
client), SWT.DEFAULT, changed);
if (getDescriptionControl() != null) {
int dwHint = cwHint;
if (dwHint == SWT.DEFAULT) {
dwHint = csize.x - tvmargin - tvmargin;
if ((expansionStyle & CLIENT_INDENT) != 0)
dwHint -= twidth;
}
dsize = getDescriptionControl().computeSize(dwHint,
SWT.DEFAULT, changed);
}
if (dsize != null) {
if ((expansionStyle & CLIENT_INDENT) != 0)
dsize.x -= twidth;
width = Math.max(width, dsize.x);
if (expanded)
height += dsize.y + clientVerticalSpacing;
}
else
height += clientVerticalSpacing - VSPACE;
if ((expansionStyle & CLIENT_INDENT) != 0)
csize.x -= twidth;
width = Math.max(width, csize.x);
if (expanded)
height += csize.y;
}
if (toggle != null) {
height = height - sizey + Math.max(sizey, tsize.y);
width += twidth;
}
return new Point(width + marginWidth + marginWidth+thmargin+thmargin, height
+ marginHeight + marginHeight+tvmargin+tvmargin);
}
public int computeMinimumWidth(Composite parent, boolean changed) {
int width = 0;
Point size = null;
if (textLabel!=null)
size = textLabel.computeSize(5, SWT.DEFAULT, changed);
Point tcsize=null;
if (textClient!=null) {
tcsize = textClient.computeSize(SWT.DEFAULT, SWT.DEFAULT, changed);
}
int thmargin = 0;
int tvmargin = 0;
if ((expansionStyle & TITLE_BAR)!=0) {
thmargin = GAP;
tvmargin = GAP;
}
if (size!=null)
width = size.x;
if (tcsize!=null)
width += GAP + tcsize.x;
if ((expanded || (expansionStyle & COMPACT) == 0) && client != null) {
Point dsize = null;
if (getDescriptionControl() != null) {
dsize = getDescriptionControl().computeSize(5, SWT.DEFAULT,
changed);
width = Math.max(width, dsize.x);
}
int cwidth = FormUtil.computeMinimumWidth(client, changed);
width = Math.max(width, cwidth);
}
if (toggle != null) {
Point tsize = toggle.computeSize(SWT.DEFAULT, SWT.DEFAULT,
changed);
width += tsize.x + GAP;
}
return width + marginWidth + marginWidth+thmargin+thmargin;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.ui.forms.parts.ILayoutExtension#computeMinimumWidth(org.eclipse.swt.widgets.Composite,
* boolean)
*/
public int computeMaximumWidth(Composite parent, boolean changed) {
int width = 0;
Point size = null;
if (textLabel!=null)
textLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT,
changed);
Point tcsize=null;
int thmargin = 0;
int tvmargin = 0;
if ((expansionStyle & TITLE_BAR)!=0) {
thmargin = GAP;
tvmargin = GAP;
}
if (textClient!=null) {
tcsize = textClient.computeSize(SWT.DEFAULT, SWT.DEFAULT, changed);
}
if (size!=null)
width = size.x;
if (tcsize!=null)
width += GAP + tcsize.x;
if ((expanded || (expansionStyle & COMPACT) == 0) && client != null) {
Point dsize = null;
if (getDescriptionControl() != null) {
dsize = getDescriptionControl().computeSize(SWT.DEFAULT,
SWT.DEFAULT, changed);
width = Math.max(width, dsize.x);
}
int cwidth = FormUtil.computeMaximumWidth(client, changed);
width = Math.max(width, cwidth);
}
if (toggle != null) {
Point tsize = toggle.computeSize(SWT.DEFAULT, SWT.DEFAULT,
changed);
width += tsize.x + GAP;
}
return width + marginWidth + marginWidth+thmargin+thmargin;
}
}
/**
* Creates an expandable composite using a TWISTIE toggle.
*
* @param parent
* the parent composite
* @param style
* SWT style bits
*/
public ExpandableComposite(Composite parent, int style) {
this(parent, style, TWISTIE);
}
/**
* Creates the expandable composite in the provided parent.
*
* @param parent
* the parent
* @param style
* the control style
* @param expansionStyle
* the style of the expansion widget (TREE_NODE, TWISTIE,
* CLIENT_INDENT, COMPACT, FOCUS_TITLE)
*/
public ExpandableComposite(Composite parent, int style, int expansionStyle) {
super(parent, style);
this.expansionStyle = expansionStyle;
super.setLayout(new ExpandableLayout());
listeners = new Vector();
if ((expansionStyle & TITLE_BAR) != 0) {
this.addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
onPaint(e);
}
});
}
if ((expansionStyle & TWISTIE) != 0)
toggle = new Twistie(this, SWT.NULL);
else if ((expansionStyle & TREE_NODE) != 0)
toggle = new TreeNode(this, SWT.NULL);
else
expanded = true;
if ((expansionStyle & EXPANDED) != 0)
expanded = true;
if (toggle != null) {
toggle.setExpanded(expanded);
toggle.addHyperlinkListener(new HyperlinkAdapter() {
public void linkActivated(HyperlinkEvent e) {
toggleState();
}
});
}
if ((expansionStyle & FOCUS_TITLE) != 0) {
Hyperlink link = new Hyperlink(this, SWT.WRAP);
link.addHyperlinkListener(new HyperlinkAdapter() {
public void linkActivated(HyperlinkEvent e) {
toggle.setExpanded(!toggle.isExpanded());
toggleState();
}
});
textLabel = link;
} else if ((expansionStyle & NO_TITLE) == 0) {
final Label label = new Label(this, SWT.WRAP);
if (!isFixedStyle()) {
label.setCursor(FormsResources.getHandCursor());
label.addListener(SWT.MouseDown, new Listener() {
public void handleEvent(Event e) {
if (toggle != null)
toggle.setFocus();
}
});
label.addListener(SWT.MouseUp, new Listener() {
public void handleEvent(Event e) {
label.setCursor(FormsResources.getBusyCursor());
toggle.setExpanded(!toggle.isExpanded());
toggleState();
label.setCursor(FormsResources.getHandCursor());
}
});
}
textLabel = label;
}
if (textLabel!=null)
textLabel.setMenu(getMenu());
}
/**
* Prevents assignment of the layout manager - expandable composite uses
* its own layout.
*/
public final void setLayout(Layout layout) {
}
/**
* Sets the background of all the custom controls in the expandable.
*/
public void setBackground(Color bg) {
super.setBackground(bg);
if (textLabel!=null)
textLabel.setBackground(bg);
if (toggle != null)
toggle.setBackground(bg);
}
/**
* Sets the foreground of all the custom controls in the expandable.
*/
public void setForeground(Color fg) {
super.setForeground(fg);
if (textLabel!=null)
textLabel.setForeground(fg);
if (toggle != null)
toggle.setForeground(fg);
}
/**
* Sets the color of the toggle control.
*
* @param c
* the color object
*/
public void setToggleColor(Color c) {
if (toggle != null)
toggle.setDecorationColor(c);
}
/**
* Sets the active color of the toggle control (when the mouse enters the
* toggle area).
*
* @param c
* the active color object
*/
public void setActiveToggleColor(Color c) {
if (toggle != null)
toggle.setHoverDecorationColor(c);
}
/**
* Sets the fonts of all the custom controls in the expandable.
*/
public void setFont(Font font) {
super.setFont(font);
if (textLabel!=null)
textLabel.setFont(font);
if (toggle != null)
toggle.setFont(font);
}
/**
* Sets the client of this expandable composite. The client must not be
* <samp>null </samp> and must be a direct child of this container.
*
* @param client
* the client that will be expanded or collapsed
*/
public void setClient(Control client) {
Assert.isTrue(client != null && client.getParent().equals(this));
this.client = client;
}
/**
* Returns the current expandable client.
*
* @return the client control
*/
public Control getClient() {
return client;
}
/**
* Sets the title of the expandable composite. The title will act as a
* hyperlink and activating it will toggle the client between expanded and
* collapsed state.
*
* @param title
* the new title string
* @see #getTitle
*/
public void setText(String title) {
if (textLabel instanceof Label)
((Label) textLabel).setText(title);
else if (textLabel instanceof Hyperlink)
((Hyperlink) textLabel).setText(title);
}
/**
* Returns the title string.
*
* @return the title string
* @see #setTitle
*/
public String getText() {
if (textLabel instanceof Label)
return ((Label) textLabel).getText();
else if (textLabel instanceof Hyperlink)
return ((Hyperlink) textLabel).getText();
else
return "";
}
/**
* Tests the expanded state of the composite.
*
* @return <samp>true </samp> if expanded, <samp>false </samp> if
* collapsed.
*/
public boolean isExpanded() {
return expanded;
}
/**
* Returns the bitwise-ORed style bits for the expansion control.
*
* @return
*/
public int getExpansionStyle() {
return expansionStyle;
}
/**
* Programmatically changes expanded state.
*
* @param expanded
* the new expanded state
*/
public void setExpanded(boolean expanded) {
internalSetExpanded(expanded);
if (toggle != null)
toggle.setExpanded(expanded);
}
/**
* Performs the expansion state change for the expandable control.
*
* @param expanded
* the expansion state
*/
protected void internalSetExpanded(boolean expanded) {
if (this.expanded != expanded) {
this.expanded = expanded;
if (getDescriptionControl() != null)
getDescriptionControl().setVisible(expanded);
if (client != null)
client.setVisible(expanded);
layout();
}
}
/**
* Adds the listener that will be notified when the expansion state
* changes.
*
* @param listener
* the listener to add
*/
public void addExpansionListener(IExpansionListener listener) {
if (!listeners.contains(listener))
listeners.add(listener);
}
/**
* Removes the expansion listener.
*
* @param listener
* the listner to remove
*/
public void removeExpansionListener(IExpansionListener listener) {
if (listeners.contains(listener))
listeners.remove(listener);
}
private void toggleState() {
boolean newState = !isExpanded();
fireExpanding(newState, true);
internalSetExpanded(!isExpanded());
fireExpanding(newState, false);
}
/**
* If TITLE_BAR style is used, title bar decoration will
* be painted behind the text in this method. The default
* implementation does nothing - subclasses are responsible
* for rendering the title area.
* @param e the paint event
*/
protected void onPaint(PaintEvent e) {
}
private void fireExpanding(boolean state, boolean before) {
int size = listeners.size();
if (size == 0)
return;
ExpansionEvent e = new ExpansionEvent(this, state);
for (int i = 0; i < size; i++) {
IExpansionListener listener = (IExpansionListener) listeners.get(i);
if (before)
listener.expansionStateChanging(e);
else
listener.expansionStateChanged(e);
}
}
/**
* Returns description control that will be placed under the title if
* present.
*
* @return the description control or <samp>null </samp> if not used.
*/
protected Control getDescriptionControl() {
return null;
}
/**
* Returns the separator control that will be placed between the title and
* the description if present.
*
* @return the separator control or <samp>null </samp> if not used.
*/
protected Control getSeparatorControl() {
return null;
}
/**
* Computes the size of the expandable composite.
*
* @see org.eclipse.swt.widgets.Composite#computeSize
*/
public Point computeSize(int wHint, int hHint, boolean changed) {
checkWidget();
Point size;
ExpandableLayout layout = (ExpandableLayout) getLayout();
if (wHint == SWT.DEFAULT || hHint == SWT.DEFAULT) {
size = layout.computeSize(this, wHint, hHint, changed);
} else {
size = new Point(wHint, hHint);
}
Rectangle trim = computeTrim(0, 0, size.x, size.y);
return new Point(trim.width, trim.height);
}
/**
* Returns <samp>true </samp> if the composite is fixed i.e. cannot be
* expanded or collapsed. Fixed control will still contain the title,
* separator and description (if present) as well as the client, but will
* be in the permanent expanded state and the toggle affordance will not be
* shown.
*
* @return <samp>true </samp> if the control is fixed in the expanded
* state, <samp>false </samp> if it can be collapsed.
*/
protected boolean isFixedStyle() {
return (expansionStyle & TWISTIE) == 0
&& (expansionStyle & TREE_NODE) == 0;
}
/**
* Returns the text client control.
* @return Returns the text client control if specified, or <code>null</code> if
* not.
*/
public Control getTextClient() {
return textClient;
}
/**
* Sets the text client control. Text client is a control that
* is a child of the expandable composite and is placed to the right
* of the text. It can be used to place small image hyperlinks. If
* more than one control is needed, use Composite to hold them. Care should
* be taken that the height of the control is comparable to the
* height of the text.
* @param textClient the textClient to set or <code>null</code> if not
* needed any more.
*/
public void setTextClient(Control textClient) {
if (this.textClient!=null)
this.textClient.dispose();
this.textClient = textClient;
}
}