/* * 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.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.FontData; 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) { Composite composite = new Composite(parent, SWT.NONE); 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 = label.getFont(); FontData normal = labelFont.getFontData()[0]; FontData bold = new FontData(normal.getName(), normal.getHeight(), SWT.BOLD); labelFont = new Font(null, bold); } 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() { public void mouseEnter(MouseEvent e) { updateBackground(label, true); } public void mouseExit(MouseEvent e) { updateBackground(label, false); } 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) { Composite composite = createChildContainer(scrolledComposite); Object header = getHeader(label); createChildren(composite, header); 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; } }