1 /*******************************************************************************
2  * Copyright (c) 2000, 2016 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *     Kai Nacke - Fix for Bug 202382
14  *     Bryan Hunt - Fix for Bug 245457
15  *     Didier Villevalois - Fix for Bug 178534
16  *     Robin Stocker - Fix for Bug 193034 (tool tip also on text)
17  *     Alena Laskavaia - Bug 481604, Bug 482024
18  *     Ralf Petter <ralf.petter@gmail.com> - Bug 183675
19  *     Jens Reimann <jreimann@redhat.com> - Bug 520833
20  *******************************************************************************/
21 package org.eclipse.ui.forms.widgets;
22 
23 import org.eclipse.core.runtime.Assert;
24 import org.eclipse.core.runtime.ListenerList;
25 import org.eclipse.swt.SWT;
26 import org.eclipse.swt.events.FocusEvent;
27 import org.eclipse.swt.events.FocusListener;
28 import org.eclipse.swt.events.KeyAdapter;
29 import org.eclipse.swt.events.KeyEvent;
30 import org.eclipse.swt.events.PaintEvent;
31 import org.eclipse.swt.graphics.Color;
32 import org.eclipse.swt.graphics.Font;
33 import org.eclipse.swt.graphics.GC;
34 import org.eclipse.swt.graphics.Point;
35 import org.eclipse.swt.graphics.Rectangle;
36 import org.eclipse.swt.widgets.Canvas;
37 import org.eclipse.swt.widgets.Composite;
38 import org.eclipse.swt.widgets.Control;
39 import org.eclipse.swt.widgets.Label;
40 import org.eclipse.swt.widgets.Layout;
41 import org.eclipse.swt.widgets.Listener;
42 import org.eclipse.swt.widgets.Menu;
43 import org.eclipse.swt.widgets.Shell;
44 import org.eclipse.ui.forms.events.ExpansionEvent;
45 import org.eclipse.ui.forms.events.HyperlinkAdapter;
46 import org.eclipse.ui.forms.events.HyperlinkEvent;
47 import org.eclipse.ui.forms.events.IExpansionListener;
48 import org.eclipse.ui.internal.forms.widgets.FormUtil;
49 import org.eclipse.ui.internal.forms.widgets.FormsResources;
50 
51 /**
52  * This composite is capable of expanding or collapsing a single client that is
53  * its direct child. The composite renders an expansion toggle affordance
54  * (according to the chosen style), and a title that also acts as a hyperlink
55  * (can be selected and is traversable). The client is laid out below the title
56  * when expanded, or hidden when collapsed.
57  * <p>
58  * The widget can be instantiated as-is, or subclassed to modify some aspects of
59  * it. *
60  * <p>
61  * Since 3.1, left/right arrow keys can be used to control the expansion state.
62  * If several expandable composites are created in the same parent, up/down
63  * arrow keys can be used to traverse between them. Expandable text accepts
64  * mnemonics and mnemonic activation will toggle the expansion state.
65  *
66  * <p>
67  * While expandable composite recognize that different styles can be used to
68  * render the title bar, and even defines the constants for these styles
69  * (<code>TITLE_BAR</code> and <code>SHORT_TITLE_BAR</code> the actual painting
70  * is done in the subclasses.
71  *
72  * @see Section
73  * @since 3.0
74  */
75 public class ExpandableComposite extends Canvas {
76 	/**
77 	 * If this style is used, a twistie will be used to render the expansion
78 	 * toggle.
79 	 */
80 	public static final int TWISTIE = 1 << 1;
81 
82 	/**
83 	 * If this style is used, a tree node with either + or - signs will be used
84 	 * to render the expansion toggle.
85 	 */
86 	public static final int TREE_NODE = 1 << 2;
87 
88 	/**
89 	 * If this style is used, the title text will be rendered as a hyperlink
90 	 * that can individually accept focus. Otherwise, it will still act like a
91 	 * hyperlink, but only the toggle control will accept focus.
92 	 */
93 	public static final int FOCUS_TITLE = 1 << 3;
94 
95 	/**
96 	 * If this style is used, the client origin will be vertically aligned with
97 	 * the title text. Otherwise, it will start at x = 0.
98 	 */
99 	public static final int CLIENT_INDENT = 1 << 4;
100 
101 	/**
102 	 * If this style is used, computed size of the composite will take the
103 	 * client width into consideration only in the expanded state. Otherwise,
104 	 * client width will always be taken into account.
105 	 */
106 	public static final int COMPACT = 1 << 5;
107 
108 	/**
109 	 * If this style is used, the control will be created in the expanded state.
110 	 * This state can later be changed programmatically or by the user if
111 	 * TWISTIE or TREE_NODE style is used.
112 	 */
113 	public static final int EXPANDED = 1 << 6;
114 
115 	/**
116 	 * If this style is used, title bar decoration will be painted behind the
117 	 * text.
118 	 */
119 	public static final int TITLE_BAR = 1 << 8;
120 
121 	/**
122 	 * If this style is used, a short version of the title bar decoration will
123 	 * be painted behind the text. This style is useful when a more discrete
124 	 * option is needed for the title bar.
125 	 *
126 	 * @since 3.1
127 	 */
128 	public static final int SHORT_TITLE_BAR = 1 << 9;
129 
130 	/**
131 	 * If this style is used, title will not be rendered.
132 	 */
133 	public static final int NO_TITLE = 1 << 12;
134 
135 	/**
136 	 * By default, text client is right-aligned. If this style is used, it will
137 	 * be positioned after the text control and vertically centered with it.
138 	 */
139 	public static final int LEFT_TEXT_CLIENT_ALIGNMENT = 1 << 13;
140 
141 	/**
142 	 * By default, a focus box is painted around the title when it receives focus.
143 	 * If this style is used, the focus box will not be painted.  This style does
144 	 * not apply when FOCUS_TITLE is used.
145 	 * @since 3.5
146 	 */
147 	public static final int NO_TITLE_FOCUS_BOX = 1 << 14;
148 
149 	/**
150 	 * Width of the margin that will be added around the control (default is 0).
151 	 */
152 	public int marginWidth = 0;
153 
154 	/**
155 	 * Height of the margin that will be added around the control (default is
156 	 * 0).
157 	 */
158 	public int marginHeight = 0;
159 
160 	/**
161 	 * Vertical spacing between the title area and the composite client control
162 	 * (default is 3).
163 	 */
164 	public int clientVerticalSpacing = 3;
165 
166 	/**
167 	 * Vertical spacing between the title area and the description control
168 	 * (default is 0). The description control is normally placed at the new
169 	 * line as defined in the font used to render it. This value will be added
170 	 * to it.
171 	 *
172 	 * @since 3.3
173 	 */
174 	public int descriptionVerticalSpacing = 0;
175 
176 	/**
177 	 * Horizontal margin around the inside of the title bar area when TITLE_BAR
178 	 * or SHORT_TITLE_BAR style is used. This variable is not used otherwise.
179 	 *
180 	 * @since 3.3
181 	 */
182 	public int titleBarTextMarginWidth = 6;
183 
184 	/**
185 	 * The toggle widget used to expand the composite.
186 	 */
187 	protected ToggleHyperlink toggle;
188 
189 	/**
190 	 * The text label for the title.
191 	 */
192 	protected Control textLabel;
193 
194 	/**
195 	 * @deprecated this variable was left as protected by mistake. It will be
196 	 *             turned into static and hidden in the future versions. Do not
197 	 *             use them and do not change its value.
198 	 */
199 	@Deprecated
200 	protected int VGAP = 3;
201 	/**
202 	 * @deprecated this variable was left as protected by mistake. It will be
203 	 *             turned into static and hidden in the future versions. Do not
204 	 *             use it and do not change its value.
205 	 */
206 	@Deprecated
207 	protected int GAP = 4;
208 
209 	static final int IGAP = 4;
210 	static final int IVGAP = 3;
211 
212 	private static final Point NULL_SIZE = new Point(0, 0);
213 
214 	private static final int VSPACE = 3;
215 
216 	private static final int SEPARATOR_HEIGHT = 2;
217 
218 	private int expansionStyle = TWISTIE | FOCUS_TITLE | EXPANDED;
219 
220 	private boolean expanded;
221 
222 	private Control textClient;
223 
224 	private Control client;
225 
226 	private ListenerList<IExpansionListener> listeners = new ListenerList<>();
227 
228 	private Color titleBarForeground;
229 
230 	private class ExpandableLayout extends Layout implements ILayoutExtension {
231 
232 		private static final int MIN_WIDTH = -2;
233 
234 		private SizeCache toggleCache = new SizeCache();
235 
236 		private SizeCache textClientCache = new SizeCache();
237 
238 		private SizeCache textLabelCache = new SizeCache();
239 
240 		private SizeCache descriptionCache = new SizeCache();
241 
242 		private SizeCache clientCache = new SizeCache();
243 
initCache(boolean shouldFlush)244 		private void initCache(boolean shouldFlush) {
245 			toggleCache.setControl(toggle);
246 			textClientCache.setControl(textClient);
247 			textLabelCache.setControl(textLabel);
248 			descriptionCache.setControl(getDescriptionControl());
249 			clientCache.setControl(client);
250 
251 			if (shouldFlush) {
252 				toggleCache.flush();
253 				textClientCache.flush();
254 				textLabelCache.flush();
255 				descriptionCache.flush();
256 				clientCache.flush();
257 			}
258 		}
259 
260 		@Override
layout(Composite parent, boolean changed)261 		protected void layout(Composite parent, boolean changed) {
262 			initCache(changed);
263 
264 			Rectangle clientArea = parent.getClientArea();
265 			int thmargin = 0;
266 			int tvmargin = 0;
267 
268 			if (hasTitleBar()) {
269 				thmargin = titleBarTextMarginWidth;
270 				tvmargin = IVGAP;
271 			}
272 			int x = marginWidth + thmargin;
273 			int y = marginHeight + tvmargin;
274 			// toggle
275 			Point toggleSize = toggleCache.computeSize(SWT.DEFAULT, SWT.DEFAULT);
276 
277 			int width = clientArea.width - marginWidth - marginWidth - thmargin - thmargin;
278 			if (toggleSize.x > 0)
279 				width -= toggleSize.x + IGAP;
280 
281 			// TODO: This code is common between computeSize and layout
282 			int gapBetweenTcAndLabel = (textClient != null && textLabel != null) ? IGAP : 0;
283 
284 			int widthForTcAndLabel = Math.max(0, width - gapBetweenTcAndLabel);
285 
286 			Point tcDefault = textClientCache.computeSize(SWT.DEFAULT, SWT.DEFAULT);
287 			Point labelDefault = this.textLabelCache.computeSize(SWT.DEFAULT, SWT.DEFAULT);
288 
289 			int tcWidthBeforeSplit = Math.min(width, tcDefault.x);
290 			int labelWidthBeforeSplit = Math.min(width, labelDefault.x);
291 
292 			int tcWidthAfterSplit = tcWidthBeforeSplit;
293 			int labelWidthAfterSplit = labelWidthBeforeSplit;
294 
295 			int expectedWidthForTcAndLabel = tcWidthBeforeSplit + labelWidthBeforeSplit;
296 
297 			if (expectedWidthForTcAndLabel > widthForTcAndLabel) {
298 				// this is heuristic since we don't have a reliable way to find
299 				// out if control can wrap. It checks if width of each label or
300 				// textClient is less then half
301 				// and gives them what they asked in this case
302 				if (labelWidthBeforeSplit < widthForTcAndLabel / 2) {
303 					labelWidthAfterSplit = labelWidthBeforeSplit;
304 				} else {
305 					labelWidthAfterSplit = widthForTcAndLabel * labelWidthBeforeSplit
306 							/ expectedWidthForTcAndLabel;
307 				}
308 
309 				if (tcWidthBeforeSplit < widthForTcAndLabel / 2) {
310 					tcWidthAfterSplit = tcWidthBeforeSplit;
311 					labelWidthAfterSplit = widthForTcAndLabel - tcWidthAfterSplit;
312 				} else {
313 					tcWidthAfterSplit = widthForTcAndLabel - labelWidthAfterSplit;
314 				}
315 			}
316 
317 			// TODO: Add support for fill alignment of textControl
318 
319 			Point tcsize = textClientCache.computeSize(tcWidthAfterSplit, SWT.DEFAULT);
320 			Point size = textLabelCache.computeSize(labelWidthAfterSplit, SWT.DEFAULT);
321 
322 			int height = Math.max(tcsize.y, size.y); // max of label/text client
323 			height = Math.max(height, toggleSize.y); // or max of toggle
324 
325 			boolean leftAlignment = textClient != null && (expansionStyle & LEFT_TEXT_CLIENT_ALIGNMENT) != 0;
326 			if (toggle != null) {
327 				// if label control is absent we vertically center the toggle,
328 				// because the text client is usually a lot thicker
329 				int ty = (height - toggleSize.y + 1) / 2 + 1;
330 				ty = Math.max(ty, 0);
331 				ty += marginHeight + tvmargin;
332 				toggle.setLocation(x, ty);
333 				toggle.setSize(toggleSize);
334 				x += toggleSize.x + IGAP;
335 			}
336 			if (textLabel != null) {
337 				int ty = y;
338 				if (leftAlignment) {
339 					if (size.y < tcsize.y)
340 						ty = (tcsize.y - size.y) / 2 + marginHeight
341 								+ tvmargin;
342 				}
343 
344 				int gap = 0;
345 				if (size.y < height) {
346 					// bug 520833: the text label is smaller in height
347 					// than the composite so we align it to the middle
348 					gap = (height - size.y) / 2;
349 				}
350 
351 				textLabelCache.setBounds(x, ty + gap, size.x, size.y);
352 			}
353 
354 			if (textClient != null) {
355 				int tcwidth = clientArea.width - marginWidth - marginWidth - thmargin - thmargin;
356 				if (toggleSize.x > 0)
357 					tcwidth -= toggleSize.x + IGAP;
358 				if (size.x > 0)
359 					tcwidth -= size.x + IGAP;
360 				tcwidth = Math.min(tcsize.x, tcwidth);
361 				if (tcwidth < 0)
362 					tcwidth = 0;
363 				int tcx;
364 				if ((expansionStyle & LEFT_TEXT_CLIENT_ALIGNMENT) != 0) {
365 					tcx = x + ((size.x > 0) ? size.x + IGAP : 0);
366 				} else {
367 					tcx = clientArea.width - tcwidth - marginWidth - thmargin;
368 				}
369 				textClientCache.setBounds(tcx, y, tcwidth, height);
370 			}
371 
372 			y += height;
373 			if (hasTitleBar())
374 				y += tvmargin;
375 			Control separatorControl = getSeparatorControl();
376 			if (separatorControl != null) {
377 				y += VSPACE;
378 				separatorControl.setBounds(marginWidth, y,
379 						clientArea.width - marginWidth - marginWidth,
380 						SEPARATOR_HEIGHT);
381 				y += SEPARATOR_HEIGHT;
382 			}
383 			if (expanded && client != null) {
384 				int areaWidth = clientArea.width - marginWidth - thmargin;
385 				int cx = marginWidth + thmargin;
386 				if ((expansionStyle & CLIENT_INDENT) != 0) {
387 					cx = x;
388 				}
389 				areaWidth -= cx;
390 				Control desc = getDescriptionControl();
391 				if (desc != null) {
392 					if (separatorControl != null) {
393 						y += VSPACE;
394 					}
395 					Point dsize = descriptionCache.computeSize(areaWidth, SWT.DEFAULT);
396 					y += descriptionVerticalSpacing;
397 					descriptionCache.setBounds(cx, y, areaWidth, dsize.y);
398 					y += dsize.y;
399 				}
400 				y += clientVerticalSpacing;
401 				int cwidth = areaWidth;
402 				int cheight = clientArea.height - marginHeight - marginHeight - y;
403 				clientCache.setBounds(cx, y, cwidth, cheight);
404 			}
405 		}
406 
407 		@Override
computeSize(Composite parent, int wHint, int hHint, boolean changed)408 		protected Point computeSize(Composite parent, int wHint, int hHint,
409 				boolean changed) {
410 			initCache(changed);
411 
412 			Point toggleSize = NULL_SIZE;
413 			int toggleWidthPlusGap = 0;
414 			if (toggle != null) {
415 				toggleSize = toggleCache.computeSize(SWT.DEFAULT, SWT.DEFAULT);
416 				toggleWidthPlusGap = toggleSize.x + IGAP;
417 			}
418 			int thmargin = 0;
419 			int tvmargin = 0;
420 
421 			if (hasTitleBar()) {
422 				thmargin = titleBarTextMarginWidth;
423 				tvmargin = IVGAP;
424 			}
425 
426 			// TODO: This code is common between computeSize and layout
427 			int gapBetweenTcAndLabel = (textClient != null && textLabel != null) ? IGAP : 0;
428 
429 			Point tcDefault = textClientCache.computeSize(SWT.DEFAULT, SWT.DEFAULT);
430 			Point labelDefault = this.textLabelCache.computeSize(SWT.DEFAULT, SWT.DEFAULT);
431 
432 			int width = 0;
433 			if (wHint == SWT.DEFAULT || wHint == MIN_WIDTH) {
434 				width += toggleWidthPlusGap;
435 				width += labelDefault.x;
436 				width += gapBetweenTcAndLabel;
437 				width += tcDefault.x;
438 			} else {
439 				width = wHint - marginWidth - marginWidth - thmargin - thmargin;
440 			}
441 
442 			width = Math.max(0, width);
443 
444 			int widthForTcAndLabel = Math.max(0, width - gapBetweenTcAndLabel - toggleWidthPlusGap);
445 
446 			int tcWidthBeforeSplit = Math.min(width, tcDefault.x);
447 			int labelWidthBeforeSplit = Math.min(width, labelDefault.x);
448 
449 			int tcWidthAfterSplit = tcWidthBeforeSplit;
450 			int labelWidthAfterSplit = labelWidthBeforeSplit;
451 
452 			int expectedWidthForTcAndLabel = tcWidthBeforeSplit + labelWidthBeforeSplit;
453 
454 			if (expectedWidthForTcAndLabel > widthForTcAndLabel) {
455 				labelWidthAfterSplit = widthForTcAndLabel * labelWidthBeforeSplit / expectedWidthForTcAndLabel;
456 				tcWidthAfterSplit = widthForTcAndLabel - labelWidthAfterSplit;
457 			}
458 
459 			// TODO: Add support for fill alignment of textControl
460 
461 			Point tcsize = textClientCache.computeSize(tcWidthAfterSplit, SWT.DEFAULT);
462 			Point size = textLabelCache.computeSize(labelWidthAfterSplit, SWT.DEFAULT);
463 
464 			int height = Math.max(tcsize.y, size.y); // max of label/text client
465 			height = Math.max(height, toggleSize.y); // or max of toggle
466 
467 			if (getSeparatorControl() != null) {
468 				height += VSPACE + SEPARATOR_HEIGHT;
469 			}
470 			// if (hasTitleBar())
471 			// height += VSPACE;
472 			if ((expanded || (expansionStyle & COMPACT) == 0) && client != null) {
473 				int cwHint = wHint;
474 				int clientIndent = 0;
475 				if ((expansionStyle & CLIENT_INDENT) != 0)
476 					clientIndent = toggleWidthPlusGap;
477 
478 				if (cwHint != SWT.DEFAULT && cwHint != MIN_WIDTH) {
479 					cwHint -= marginWidth + marginWidth + thmargin + thmargin;
480 					if ((expansionStyle & CLIENT_INDENT) != 0)
481 						if (tcsize.x > 0)
482 							cwHint -= toggleWidthPlusGap;
483 				}
484 				Point dsize = null;
485 				Point csize;
486 				if (cwHint == MIN_WIDTH) {
487 					int minWidth = clientCache.computeMinimumWidth();
488 					csize = clientCache.computeSize(minWidth, SWT.DEFAULT);
489 				} else {
490 					csize = clientCache.computeSize(cwHint, SWT.DEFAULT);
491 				}
492 				if (getDescriptionControl() != null) {
493 					int dwHint = cwHint;
494 					if (dwHint == SWT.DEFAULT || dwHint == MIN_WIDTH) {
495 						dwHint = csize.x;
496 						if ((expansionStyle & CLIENT_INDENT) != 0)
497 							dwHint -= toggleWidthPlusGap;
498 					}
499 					dsize = descriptionCache.computeSize(dwHint, SWT.DEFAULT);
500 					width = Math.max(width, dsize.x + clientIndent);
501 					if (expanded) {
502 						if (getSeparatorControl() != null) {
503 							height += VSPACE;
504 						}
505 						height += descriptionVerticalSpacing + dsize.y;
506 					}
507 				}
508 				width = Math.max(width, csize.x + clientIndent);
509 				if (expanded) {
510 					height += clientVerticalSpacing;
511 					height += csize.y;
512 				}
513 			}
514 
515 			int resultWidth = width + marginWidth + marginWidth + thmargin + thmargin;
516 
517 			if (wHint != SWT.DEFAULT && wHint != MIN_WIDTH) {
518 				resultWidth = wHint;
519 			}
520 
521 			int resultHeight = height + marginHeight + marginHeight + tvmargin + tvmargin;
522 
523 			if (hHint != SWT.DEFAULT) {
524 				resultHeight = hHint;
525 			}
526 
527 			return new Point(resultWidth, resultHeight);
528 		}
529 
530 		@Override
computeMinimumWidth(Composite parent, boolean changed)531 		public int computeMinimumWidth(Composite parent, boolean changed) {
532 			return computeSize(parent, MIN_WIDTH, SWT.DEFAULT, changed).x;
533 		}
534 
535 		@Override
computeMaximumWidth(Composite parent, boolean changed)536 		public int computeMaximumWidth(Composite parent, boolean changed) {
537 			return computeSize(parent, SWT.DEFAULT, SWT.DEFAULT, changed).x;
538 		}
539 	}
540 
541 	/**
542 	 * Creates an expandable composite using a TWISTIE toggle.
543 	 *
544 	 * @param parent
545 	 *            the parent composite
546 	 * @param style
547 	 *            SWT style bits
548 	 */
ExpandableComposite(Composite parent, int style)549 	public ExpandableComposite(Composite parent, int style) {
550 		this(parent, style, TWISTIE);
551 	}
552 
553 	/**
554 	 * Creates the expandable composite in the provided parent.
555 	 *
556 	 * @param parent
557 	 *            the parent
558 	 * @param style
559 	 *            the control style (as expected by SWT subclass)
560 	 * @param expansionStyle
561 	 *            the style of the expansion widget (TREE_NODE, TWISTIE,
562 	 *            CLIENT_INDENT, COMPACT, FOCUS_TITLE,
563 	 *            LEFT_TEXT_CLIENT_ALIGNMENT, NO_TITLE)
564 	 */
ExpandableComposite(Composite parent, int style, int expansionStyle)565 	public ExpandableComposite(Composite parent, int style, int expansionStyle) {
566 		super(parent, style);
567 		this.expansionStyle = expansionStyle;
568 		if ((expansionStyle & TITLE_BAR) != 0)
569 			setBackgroundMode(SWT.INHERIT_DEFAULT);
570 		super.setLayout(new ExpandableLayout());
571 		if (hasTitleBar()) {
572 			this.addPaintListener(e -> {
573 				if (!isDisposed()) {
574 					onPaint(e);
575 				}
576 			});
577 		}
578 		if ((expansionStyle & TWISTIE) != 0)
579 			toggle = new Twistie(this, SWT.NULL);
580 		else if ((expansionStyle & TREE_NODE) != 0)
581 			toggle = new TreeNode(this, SWT.NULL);
582 		else
583 			expanded = true;
584 		if ((expansionStyle & EXPANDED) != 0)
585 			expanded = true;
586 		if (toggle != null) {
587 			toggle.setExpanded(expanded);
588 			toggle.addHyperlinkListener(new HyperlinkAdapter() {
589 				@Override
590 				public void linkActivated(HyperlinkEvent e) {
591 					toggleState();
592 				}
593 			});
594 			toggle.addPaintListener(e -> {
595 				if (textLabel instanceof Label && !isFixedStyle())
596 					if (toggle.hover) {
597 						textLabel.setForeground(toggle.getHoverDecorationColor());
598 					} else {
599 						textLabel.setForeground(getTitleBarForeground());
600 					}
601 			});
602 			toggle.addKeyListener(new KeyAdapter() {
603 				@Override
604 				public void keyPressed(KeyEvent e) {
605 					if (e.keyCode == SWT.ARROW_UP) {
606 						verticalMove(false);
607 						e.doit = false;
608 					} else if (e.keyCode == SWT.ARROW_DOWN) {
609 						verticalMove(true);
610 						e.doit = false;
611 					}
612 				}
613 			});
614 			if ((getExpansionStyle()&FOCUS_TITLE)==0) {
615 				toggle.paintFocus=false;
616 				toggle.addFocusListener(new FocusListener() {
617 					@Override
618 					public void focusGained(FocusEvent e) {
619 						if (textLabel != null) {
620 							textLabel.redraw();
621 						}
622 					}
623 
624 					@Override
625 					public void focusLost(FocusEvent e) {
626 						if (textLabel != null) {
627 							textLabel.redraw();
628 						}
629 					}
630 				});
631 			}
632 		}
633 		if ((expansionStyle & FOCUS_TITLE) != 0) {
634 			Hyperlink link = new Hyperlink(this, SWT.WRAP);
635 			link.addHyperlinkListener(new HyperlinkAdapter() {
636 				@Override
637 				public void linkActivated(HyperlinkEvent e) {
638 					programmaticToggleState();
639 				}
640 			});
641 			textLabel = link;
642 		} else if ((expansionStyle & NO_TITLE) == 0) {
643 			final Label label = new Label(this, SWT.WRAP);
644 			if (!isFixedStyle()) {
645 				label.setCursor(FormsResources.getHandCursor());
646 				Listener listener = e -> {
647 					switch (e.type) {
648 					case SWT.MouseDown:
649 						if (toggle != null)
650 							toggle.setFocus();
651 						break;
652 					case SWT.MouseUp:
653 						label.setCursor(FormsResources.getBusyCursor());
654 						programmaticToggleState();
655 						label.setCursor(FormsResources.getHandCursor());
656 						break;
657 					case SWT.MouseEnter:
658 						if (toggle != null) {
659 							label.setForeground(toggle.getHoverDecorationColor());
660 							toggle.hover = true;
661 							toggle.redraw();
662 						}
663 						break;
664 					case SWT.MouseExit:
665 						if (toggle != null) {
666 							label.setForeground(getTitleBarForeground());
667 							toggle.hover = false;
668 							toggle.redraw();
669 						}
670 						break;
671 					case SWT.Paint:
672 						if (toggle != null && (getExpansionStyle() & NO_TITLE_FOCUS_BOX) == 0) {
673 							paintTitleFocus(e.gc);
674 						}
675 						break;
676 					}
677 				};
678 				label.addListener(SWT.MouseDown, listener);
679 				label.addListener(SWT.MouseUp, listener);
680 				label.addListener(SWT.MouseEnter, listener);
681 				label.addListener(SWT.MouseExit, listener);
682 				label.addListener(SWT.Paint, listener);
683 			}
684 			textLabel = label;
685 		}
686 		if (textLabel != null) {
687 			textLabel.setMenu(getMenu());
688 			textLabel.addTraverseListener(e -> {
689 				if (e.detail == SWT.TRAVERSE_MNEMONIC) {
690 					// steal the mnemonic
691 					if (!isVisible() || !isEnabled())
692 						return;
693 					if (FormUtil.mnemonicMatch(getText(), e.character)) {
694 						e.doit = false;
695 						if (!isFixedStyle()) {
696 							programmaticToggleState();
697 						}
698 						setFocus();
699 					}
700 				}
701 			});
702 		}
703 	}
704 
705 	@Override
forceFocus()706 	public boolean forceFocus() {
707 		return false;
708 	}
709 
710 	/**
711 	 * Overrides 'super' to pass the menu to the text label.
712 	 *
713 	 * @param menu
714 	 *            the menu from the parent to attach to this control.
715 	 */
716 
717 	@Override
setMenu(Menu menu)718 	public void setMenu(Menu menu) {
719 		if (textLabel != null)
720 			textLabel.setMenu(menu);
721 		super.setMenu(menu);
722 	}
723 
724 	/**
725 	 * Prevents assignment of the layout manager - expandable composite uses its
726 	 * own layout.
727 	 */
728 	@Override
setLayout(Layout layout)729 	public final void setLayout(Layout layout) {
730 	}
731 
732 	/**
733 	 * Sets the background of all the custom controls in the expandable.
734 	 */
735 	@Override
setBackground(Color bg)736 	public void setBackground(Color bg) {
737 		super.setBackground(bg);
738 		if ((getExpansionStyle() & TITLE_BAR) == 0) {
739 			if (textLabel != null)
740 				textLabel.setBackground(bg);
741 			if (toggle != null)
742 				toggle.setBackground(bg);
743 		}
744 	}
745 
746 	/**
747 	 * Sets the foreground of all the custom controls in the expandable.
748 	 */
749 	@Override
setForeground(Color fg)750 	public void setForeground(Color fg) {
751 		super.setForeground(fg);
752 		if (textLabel != null)
753 			textLabel.setForeground(fg);
754 		if (toggle != null)
755 			toggle.setForeground(fg);
756 	}
757 
758 	/**
759 	 * Sets the color of the toggle control.
760 	 *
761 	 * @param c
762 	 *            the color object
763 	 */
setToggleColor(Color c)764 	public void setToggleColor(Color c) {
765 		if (toggle != null)
766 			toggle.setDecorationColor(c);
767 	}
768 
769 	/**
770 	 * Sets the active color of the toggle control (when the mouse enters the
771 	 * toggle area).
772 	 *
773 	 * @param c
774 	 *            the active color object
775 	 */
setActiveToggleColor(Color c)776 	public void setActiveToggleColor(Color c) {
777 		if (toggle != null)
778 			toggle.setHoverDecorationColor(c);
779 	}
780 
781 	/**
782 	 * Sets the fonts of all the custom controls in the expandable.
783 	 */
784 	@Override
setFont(Font font)785 	public void setFont(Font font) {
786 		super.setFont(font);
787 		if (textLabel != null)
788 			textLabel.setFont(font);
789 		if (toggle != null)
790 			toggle.setFont(font);
791 	}
792 
793 	@Override
setEnabled(boolean enabled)794 	public void setEnabled(boolean enabled) {
795 		if (textLabel != null)
796 			textLabel.setEnabled(enabled);
797 		if (toggle != null)
798 			toggle.setEnabled(enabled);
799 		super.setEnabled(enabled);
800 	}
801 
802 	/**
803 	 * Sets the client of this expandable composite. The client must not be
804 	 * <samp>null </samp> and must be a direct child of this container.
805 	 *
806 	 * @param client
807 	 *            the client that will be expanded or collapsed
808 	 */
setClient(Control client)809 	public void setClient(Control client) {
810 		Assert.isTrue(client != null && client.getParent().equals(this));
811 		this.client = client;
812 	}
813 
814 	/**
815 	 * Returns the current expandable client.
816 	 *
817 	 * @return the client control
818 	 */
getClient()819 	public Control getClient() {
820 		return client;
821 	}
822 
823 	/**
824 	 * Sets the title of the expandable composite. The title will act as a
825 	 * hyperlink and activating it will toggle the client between expanded and
826 	 * collapsed state.
827 	 *
828 	 * @param title
829 	 *            the new title string
830 	 * @see #getText()
831 	 */
setText(String title)832 	public void setText(String title) {
833 		if (textLabel instanceof Label) {
834 			((Label) textLabel).setText(title);
835 		} else if (textLabel instanceof Hyperlink) {
836 			((Hyperlink) textLabel).setText(title);
837 		} else {
838 			return;
839 		}
840 		layout();
841 	}
842 
843 	@Override
setToolTipText(String string)844 	public void setToolTipText(String string) {
845 		super.setToolTipText(string);
846 		// Also set on label, otherwise it's just on the background without text.
847 		if (textLabel instanceof Label) {
848 			((Label) textLabel).setToolTipText(string);
849 		} else if (textLabel instanceof Hyperlink) {
850 			((Hyperlink) textLabel).setToolTipText(string);
851 		}
852 	}
853 
854 	/**
855 	 * Returns the title string.
856 	 *
857 	 * @return the title string
858 	 * @see #setText(String)
859 	 */
getText()860 	public String getText() {
861 		if (textLabel instanceof Label)
862 			return ((Label) textLabel).getText();
863 		else if (textLabel instanceof Hyperlink)
864 			return ((Hyperlink) textLabel).getText();
865 		else
866 			return ""; //$NON-NLS-1$
867 	}
868 
869 	/**
870 	 * Tests the expanded state of the composite.
871 	 *
872 	 * @return <samp>true </samp> if expanded, <samp>false </samp> if collapsed.
873 	 */
isExpanded()874 	public boolean isExpanded() {
875 		return expanded;
876 	}
877 
878 	/**
879 	 * Returns the bitwise-ORed style bits for the expansion control.
880 	 *
881 	 * @return the bitwise-ORed style bits for the expansion control
882 	 */
getExpansionStyle()883 	public int getExpansionStyle() {
884 		return expansionStyle;
885 	}
886 
887 	/**
888 	 * Programmatically changes expanded state.
889 	 *
890 	 * @param expanded
891 	 *            the new expanded state
892 	 */
setExpanded(boolean expanded)893 	public void setExpanded(boolean expanded) {
894 		internalSetExpanded(expanded);
895 		if (toggle != null)
896 			toggle.setExpanded(expanded);
897 	}
898 
899 	/**
900 	 * Performs the expansion state change for the expandable control.
901 	 *
902 	 * @param expanded
903 	 *            the expansion state
904 	 */
internalSetExpanded(boolean expanded)905 	protected void internalSetExpanded(boolean expanded) {
906 		if (this.expanded != expanded) {
907 			this.expanded = expanded;
908 			if (getDescriptionControl() != null)
909 				getDescriptionControl().setVisible(expanded);
910 			if (client != null)
911 				client.setVisible(expanded);
912 			reflow();
913 		}
914 	}
915 
916 	/**
917 	 * Adds the listener that will be notified when the expansion state changes.
918 	 *
919 	 * @param listener
920 	 *            the listener to add
921 	 */
addExpansionListener(IExpansionListener listener)922 	public void addExpansionListener(IExpansionListener listener) {
923 		listeners.add(listener);
924 	}
925 
926 	/**
927 	 * Removes the expansion listener.
928 	 *
929 	 * @param listener
930 	 *            the listener to remove
931 	 */
removeExpansionListener(IExpansionListener listener)932 	public void removeExpansionListener(IExpansionListener listener) {
933 		listeners.remove(listener);
934 	}
935 
936 	/**
937 	 * If TITLE_BAR or SHORT_TITLE_BAR style is used, title bar decoration will
938 	 * be painted behind the text in this method. The default implementation
939 	 * does nothing - subclasses are responsible for rendering the title area.
940 	 *
941 	 * @param e
942 	 *            the paint event
943 	 */
onPaint(PaintEvent e)944 	protected void onPaint(PaintEvent e) {
945 	}
946 
947 	/**
948 	 * Returns description control that will be placed under the title if
949 	 * present.
950 	 *
951 	 * @return the description control or <samp>null </samp> if not used.
952 	 */
getDescriptionControl()953 	protected Control getDescriptionControl() {
954 		return null;
955 	}
956 
957 	/**
958 	 * Returns the separator control that will be placed between the title and
959 	 * the description if present.
960 	 *
961 	 * @return the separator control or <samp>null </samp> if not used.
962 	 */
getSeparatorControl()963 	protected Control getSeparatorControl() {
964 		return null;
965 	}
966 
967 	/**
968 	 * Computes the size of the expandable composite.
969 	 *
970 	 * @see org.eclipse.swt.widgets.Composite#computeSize
971 	 */
972 	@Override
computeSize(int wHint, int hHint, boolean changed)973 	public Point computeSize(int wHint, int hHint, boolean changed) {
974 		checkWidget();
975 		Point size;
976 		ExpandableLayout layout = (ExpandableLayout) getLayout();
977 		if (wHint == SWT.DEFAULT || hHint == SWT.DEFAULT) {
978 			size = layout.computeSize(this, wHint, hHint, changed);
979 		} else {
980 			size = new Point(wHint, hHint);
981 		}
982 		Rectangle trim = computeTrim(0, 0, size.x, size.y);
983 		return new Point(trim.width, trim.height);
984 	}
985 
986 	/**
987 	 * Returns <samp>true </samp> if the composite is fixed i.e. cannot be
988 	 * expanded or collapsed. Fixed control will still contain the title,
989 	 * separator and description (if present) as well as the client, but will be
990 	 * in the permanent expanded state and the toggle affordance will not be
991 	 * shown.
992 	 *
993 	 * @return <samp>true </samp> if the control is fixed in the expanded state,
994 	 *         <samp>false </samp> if it can be collapsed.
995 	 */
isFixedStyle()996 	protected boolean isFixedStyle() {
997 		return (expansionStyle & TWISTIE) == 0
998 				&& (expansionStyle & TREE_NODE) == 0;
999 	}
1000 
1001 	/**
1002 	 * Returns the text client control.
1003 	 *
1004 	 * @return Returns the text client control if specified, or
1005 	 *         <code>null</code> if not.
1006 	 */
getTextClient()1007 	public Control getTextClient() {
1008 		return textClient;
1009 	}
1010 
1011 	/**
1012 	 * Sets the text client control. Text client is a control that is a child of
1013 	 * the expandable composite and is placed to the right of the text. It can
1014 	 * be used to place small image hyperlinks. If more than one control is
1015 	 * needed, use Composite to hold them. Care should be taken that the height
1016 	 * of the control is comparable to the height of the text.
1017 	 *
1018 	 * @param textClient
1019 	 *            the textClient to set or <code>null</code> if not needed any
1020 	 *            more.
1021 	 */
setTextClient(Control textClient)1022 	public void setTextClient(Control textClient) {
1023 		if (this.textClient != null)
1024 			this.textClient.dispose();
1025 		this.textClient = textClient;
1026 	}
1027 
1028 	/**
1029 	 * Returns the difference in height between the text and the text client (if
1030 	 * set). This difference can cause vertical alignment problems when two
1031 	 * expandable composites are placed side by side, one with and one without
1032 	 * the text client. Use this method obtain the value to add to either
1033 	 * <code>descriptionVerticalSpacing</code> (if you have description) or
1034 	 * <code>clientVerticalSpacing</code> to correct the alignment of the
1035 	 * expandable without the text client.
1036 	 *
1037 	 * @return the difference in height between the text and the text client or
1038 	 *         0 if no corrective action is needed.
1039 	 * @since 3.3
1040 	 */
getTextClientHeightDifference()1041 	public int getTextClientHeightDifference() {
1042 		if (textClient == null || textLabel == null)
1043 			return 0;
1044 		int theight = textLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
1045 		int tcheight = textClient.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
1046 		return Math.max(tcheight - theight, 0);
1047 	}
1048 
1049 	/**
1050 	 * Tests if this expandable composite renders a title bar around the text.
1051 	 *
1052 	 * @return <code>true</code> for <code>TITLE_BAR</code> or
1053 	 *         <code>SHORT_TITLE_BAR</code> styles, <code>false</code>
1054 	 *         otherwise.
1055 	 */
hasTitleBar()1056 	protected boolean hasTitleBar() {
1057 		return (getExpansionStyle() & TITLE_BAR) != 0
1058 				|| (getExpansionStyle() & SHORT_TITLE_BAR) != 0;
1059 	}
1060 
1061 	/**
1062 	 * Sets the color of the title bar foreground when TITLE_BAR style is used.
1063 	 *
1064 	 * @param color
1065 	 *            the title bar foreground
1066 	 */
setTitleBarForeground(Color color)1067 	public void setTitleBarForeground(Color color) {
1068 		titleBarForeground = color;
1069 		if (textLabel != null)
1070 			textLabel.setForeground(color);
1071 	}
1072 
1073 	/**
1074 	 * Returns the title bar foreground when TITLE_BAR style is used.
1075 	 *
1076 	 * @return the title bar foreground
1077 	 */
getTitleBarForeground()1078 	public Color getTitleBarForeground() {
1079 		return titleBarForeground;
1080 	}
1081 
1082 	// end of APIs
1083 
toggleState()1084 	private void toggleState() {
1085 		boolean newState = !isExpanded();
1086 		fireExpanding(newState, true);
1087 		internalSetExpanded(newState);
1088 		fireExpanding(newState, false);
1089 		if (newState)
1090 			FormUtil.ensureVisible(this);
1091 	}
1092 
fireExpanding(boolean state, boolean before)1093 	private void fireExpanding(boolean state, boolean before) {
1094 		int size = listeners.size();
1095 		if (size == 0)
1096 			return;
1097 		ExpansionEvent e = new ExpansionEvent(this, state);
1098 		for (IExpansionListener listener : listeners) {
1099 			if (before)
1100 				listener.expansionStateChanging(e);
1101 			else
1102 				listener.expansionStateChanged(e);
1103 		}
1104 	}
1105 
verticalMove(boolean down)1106 	private void verticalMove(boolean down) {
1107 		Composite parent = getParent();
1108 		Control[] children = parent.getChildren();
1109 		for (int i = 0; i < children.length; i++) {
1110 			Control child = children[i];
1111 			if (child == this) {
1112 				ExpandableComposite sibling = getSibling(children, i, down);
1113 				if (sibling != null && sibling.toggle != null) {
1114 					sibling.setFocus();
1115 				}
1116 				break;
1117 			}
1118 		}
1119 	}
1120 
getSibling(Control[] children, int index, boolean down)1121 	private ExpandableComposite getSibling(Control[] children, int index,
1122 			boolean down) {
1123 		int loc = down ? index + 1 : index - 1;
1124 		while (loc >= 0 && loc < children.length) {
1125 			Control c = children[loc];
1126 			if (c instanceof ExpandableComposite && c.isVisible())
1127 				return (ExpandableComposite) c;
1128 			loc = down ? loc + 1 : loc - 1;
1129 		}
1130 		return null;
1131 	}
1132 
programmaticToggleState()1133 	private void programmaticToggleState() {
1134 		if (toggle != null)
1135 			toggle.setExpanded(!toggle.isExpanded());
1136 		toggleState();
1137 	}
1138 
paintTitleFocus(GC gc)1139 	private void paintTitleFocus(GC gc) {
1140 		Point size = textLabel.getSize();
1141 		gc.setBackground(textLabel.getBackground());
1142 		gc.setForeground(textLabel.getForeground());
1143 		if (toggle.isFocusControl())
1144 			gc.drawFocus(0, 0, size.x, size.y);
1145 	}
1146 
reflow()1147 	void reflow() {
1148 		Composite c = this;
1149 		while (c != null) {
1150 			c.setRedraw(false);
1151 			c = c.getParent();
1152 			if (c instanceof SharedScrolledComposite || c instanceof Shell) {
1153 				break;
1154 			}
1155 		}
1156 		c = this;
1157 		while (c != null) {
1158 			c.requestLayout();
1159 			c = c.getParent();
1160 			if (c instanceof SharedScrolledComposite) {
1161 				((SharedScrolledComposite) c).reflow(true);
1162 				break;
1163 			}
1164 		}
1165 		c = this;
1166 		while (c != null) {
1167 			c.setRedraw(true);
1168 			c = c.getParent();
1169 			if (c instanceof SharedScrolledComposite || c instanceof Shell) {
1170 				break;
1171 			}
1172 		}
1173 	}
1174 }
1175