1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2; 18 19 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 20 21 import org.eclipse.jface.resource.JFaceResources; 22 import org.eclipse.swt.SWT; 23 import org.eclipse.swt.custom.CLabel; 24 import org.eclipse.swt.custom.ScrolledComposite; 25 import org.eclipse.swt.events.ControlAdapter; 26 import org.eclipse.swt.events.ControlEvent; 27 import org.eclipse.swt.events.MouseAdapter; 28 import org.eclipse.swt.events.MouseEvent; 29 import org.eclipse.swt.events.MouseTrackListener; 30 import org.eclipse.swt.graphics.Color; 31 import org.eclipse.swt.graphics.Font; 32 import org.eclipse.swt.graphics.Image; 33 import org.eclipse.swt.graphics.Point; 34 import org.eclipse.swt.graphics.Rectangle; 35 import org.eclipse.swt.layout.GridData; 36 import org.eclipse.swt.layout.GridLayout; 37 import org.eclipse.swt.layout.RowLayout; 38 import org.eclipse.swt.widgets.Composite; 39 import org.eclipse.swt.widgets.Control; 40 import org.eclipse.swt.widgets.Display; 41 import org.eclipse.swt.widgets.ScrollBar; 42 43 import java.util.ArrayList; 44 import java.util.HashSet; 45 import java.util.List; 46 import java.util.Set; 47 48 /** 49 * The accordion control allows a series of labels with associated content that can be 50 * shown. For more details on accordions, see http://en.wikipedia.org/wiki/Accordion_(GUI) 51 * <p> 52 * This control allows the children to be created lazily. You can also customize the 53 * composite which is created to hold the children items, to for example allow multiple 54 * columns of items rather than just the default vertical stack. 55 * <p> 56 * The visual appearance of the headers is built in; it uses a mild gradient, with a 57 * heavier gradient during mouse-overs. It also uses a bold label along with the eclipse 58 * folder icons. 59 * <p> 60 * The control can be configured to enforce a single category open at any time (the 61 * default), or allowing multiple categories open (where they share the available space). 62 * The control can also be configured to fill the available vertical space for the open 63 * category/categories. 64 */ 65 public abstract class AccordionControl extends Composite { 66 /** Pixel spacing between header items */ 67 private static final int HEADER_SPACING = 0; 68 69 /** Pixel spacing between items in the content area */ 70 private static final int ITEM_SPACING = 0; 71 72 private static final String KEY_CONTENT = "content"; //$NON-NLS-1$ 73 private static final String KEY_HEADER = "header"; //$NON-NLS-1$ 74 75 private Image mClosed; 76 private Image mOpen; 77 private boolean mSingle = true; 78 private boolean mWrap; 79 80 /** 81 * Creates the container which will hold the items in a category; this can be 82 * overridden to lay out the children with a different layout than the default 83 * vertical RowLayout 84 */ createChildContainer(Composite parent, Object header, int style)85 protected Composite createChildContainer(Composite parent, Object header, int style) { 86 Composite composite = new Composite(parent, style); 87 if (mWrap) { 88 RowLayout layout = new RowLayout(SWT.HORIZONTAL); 89 layout.center = true; 90 composite.setLayout(layout); 91 } else { 92 RowLayout layout = new RowLayout(SWT.VERTICAL); 93 layout.spacing = ITEM_SPACING; 94 layout.marginHeight = 0; 95 layout.marginWidth = 0; 96 layout.marginLeft = 0; 97 layout.marginTop = 0; 98 layout.marginRight = 0; 99 layout.marginBottom = 0; 100 composite.setLayout(layout); 101 } 102 103 // TODO - maybe do multi-column arrangement for simple nodes 104 return composite; 105 } 106 107 /** 108 * Creates the children under a particular header 109 * 110 * @param parent the parent composite to add the SWT items to 111 * @param header the header object that is being opened for the first time 112 */ createChildren(Composite parent, Object header)113 protected abstract void createChildren(Composite parent, Object header); 114 115 /** 116 * Set whether a single category should be enforced or not (default=true) 117 * 118 * @param single if true, enforce a single category open at a time 119 */ setAutoClose(boolean single)120 public void setAutoClose(boolean single) { 121 mSingle = single; 122 } 123 124 /** 125 * Returns whether a single category should be enforced or not (default=true) 126 * 127 * @return true if only a single category can be open at a time 128 */ isAutoClose()129 public boolean isAutoClose() { 130 return mSingle; 131 } 132 133 /** 134 * Returns the labels used as header categories 135 * 136 * @return list of header labels 137 */ getHeaderLabels()138 public List<CLabel> getHeaderLabels() { 139 List<CLabel> headers = new ArrayList<CLabel>(); 140 for (Control c : getChildren()) { 141 if (c instanceof CLabel) { 142 headers.add((CLabel) c); 143 } 144 } 145 146 return headers; 147 } 148 149 /** 150 * Show all categories 151 * 152 * @param performLayout if true, call {@link #layout} and {@link #pack} when done 153 */ expandAll(boolean performLayout)154 public void expandAll(boolean performLayout) { 155 for (Control c : getChildren()) { 156 if (c instanceof CLabel) { 157 if (!isOpen(c)) { 158 toggle((CLabel) c, false, false); 159 } 160 } 161 } 162 if (performLayout) { 163 pack(); 164 layout(); 165 } 166 } 167 168 /** 169 * Hide all categories 170 * 171 * @param performLayout if true, call {@link #layout} and {@link #pack} when done 172 */ collapseAll(boolean performLayout)173 public void collapseAll(boolean performLayout) { 174 for (Control c : getChildren()) { 175 if (c instanceof CLabel) { 176 if (isOpen(c)) { 177 toggle((CLabel) c, false, false); 178 } 179 } 180 } 181 if (performLayout) { 182 layout(); 183 } 184 } 185 186 /** 187 * Create the composite. 188 * 189 * @param parent the parent widget to add the accordion to 190 * @param style the SWT style mask to use 191 * @param headers a list of headers, whose {@link Object#toString} method should 192 * produce the heading label 193 * @param greedy if true, grow vertically as much as possible 194 * @param wrapChildren if true, configure the child area to be horizontally laid out 195 * with wrapping 196 * @param expand Set of headers to expand initially 197 */ AccordionControl(Composite parent, int style, List<?> headers, boolean greedy, boolean wrapChildren, Set<String> expand)198 public AccordionControl(Composite parent, int style, List<?> headers, 199 boolean greedy, boolean wrapChildren, Set<String> expand) { 200 super(parent, style); 201 mWrap = wrapChildren; 202 203 GridLayout gridLayout = new GridLayout(1, false); 204 gridLayout.verticalSpacing = HEADER_SPACING; 205 gridLayout.horizontalSpacing = 0; 206 gridLayout.marginWidth = 0; 207 gridLayout.marginHeight = 0; 208 setLayout(gridLayout); 209 210 Font labelFont = null; 211 212 mOpen = IconFactory.getInstance().getIcon("open-folder"); //$NON-NLS-1$ 213 mClosed = IconFactory.getInstance().getIcon("closed-folder"); //$NON-NLS-1$ 214 List<CLabel> expandLabels = new ArrayList<CLabel>(); 215 216 for (Object header : headers) { 217 final CLabel label = new CLabel(this, SWT.SHADOW_OUT); 218 label.setText(header.toString().replace("&", "&&")); //$NON-NLS-1$ //$NON-NLS-2$ 219 updateBackground(label, false); 220 if (labelFont == null) { 221 labelFont = JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT); 222 } 223 label.setFont(labelFont); 224 label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); 225 setHeader(header, label); 226 label.addMouseListener(new MouseAdapter() { 227 @Override 228 public void mouseUp(MouseEvent e) { 229 if (e.button == 1 && (e.stateMask & SWT.MODIFIER_MASK) == 0) { 230 toggle(label, true, mSingle); 231 } 232 } 233 }); 234 label.addMouseTrackListener(new MouseTrackListener() { 235 @Override 236 public void mouseEnter(MouseEvent e) { 237 updateBackground(label, true); 238 } 239 240 @Override 241 public void mouseExit(MouseEvent e) { 242 updateBackground(label, false); 243 } 244 245 @Override 246 public void mouseHover(MouseEvent e) { 247 } 248 }); 249 250 // Turn off border? 251 final ScrolledComposite scrolledComposite = new ScrolledComposite(this, SWT.V_SCROLL); 252 ScrollBar verticalBar = scrolledComposite.getVerticalBar(); 253 verticalBar.setIncrement(20); 254 verticalBar.setPageIncrement(100); 255 256 // Do we need the scrolled composite or can we just look at the next 257 // wizard in the hierarchy? 258 259 setContentArea(label, scrolledComposite); 260 scrolledComposite.setExpandHorizontal(true); 261 scrolledComposite.setExpandVertical(true); 262 GridData scrollGridData = new GridData(SWT.FILL, 263 greedy ? SWT.FILL : SWT.TOP, false, greedy, 1, 1); 264 scrollGridData.exclude = true; 265 scrollGridData.grabExcessHorizontalSpace = wrapChildren; 266 scrolledComposite.setLayoutData(scrollGridData); 267 268 if (wrapChildren) { 269 scrolledComposite.addControlListener(new ControlAdapter() { 270 @Override 271 public void controlResized(ControlEvent e) { 272 Rectangle r = scrolledComposite.getClientArea(); 273 Control content = scrolledComposite.getContent(); 274 if (content != null && r != null) { 275 Point minSize = content.computeSize(r.width, SWT.DEFAULT); 276 scrolledComposite.setMinSize(minSize); 277 ScrollBar vBar = scrolledComposite.getVerticalBar(); 278 vBar.setPageIncrement(r.height); 279 } 280 } 281 }); 282 } 283 284 updateIcon(label); 285 if (expand != null && expand.contains(label.getText())) { 286 // Comparing "label.getText()" rather than "header" because we make some 287 // tweaks to the label (replacing & with && etc) and in the getExpandedCategories 288 // method we return the label texts 289 expandLabels.add(label); 290 } 291 } 292 293 // Expand the requested categories 294 for (CLabel label : expandLabels) { 295 toggle(label, false, false); 296 } 297 } 298 299 /** Updates the background gradient of the given header label */ updateBackground(CLabel label, boolean mouseOver)300 private void updateBackground(CLabel label, boolean mouseOver) { 301 Display display = label.getDisplay(); 302 label.setBackground(new Color[] { 303 display.getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW), 304 display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND), 305 display.getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW) 306 }, new int[] { 307 mouseOver ? 60 : 40, 100 308 }, true); 309 } 310 311 /** 312 * Updates the icon for a header label to be open/close based on the {@link #isOpen} 313 * state 314 */ updateIcon(CLabel label)315 private void updateIcon(CLabel label) { 316 label.setImage(isOpen(label) ? mOpen : mClosed); 317 } 318 319 /** Returns true if the content area for the given label is open/showing */ isOpen(Control label)320 private boolean isOpen(Control label) { 321 return !((GridData) getContentArea(label).getLayoutData()).exclude; 322 } 323 324 /** Toggles the visibility of the children of the given label */ toggle(CLabel label, boolean performLayout, boolean autoClose)325 private void toggle(CLabel label, boolean performLayout, boolean autoClose) { 326 if (autoClose) { 327 collapseAll(true); 328 } 329 ScrolledComposite scrolledComposite = getContentArea(label); 330 331 GridData scrollGridData = (GridData) scrolledComposite.getLayoutData(); 332 boolean close = !scrollGridData.exclude; 333 scrollGridData.exclude = close; 334 scrolledComposite.setVisible(!close); 335 updateIcon(label); 336 337 if (!scrollGridData.exclude && scrolledComposite.getContent() == null) { 338 Object header = getHeader(label); 339 Composite composite = createChildContainer(scrolledComposite, header, SWT.NONE); 340 createChildren(composite, header); 341 while (composite.getParent() != scrolledComposite) { 342 composite = composite.getParent(); 343 } 344 scrolledComposite.setContent(composite); 345 scrolledComposite.setMinSize(composite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); 346 } 347 348 if (performLayout) { 349 layout(true); 350 } 351 } 352 353 /** Returns the header object for the given header label */ getHeader(Control label)354 private Object getHeader(Control label) { 355 return label.getData(KEY_HEADER); 356 } 357 358 /** Sets the header object for the given header label */ setHeader(Object header, final CLabel label)359 private void setHeader(Object header, final CLabel label) { 360 label.setData(KEY_HEADER, header); 361 } 362 363 /** Returns the content area for the given header label */ getContentArea(Control label)364 private ScrolledComposite getContentArea(Control label) { 365 return (ScrolledComposite) label.getData(KEY_CONTENT); 366 } 367 368 /** Sets the content area for the given header label */ setContentArea(final CLabel label, ScrolledComposite scrolledComposite)369 private void setContentArea(final CLabel label, ScrolledComposite scrolledComposite) { 370 label.setData(KEY_CONTENT, scrolledComposite); 371 } 372 373 @Override checkSubclass()374 protected void checkSubclass() { 375 // Disable the check that prevents subclassing of SWT components 376 } 377 378 /** 379 * Returns the set of expanded categories in the palette. Note: Header labels will have 380 * escaped ampersand characters with double ampersands. 381 * 382 * @return the set of expanded categories in the palette - never null 383 */ getExpandedCategories()384 public Set<String> getExpandedCategories() { 385 Set<String> expanded = new HashSet<String>(); 386 for (Control c : getChildren()) { 387 if (c instanceof CLabel) { 388 if (isOpen(c)) { 389 expanded.add(((CLabel) c).getText()); 390 } 391 } 392 } 393 394 return expanded; 395 } 396 } 397