blob: b3dce0756be8286605f2de207a96bb1fa51717ac [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.eclipse.org/org/documents/epl-v10.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
import com.android.ide.eclipse.adt.internal.editors.IconFactory;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.ScrollBar;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* The accordion control allows a series of labels with associated content that can be
* shown. For more details on accordions, see http://en.wikipedia.org/wiki/Accordion_(GUI)
* <p>
* This control allows the children to be created lazily. You can also customize the
* composite which is created to hold the children items, to for example allow multiple
* columns of items rather than just the default vertical stack.
* <p>
* The visual appearance of the headers is built in; it uses a mild gradient, with a
* heavier gradient during mouse-overs. It also uses a bold label along with the eclipse
* folder icons.
* <p>
* The control can be configured to enforce a single category open at any time (the
* default), or allowing multiple categories open (where they share the available space).
* The control can also be configured to fill the available vertical space for the open
* category/categories.
*/
public abstract class AccordionControl extends Composite {
/** Pixel spacing between header items */
private static final int HEADER_SPACING = 0;
/** Pixel spacing between items in the content area */
private static final int ITEM_SPACING = 0;
private static final String KEY_CONTENT = "content"; //$NON-NLS-1$
private static final String KEY_HEADER = "header"; //$NON-NLS-1$
private Image mClosed;
private Image mOpen;
private boolean mSingle = true;
private boolean mWrap;
/**
* Creates the container which will hold the items in a category; this can be
* overridden to lay out the children with a different layout than the default
* vertical RowLayout
*/
protected Composite createChildContainer(Composite parent, Object header, int style) {
Composite composite = new Composite(parent, style);
if (mWrap) {
RowLayout layout = new RowLayout(SWT.HORIZONTAL);
layout.center = true;
composite.setLayout(layout);
} else {
RowLayout layout = new RowLayout(SWT.VERTICAL);
layout.spacing = ITEM_SPACING;
layout.marginHeight = 0;
layout.marginWidth = 0;
layout.marginLeft = 0;
layout.marginTop = 0;
layout.marginRight = 0;
layout.marginBottom = 0;
composite.setLayout(layout);
}
// TODO - maybe do multi-column arrangement for simple nodes
return composite;
}
/**
* Creates the children under a particular header
*
* @param parent the parent composite to add the SWT items to
* @param header the header object that is being opened for the first time
*/
protected abstract void createChildren(Composite parent, Object header);
/**
* Set whether a single category should be enforced or not (default=true)
*
* @param single if true, enforce a single category open at a time
*/
public void setAutoClose(boolean single) {
mSingle = single;
}
/**
* Returns whether a single category should be enforced or not (default=true)
*
* @return true if only a single category can be open at a time
*/
public boolean isAutoClose() {
return mSingle;
}
/**
* Returns the labels used as header categories
*
* @return list of header labels
*/
public List<CLabel> getHeaderLabels() {
List<CLabel> headers = new ArrayList<CLabel>();
for (Control c : getChildren()) {
if (c instanceof CLabel) {
headers.add((CLabel) c);
}
}
return headers;
}
/**
* Show all categories
*
* @param performLayout if true, call {@link #layout} and {@link #pack} when done
*/
public void expandAll(boolean performLayout) {
for (Control c : getChildren()) {
if (c instanceof CLabel) {
if (!isOpen(c)) {
toggle((CLabel) c, false, false);
}
}
}
if (performLayout) {
pack();
layout();
}
}
/**
* Hide all categories
*
* @param performLayout if true, call {@link #layout} and {@link #pack} when done
*/
public void collapseAll(boolean performLayout) {
for (Control c : getChildren()) {
if (c instanceof CLabel) {
if (isOpen(c)) {
toggle((CLabel) c, false, false);
}
}
}
if (performLayout) {
layout();
}
}
/**
* Create the composite.
*
* @param parent the parent widget to add the accordion to
* @param style the SWT style mask to use
* @param headers a list of headers, whose {@link Object#toString} method should
* produce the heading label
* @param greedy if true, grow vertically as much as possible
* @param wrapChildren if true, configure the child area to be horizontally laid out
* with wrapping
* @param expand Set of headers to expand initially
*/
public AccordionControl(Composite parent, int style, List<?> headers,
boolean greedy, boolean wrapChildren, Set<String> expand) {
super(parent, style);
mWrap = wrapChildren;
GridLayout gridLayout = new GridLayout(1, false);
gridLayout.verticalSpacing = HEADER_SPACING;
gridLayout.horizontalSpacing = 0;
gridLayout.marginWidth = 0;
gridLayout.marginHeight = 0;
setLayout(gridLayout);
Font labelFont = null;
mOpen = IconFactory.getInstance().getIcon("open-folder"); //$NON-NLS-1$
mClosed = IconFactory.getInstance().getIcon("closed-folder"); //$NON-NLS-1$
List<CLabel> expandLabels = new ArrayList<CLabel>();
for (Object header : headers) {
final CLabel label = new CLabel(this, SWT.SHADOW_OUT);
label.setText(header.toString().replace("&", "&&")); //$NON-NLS-1$ //$NON-NLS-2$
updateBackground(label, false);
if (labelFont == null) {
labelFont = JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT);
}
label.setFont(labelFont);
label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
setHeader(header, label);
label.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
if (e.button == 1 && (e.stateMask & SWT.MODIFIER_MASK) == 0) {
toggle(label, true, mSingle);
}
}
});
label.addMouseTrackListener(new MouseTrackListener() {
@Override
public void mouseEnter(MouseEvent e) {
updateBackground(label, true);
}
@Override
public void mouseExit(MouseEvent e) {
updateBackground(label, false);
}
@Override
public void mouseHover(MouseEvent e) {
}
});
// Turn off border?
final ScrolledComposite scrolledComposite = new ScrolledComposite(this, SWT.V_SCROLL);
ScrollBar verticalBar = scrolledComposite.getVerticalBar();
verticalBar.setIncrement(20);
verticalBar.setPageIncrement(100);
// Do we need the scrolled composite or can we just look at the next
// wizard in the hierarchy?
setContentArea(label, scrolledComposite);
scrolledComposite.setExpandHorizontal(true);
scrolledComposite.setExpandVertical(true);
GridData scrollGridData = new GridData(SWT.FILL,
greedy ? SWT.FILL : SWT.TOP, false, greedy, 1, 1);
scrollGridData.exclude = true;
scrollGridData.grabExcessHorizontalSpace = wrapChildren;
scrolledComposite.setLayoutData(scrollGridData);
if (wrapChildren) {
scrolledComposite.addControlListener(new ControlAdapter() {
@Override
public void controlResized(ControlEvent e) {
Rectangle r = scrolledComposite.getClientArea();
Control content = scrolledComposite.getContent();
if (content != null && r != null) {
Point minSize = content.computeSize(r.width, SWT.DEFAULT);
scrolledComposite.setMinSize(minSize);
ScrollBar vBar = scrolledComposite.getVerticalBar();
vBar.setPageIncrement(r.height);
}
}
});
}
updateIcon(label);
if (expand != null && expand.contains(label.getText())) {
// Comparing "label.getText()" rather than "header" because we make some
// tweaks to the label (replacing & with && etc) and in the getExpandedCategories
// method we return the label texts
expandLabels.add(label);
}
}
// Expand the requested categories
for (CLabel label : expandLabels) {
toggle(label, false, false);
}
}
/** Updates the background gradient of the given header label */
private void updateBackground(CLabel label, boolean mouseOver) {
Display display = label.getDisplay();
label.setBackground(new Color[] {
display.getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW),
display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND),
display.getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW)
}, new int[] {
mouseOver ? 60 : 40, 100
}, true);
}
/**
* Updates the icon for a header label to be open/close based on the {@link #isOpen}
* state
*/
private void updateIcon(CLabel label) {
label.setImage(isOpen(label) ? mOpen : mClosed);
}
/** Returns true if the content area for the given label is open/showing */
private boolean isOpen(Control label) {
return !((GridData) getContentArea(label).getLayoutData()).exclude;
}
/** Toggles the visibility of the children of the given label */
private void toggle(CLabel label, boolean performLayout, boolean autoClose) {
if (autoClose) {
collapseAll(true);
}
ScrolledComposite scrolledComposite = getContentArea(label);
GridData scrollGridData = (GridData) scrolledComposite.getLayoutData();
boolean close = !scrollGridData.exclude;
scrollGridData.exclude = close;
scrolledComposite.setVisible(!close);
updateIcon(label);
if (!scrollGridData.exclude && scrolledComposite.getContent() == null) {
Object header = getHeader(label);
Composite composite = createChildContainer(scrolledComposite, header, SWT.NONE);
createChildren(composite, header);
while (composite.getParent() != scrolledComposite) {
composite = composite.getParent();
}
scrolledComposite.setContent(composite);
scrolledComposite.setMinSize(composite.computeSize(SWT.DEFAULT, SWT.DEFAULT));
}
if (performLayout) {
layout(true);
}
}
/** Returns the header object for the given header label */
private Object getHeader(Control label) {
return label.getData(KEY_HEADER);
}
/** Sets the header object for the given header label */
private void setHeader(Object header, final CLabel label) {
label.setData(KEY_HEADER, header);
}
/** Returns the content area for the given header label */
private ScrolledComposite getContentArea(Control label) {
return (ScrolledComposite) label.getData(KEY_CONTENT);
}
/** Sets the content area for the given header label */
private void setContentArea(final CLabel label, ScrolledComposite scrolledComposite) {
label.setData(KEY_CONTENT, scrolledComposite);
}
@Override
protected void checkSubclass() {
// Disable the check that prevents subclassing of SWT components
}
/**
* Returns the set of expanded categories in the palette. Note: Header labels will have
* escaped ampersand characters with double ampersands.
*
* @return the set of expanded categories in the palette - never null
*/
public Set<String> getExpandedCategories() {
Set<String> expanded = new HashSet<String>();
for (Control c : getChildren()) {
if (c instanceof CLabel) {
if (isOpen(c)) {
expanded.add(((CLabel) c).getText());
}
}
}
return expanded;
}
}