Bug 575578: [UI-SWT] Add ExpandableComposite including styling for
dark theme

Change-Id: I20abda6d1743b9add088b204fbae998c28d2ee1c
diff --git a/ecommons/org.eclipse.statet.ecommons.uimisc/META-INF/MANIFEST.MF b/ecommons/org.eclipse.statet.ecommons.uimisc/META-INF/MANIFEST.MF
index fb076ff..2082894 100644
--- a/ecommons/org.eclipse.statet.ecommons.uimisc/META-INF/MANIFEST.MF
+++ b/ecommons/org.eclipse.statet.ecommons.uimisc/META-INF/MANIFEST.MF
@@ -12,6 +12,7 @@
 Require-Bundle: org.eclipse.statet.ecommons.runtime.core;bundle-version="[4.5.0,4.6.0)",
  org.eclipse.core.runtime;bundle-version="3.19.0",
  org.eclipse.e4.core.contexts;bundle-version="1.7.0";resolution:=optional,
+ org.eclipse.statet.ecommons.preferences.core;bundle-version="[4.5.0,4.6.0)",
  org.eclipse.ui,
  org.eclipse.e4.ui.services,
  org.eclipse.jface.text;resolution:=optional,
@@ -22,10 +23,15 @@
  org.eclipse.core.variables;resolution:=optional,
  org.eclipse.core.resources;resolution:=optional,
  org.eclipse.core.filesystem;resolution:=optional,
- org.eclipse.statet.ecommons.preferences.core;bundle-version="[4.5.0,4.6.0)"
+ org.eclipse.ui.forms;resolution:=optional
 Import-Package: com.ibm.icu.lang;version="67.1.0",
  com.ibm.icu.text;version="67.1.0",
  org.eclipse.debug.ui;resolution:=optional,
+ org.eclipse.e4.ui.css.core.dom;resolution:=optional,
+ org.eclipse.e4.ui.css.core.engine;resolution:=optional,
+ org.eclipse.e4.ui.css.swt.dom;resolution:=optional,
+ org.eclipse.e4.ui.css.swt.properties;resolution:=optional,
+ org.eclipse.e4.ui.css.swt.theme;resolution:=optional,
  org.eclipse.statet.ecommons.collections;version="4.5.0",
  org.eclipse.statet.ecommons.commands.core;version="4.5.0",
  org.eclipse.statet.ecommons.databinding,
@@ -39,7 +45,6 @@
  org.eclipse.statet.ecommons.variables.core,
  org.eclipse.statet.jcommons.collections;version="4.5.0",
  org.eclipse.statet.jcommons.lang;version="4.5.0",
- org.eclipse.e4.ui.css.swt.theme;resolution:=optional,
  org.eclipse.ui.dialogs;resolution:=optional,
  org.eclipse.ui.model;resolution:=optional,
  org.eclipse.ui.views.contentoutline;resolution:=optional,
@@ -53,6 +58,7 @@
  org.eclipse.statet.ecommons.ui.dialogs,
  org.eclipse.statet.ecommons.ui.mpbv,
  org.eclipse.statet.ecommons.ui.swt,
+ org.eclipse.statet.ecommons.ui.swt.expandable,
  org.eclipse.statet.ecommons.ui.util,
  org.eclipse.statet.ecommons.ui.viewers,
  org.eclipse.statet.ecommons.ui.viewers.breadcrumb,
diff --git a/ecommons/org.eclipse.statet.ecommons.uimisc/css/e4_dark.css b/ecommons/org.eclipse.statet.ecommons.uimisc/css/e4_dark.css
new file mode 100644
index 0000000..a7921b2
--- /dev/null
+++ b/ecommons/org.eclipse.statet.ecommons.uimisc/css/e4_dark.css
@@ -0,0 +1,19 @@
+/*
+ #=============================================================================#
+ # Copyright (c) 2021 Stephan Wahlbrink and others.
+ # 
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ # 
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ # 
+ # Contributors:
+ #     Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
+ #=============================================================================*/
+
+.MPart ExpandableComposite Table {
+    background-color: inherit;
+    color: inherit;
+}
diff --git a/ecommons/org.eclipse.statet.ecommons.uimisc/plugin.xml b/ecommons/org.eclipse.statet.ecommons.uimisc/plugin.xml
index 28b541b..16c161c 100644
--- a/ecommons/org.eclipse.statet.ecommons.uimisc/plugin.xml
+++ b/ecommons/org.eclipse.statet.ecommons.uimisc/plugin.xml
@@ -229,4 +229,27 @@
       </command>
    </extension>
    
+   <extension
+         point="org.eclipse.e4.ui.css.core.elementProvider">
+      <provider
+            class="org.eclipse.statet.internal.ecommons.ui.swt.css.dom.ECommonsSwtElementProvider">
+         <widget
+               class="org.eclipse.statet.ecommons.ui.swt.expandable.ExpandableComposite"/>
+      </provider>
+   </extension>
+   <extension
+         point="org.eclipse.e4.ui.css.core.propertyHandler">
+      <handler
+            adapter="org.eclipse.statet.internal.ecommons.ui.swt.css.dom.ExpandableCompositeElement"
+            handler="org.eclipse.statet.internal.ecommons.ui.swt.css.properties.ExpandableCompositeHandler">
+         <property-name
+               name="swt-titlebar-color"/>
+      </handler>
+   </extension>
+   <extension
+         point="org.eclipse.e4.ui.css.swt.theme">
+      <stylesheet
+            uri="css/e4_dark.css"/>
+   </extension>
+   
 </plugin>
diff --git a/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/ecommons/ui/swt/expandable/ExpandableComposite.java b/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/ecommons/ui/swt/expandable/ExpandableComposite.java
new file mode 100644
index 0000000..af1f424
--- /dev/null
+++ b/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/ecommons/ui/swt/expandable/ExpandableComposite.java
@@ -0,0 +1,1318 @@
+/*=============================================================================#
+ # Copyright (c) 2000, 2021 IBM Corporation and others.
+ # 
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0.
+ # 
+ # SPDX-License-Identifier: EPL-2.0
+ # 
+ # Contributors:
+ #     IBM Corporation - org.eclipse.ui.forms: initial API and implementation
+ #     Kai Nacke - org.eclipse.ui.forms: Bug 202382
+ #     Bryan Hunt - org.eclipse.ui.forms: Bug 245457
+ #     Didier Villevalois - org.eclipse.ui.forms: Bug 178534
+ #     Alena Laskavaia - org.eclipse.ui.forms: Bug 481604
+ #     Ralf Petter <ralf.petter@gmail.com> - org.eclipse.ui.forms: Bug 183675
+ #     Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
+ #=============================================================================*/
+
+// org.eclipse.ui.forms.widgets.ExpandableComposite
+// a2318aea7aeb731a6b1cadf0e85dd1e1287f4dd1
+//     removed textClient
+//     without changed layout for wrapping controls (2016-2017)
+
+package org.eclipse.statet.ecommons.ui.swt.expandable;
+
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.jface.action.LegacyActionTools;
+import org.eclipse.osgi.service.environment.Constants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.KeyAdapter;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.TraverseEvent;
+import org.eclipse.swt.events.TraverseListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontMetrics;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Layout;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.forms.events.ExpansionEvent;
+import org.eclipse.ui.forms.events.HyperlinkAdapter;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.events.IExpansionListener;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Hyperlink;
+import org.eclipse.ui.forms.widgets.ILayoutExtension;
+import org.eclipse.ui.forms.widgets.Section;
+import org.eclipse.ui.forms.widgets.SharedScrolledComposite;
+import org.eclipse.ui.forms.widgets.SizeCache;
+import org.eclipse.ui.forms.widgets.Twistie;
+
+import org.eclipse.statet.jcommons.collections.CopyOnWriteIdentityListSet;
+import org.eclipse.statet.jcommons.collections.ImIdentityList;
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
+
+/**
+ * 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 laid 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. *
+ * <p>
+ * Since 3.1, left/right arrow keys can be used to control the expansion state.
+ * If several expandable composites are created in the same parent, up/down
+ * arrow keys can be used to traverse between them. Expandable text accepts
+ * mnemonics and mnemonic activation will toggle the expansion state.
+ * 
+ * <p>
+ * While expandable composite recognize that different styles can be used to
+ * render the title bar, and even defines the constants for these styles
+ * (<code>TITLE_BAR</code> and <code>SHORT_TITLE_BAR</code>) the actual painting
+ * is done in the subclasses.
+ * 
+ * @see Section
+ */
+@NonNullByDefault
+public class ExpandableComposite extends Canvas {
+	
+	
+	private static class Toggle extends Twistie {
+		
+		public Toggle(final Composite parent, final int style) {
+			super(parent, style);
+		}
+		
+		public void setHover(final boolean on) {
+			this.hover= on;
+		}
+		
+		public boolean getHover() {
+			return this.hover;
+		}
+		
+		@Override
+		protected void paint(final PaintEvent e) {
+			// don't paint focus
+			final GC gc= e.gc;
+			final Rectangle clientArea= getClientArea();
+			if (clientArea.width == 0 || clientArea.height == 0) {
+				return;
+			}
+			paintHyperlink(gc);
+		}
+		
+	}
+	
+	
+	/**
+	 * 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, 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 account.
+	 */
+	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, a short version of the title bar decoration will
+	 * be painted behind the text. This style is useful when a more discrete
+	 * option is needed for the title bar.
+	 * 
+	 * @since 3.1
+	 */
+	public static final int SHORT_TITLE_BAR= 1 << 9;
+	
+	/**
+	 * By default, text client is right-aligned. If this style is used, it will
+	 * be positioned after the text control and vertically centered with it.
+	 */
+	public static final int LEFT_TEXT_CLIENT_ALIGNMENT= 1 << 13;
+	
+	/**
+	 * By default, a focus box is painted around the title when it receives focus.
+	 * If this style is used, the focus box will not be painted.  This style does
+	 * not apply when FOCUS_TITLE is used.
+	 * @since 3.5
+	 */
+	public static final int NO_TITLE_FOCUS_BOX= 1 << 14;
+	
+	public static final int IMAGE= 1 << 30;
+	
+	
+	/**
+	 * The toggle widget used to expand the composite.
+	 */
+	protected @Nullable Toggle toggle;
+	
+	/**
+	 * The text label for the title.
+	 */
+	protected @Nullable Control textLabel;
+	
+	private static final int IGAP= 4;
+	private static final int IVGAP= 3;
+	
+	private static final Point NULL_SIZE= new Point(0, 0);
+	
+	private static final int VSPACE= 3;
+	
+	private static final int SEPARATOR_HEIGHT= 2;
+	
+	private int expansionStyle= TWISTIE | FOCUS_TITLE | EXPANDED;
+	
+	private boolean expanded;
+	
+	private @Nullable Label imageLabel;
+	
+	private Control client;
+	
+	private final CopyOnWriteIdentityListSet<IExpansionListener> listeners= new CopyOnWriteIdentityListSet<>();
+	
+	private @Nullable Color titleBarForeground;
+	
+	private final Rectangle titleHeaderRegion= new Rectangle(0, 0, 0, 0);
+	
+	
+	private class ExpandableLayout extends Layout implements ILayoutExtension {
+		
+		/**
+		 * Width of the margin that will be added around the control (default is 0).
+		 */
+		private int marginWidth= 0;
+		
+		/**
+		 * Height of the margin that will be added around the control (default is
+		 * 0).
+		 */
+		private int marginHeight= 0;
+		
+		/**
+		 * Horizontal margin around the inside of the title bar area when TITLE_BAR
+		 * or SHORT_TITLE_BAR style is used. This variable is not used otherwise.
+		 */
+		private int titleBarTextMarginWidth= 6;
+		
+		/**
+		 * Vertical spacing between the title area and the description control
+		 * (default is 0). The description control is normally placed at the new
+		 * line as defined in the font used to render it. This value will be added
+		 * to it.
+		 */
+		public int descriptionVerticalSpacing= 0;
+		
+		/**
+		 * Vertical spacing between the title area and the composite client control
+		 * (default is 3).
+		 */
+		public int clientVerticalSpacing= 3;
+		
+		
+		private final SizeCache toggleCache= new SizeCache();
+		
+		private final SizeCache textClientCache= new SizeCache();
+		
+		private final SizeCache textLabelCache= new SizeCache();
+		
+		private final SizeCache descriptionCache= new SizeCache();
+		
+		private final SizeCache clientCache= new SizeCache();
+		
+		
+		private void initCache(final boolean shouldFlush) {
+			this.toggleCache.setControl(ExpandableComposite.this.toggle);
+			this.textLabelCache.setControl(ExpandableComposite.this.textLabel);
+			this.descriptionCache.setControl(getDescriptionControl());
+			this.clientCache.setControl(ExpandableComposite.this.client);
+			
+			if (shouldFlush) {
+				this.toggleCache.flush();
+				this.textClientCache.flush();
+				this.textLabelCache.flush();
+				this.descriptionCache.flush();
+				this.clientCache.flush();
+			}
+		}
+		
+		@Override
+		protected void layout(final Composite parent, final boolean changed) {
+			initCache(changed);
+			
+			final var toggle= ExpandableComposite.this.toggle;
+			final var imageLabel= ExpandableComposite.this.imageLabel;
+			final var textLabel= ExpandableComposite.this.textLabel;
+			
+			final Rectangle clientArea= parent.getClientArea();
+			int thmargin= 0;
+			int tvmargin= 0;
+			
+			if (hasTitleBar()) {
+				thmargin= this.titleBarTextMarginWidth;
+				tvmargin= IVGAP;
+			}
+			int x= this.marginWidth + thmargin;
+			int y= this.marginHeight + tvmargin;
+			Point toggleSize= NULL_SIZE;
+			if (toggle != null) {
+				toggleSize= this.toggleCache.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+			}
+			Point imageSize= NULL_SIZE;
+			if (imageLabel != null) {
+				imageSize= imageLabel.computeSize(16, 16);
+			}
+			int toggleWidth= clientArea.width - this.marginWidth - this.marginWidth - thmargin - thmargin;
+			if (toggleSize.x > 0) {
+				toggleWidth-= toggleSize.x + IGAP;
+			}
+			if (imageSize.x > 0) {
+				toggleWidth-= imageSize.x;
+			}
+			
+			Point size= NULL_SIZE;
+			if (textLabel != null) {
+				size= this.textLabelCache.computeSize(toggleWidth, SWT.DEFAULT);
+				
+				if (textLabel instanceof Label) {
+					final Point defSize= this.textLabelCache.computeSize(SWT.DEFAULT,
+							SWT.DEFAULT);
+					if (defSize.y == size.y) {
+						// One line - pick the smaller of the two widths
+						size.x= Math.min(defSize.x, size.x);
+					}
+				}
+			}
+			int theaderHeight;
+			{	final GC gc= new GC(ExpandableComposite.this);
+				gc.setFont(getFont());
+				final FontMetrics fm= gc.getFontMetrics();
+				theaderHeight= fm.getHeight();
+				gc.dispose();
+				
+				if (imageSize.y > theaderHeight) {
+					theaderHeight= imageSize.y;
+				}
+			}
+			if (toggle != null) {
+				int ty= theaderHeight / 2 - toggleSize.y / 2 + 1;
+				ty= Math.max(ty, 0);
+				ty+= this.marginHeight + tvmargin;
+				toggle.setLocation(x, ty);
+				toggle.setSize(toggleSize);
+				x+= toggleSize.x + IGAP;
+			}
+			if (imageLabel != null) {
+				final int iy= theaderHeight / 2 - imageSize.y / 2;
+				imageLabel.setBounds(x, iy, imageSize.x, imageSize.y);
+			}
+			if (textLabel != null) {
+				int ty= theaderHeight / 2 - size.y / 2;
+				int tx= x;
+				if (imageLabel != null) {
+					tx+= imageSize.x + IGAP;
+				}
+				if (Constants.WS_GTK.equals(Platform.getWS())) {
+					size.x+= 1; // See Bug 342610
+				}
+				
+				this.textLabelCache.setBounds(tx, ty, size.x, size.y);
+			}
+			int tbarHeight= theaderHeight;
+			if (size.y > tbarHeight) {
+				tbarHeight= size.y;
+			}
+			y+= tbarHeight;
+			if (hasTitleBar()) {
+				y+= tvmargin;
+			}
+			final Control separatorControl= getSeparatorControl();
+			if (separatorControl != null) {
+				y+= VSPACE;
+				separatorControl.setBounds(this.marginWidth, y,
+						clientArea.width - this.marginWidth - this.marginWidth,
+						SEPARATOR_HEIGHT);
+				y+= SEPARATOR_HEIGHT;
+			}
+			if (ExpandableComposite.this.expanded
+					&& ExpandableComposite.this.client != null) {
+				int areaWidth= clientArea.width - this.marginWidth - thmargin;
+				int cx= this.marginWidth + thmargin;
+				if ((ExpandableComposite.this.expansionStyle & CLIENT_INDENT) != 0) {
+					cx= x;
+				}
+				areaWidth-= cx;
+				if (getDescriptionControl() != null) {
+					if (ExpandableComposite.this.expanded) {
+						y+= VSPACE;
+					}
+					final Point dsize= this.descriptionCache.computeSize(areaWidth, SWT.DEFAULT);
+					y+= this.descriptionVerticalSpacing;
+					this.descriptionCache.setBounds(cx, y, areaWidth, dsize.y);
+					y+= dsize.y + this.clientVerticalSpacing;
+				}
+				y+= this.clientVerticalSpacing;
+				final int cwidth= areaWidth;
+				final int cheight= clientArea.height - this.marginHeight -
+						this.marginHeight - y;
+				this.clientCache.setBounds(cx, y, cwidth, cheight);
+			}
+			
+			ExpandableComposite.this.titleHeaderRegion.x= this.marginWidth;
+			ExpandableComposite.this.titleHeaderRegion.y= this.marginHeight;
+			ExpandableComposite.this.titleHeaderRegion.width= clientArea.width - this.marginWidth - this.marginWidth;
+			ExpandableComposite.this.titleHeaderRegion.height= theaderHeight;
+		}
+		
+		@Override
+		protected Point computeSize(final Composite parent, final int wHint, final int hHint,
+				final boolean changed) {
+			initCache(changed);
+			
+			final var toggle= ExpandableComposite.this.toggle;
+			final var imageLabel= ExpandableComposite.this.imageLabel;
+			final var textLabel= ExpandableComposite.this.textLabel;
+			
+			int width= 0, height= 0;
+			Point toggleSize= NULL_SIZE;
+			int toggleWidth= 0;
+			if (toggle != null) {
+				toggleSize= this.toggleCache.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+				toggleWidth= toggleSize.x + IGAP;
+			}
+			int thmargin= 0;
+			int tvmargin= 0;
+			
+			Point imageSize= NULL_SIZE;
+			if (imageLabel != null) {
+				imageSize= imageLabel.computeSize(16, 16);
+			}
+			
+			if (hasTitleBar()) {
+				thmargin= this.titleBarTextMarginWidth;
+				tvmargin= IVGAP;
+			}
+			int innerwHint= wHint;
+			if (innerwHint != SWT.DEFAULT) {
+				innerwHint-= toggleWidth + this.marginWidth + this.marginWidth + thmargin + thmargin;
+				if (imageSize.x > 0) {
+					innerwHint-= imageSize.x + IGAP;
+				}
+			}
+			
+			int innertHint= innerwHint;
+			
+			Point size= NULL_SIZE;
+			if (textLabel != null) {
+				size= this.textLabelCache.computeSize(innertHint, SWT.DEFAULT);
+				
+				if (textLabel instanceof Label) {
+					final Point defSize= this.textLabelCache.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+					if (defSize.y == size.y) {
+						// One line - pick the smaller of the two widths
+						size.x= Math.min(defSize.x, size.x);
+					}
+				}
+			}
+			
+			if (toggleWidth > 0) {
+				width+= toggleWidth;
+			}
+			if (imageSize.x > 0) {
+				width+= imageSize.x + IGAP;
+			}
+			if (size.x > 0) {
+				width+= size.x;
+			}
+			if (toggleSize.y > height) {
+				height= toggleSize.y;
+			}
+			if (imageSize.y > height) {
+				height= imageSize.y;
+			}
+			if (size.y > height) {
+				height= size.y;
+			}
+			final Control separatorControl= getSeparatorControl();
+			if (separatorControl != null) {
+				height+= VSPACE + SEPARATOR_HEIGHT;
+			}
+			
+			if (wHint < width && size.x > 50) {
+				width-= size.x - 50;
+			}
+			
+			// if (hasTitleBar())
+			// height+= VSPACE;
+			if ((ExpandableComposite.this.expanded || (ExpandableComposite.this.expansionStyle & COMPACT) == 0)
+					&& ExpandableComposite.this.client != null) {
+				int cwHint= wHint;
+				int clientIndent= 0;
+				if ((ExpandableComposite.this.expansionStyle & CLIENT_INDENT) != 0) {
+					clientIndent= toggleWidth;
+				}
+				
+				if (cwHint != SWT.DEFAULT) {
+					cwHint-= this.marginWidth + this.marginWidth + thmargin + thmargin;
+				}
+				final Point csize= this.clientCache.computeSize(cwHint, SWT.DEFAULT);
+				if (getDescriptionControl() != null) {
+					int dwHint= cwHint;
+					if (dwHint == SWT.DEFAULT) {
+						dwHint= csize.x;
+						if ((ExpandableComposite.this.expansionStyle & CLIENT_INDENT) != 0) {
+							dwHint-= toggleWidth;
+						}
+					}
+					final Point dsize= this.descriptionCache.computeSize(dwHint, SWT.DEFAULT);
+					width= Math.max(width, dsize.x + clientIndent);
+					if (ExpandableComposite.this.expanded) {
+						if (separatorControl != null) {
+							height+= VSPACE;
+						}
+						height+= this.descriptionVerticalSpacing + dsize.y;
+					}
+				}
+				width= Math.max(width, csize.x + clientIndent);
+				if (ExpandableComposite.this.expanded) {
+					height+= this.clientVerticalSpacing;
+					height+= csize.y;
+				}
+			}
+			
+			final Point result= new Point(width + this.marginWidth + this.marginWidth
+					+ thmargin + thmargin, height + this.marginHeight + this.marginHeight
+					+ tvmargin + tvmargin);
+			return result;
+		}
+		
+		@Override
+		public int computeMinimumWidth(final Composite parent, final boolean changed) {
+			return computeSize(parent, 0, SWT.DEFAULT, changed).x;
+		}
+		
+		@Override
+		public int computeMaximumWidth(final Composite parent, final boolean changed) {
+			return computeSize(parent, SWT.DEFAULT, SWT.DEFAULT, changed).x;
+		}
+		
+	}
+	
+	
+	/**
+	 * Creates an expandable composite using a TWISTIE toggle.
+	 * 
+	 * @param parent
+	 *            the parent composite
+	 * @param style
+	 *            SWT style bits
+	 */
+	public ExpandableComposite(final Composite parent, final int style) {
+		this(parent, style, TWISTIE);
+	}
+	
+	/**
+	 * Creates the expandable composite in the provided parent.
+	 * 
+	 * @param parent
+	 *            the parent
+	 * @param style
+	 *            the control style (as expected by SWT subclass)
+	 * @param expansionStyle
+	 *            the style of the expansion widget (TREE_NODE, TWISTIE,
+	 *            CLIENT_INDENT, COMPACT, FOCUS_TITLE,
+	 *            LEFT_TEXT_CLIENT_ALIGNMENT, NO_TITLE)
+	 */
+	public ExpandableComposite(final Composite parent, final int style, final int expansionStyle) {
+		super(parent, style);
+		this.expansionStyle= expansionStyle;
+		if ((expansionStyle & TITLE_BAR) != 0) {
+			setBackgroundMode(SWT.INHERIT_DEFAULT);
+		}
+		super.setLayout(new ExpandableLayout());
+		if (hasTitleBar()) {
+			addPaintListener(this::onPaint);
+		}
+		
+		final Toggle toggle;
+		final Label imageLabel;
+		final Control textLabel;
+		if ((expansionStyle & TWISTIE) != 0) {
+			toggle= new Toggle(this, SWT.NULL);
+		}
+		else {
+			toggle= null;
+			this.expanded= true;
+		}
+		if ((expansionStyle & EXPANDED) != 0) {
+			this.expanded= true;
+		}
+		this.toggle= toggle;
+		if (toggle != null) {
+			toggle.setExpanded(this.expanded);
+			toggle.addHyperlinkListener(new HyperlinkAdapter() {
+				@Override
+				public void linkActivated(final HyperlinkEvent e) {
+					toggleState();
+				}
+			});
+			toggle.addKeyListener(new KeyAdapter() {
+				@Override
+				public void keyPressed(final KeyEvent e) {
+					if (e.keyCode == SWT.ARROW_UP) {
+						verticalMove(false);
+						e.doit= false;
+					}
+					else if (e.keyCode == SWT.ARROW_DOWN) {
+						verticalMove(true);
+						e.doit= false;
+					}
+				}
+			});
+			if ((getExpansionStyle() & FOCUS_TITLE) == 0) {
+				toggle.addFocusListener(new FocusListener() {
+					@Override
+					public void focusGained(final FocusEvent e) {
+						final var textLabel= ExpandableComposite.this.textLabel;
+						if (textLabel != null) {
+							textLabel.redraw();
+						}
+					}
+					@Override
+					public void focusLost(final FocusEvent e) {
+						final var textLabel= ExpandableComposite.this.textLabel;
+						if (textLabel != null) {
+							textLabel.redraw();
+						}
+					}
+				});
+			}
+		}
+		setToggleHoverColor(computeToggleHoverColor());
+		
+		if ((expansionStyle & IMAGE) != 0) {
+			imageLabel= new Label(this, SWT.NONE);
+		}
+		else {
+			imageLabel= null;
+		}
+		this.imageLabel= imageLabel;
+		
+		if ((expansionStyle & FOCUS_TITLE) != 0) {
+			final Hyperlink link= new Hyperlink(this, SWT.NONE);
+			link.addHyperlinkListener(new HyperlinkAdapter() {
+				@Override
+				public void linkActivated(final HyperlinkEvent e) {
+					programmaticToggleState();
+				}
+			});
+			textLabel= link;
+		}
+		else {
+			final Label label= new Label(this, SWT.NONE);
+			if (!isFixedStyle()) {
+				final Listener listener= new Listener() {
+					@Override
+					public void handleEvent(final Event e) {
+						switch (e.type) {
+						case SWT.Paint:
+							if (ExpandableComposite.this.toggle != null
+									&& (getExpansionStyle() & NO_TITLE_FOCUS_BOX) == 0) {
+								paintTitleFocus(e.gc);
+							}
+							break;
+						case SWT.Resize:
+							updateLabelTrim();
+							break;
+						}
+					}
+				};
+				label.addListener(SWT.Paint, listener);
+				label.addListener(SWT.Resize, listener);
+			}
+			textLabel= label;
+		}
+		this.textLabel= textLabel;
+		if (textLabel != null) {
+			textLabel.setMenu(getMenu());
+			textLabel.addTraverseListener(new TraverseListener() {
+				@Override
+				public void keyTraversed(final TraverseEvent e) {
+					if (e.detail == SWT.TRAVERSE_MNEMONIC) {
+						// steal the mnemonic
+						if (!isVisible() || !isEnabled()) {
+							return;
+						}
+						if (e.character != 0
+								&& e.character == LegacyActionTools.extractMnemonic(getText())) {
+							e.doit= false;
+							if (!isFixedStyle()) {
+								programmaticToggleState();
+							}
+							setFocus();
+						}
+					}
+				}
+			});
+		}
+		{	// Complete Title header
+			final Listener listener= new Listener() {
+				@Override
+				public void handleEvent(final Event event) {
+					int x= event.x;
+					int y= event.y;
+					if (event.widget != ExpandableComposite.this) {
+						final Point p= ((Control)event.widget).getLocation();
+						x+= p.x;
+						y+= p.y;
+					}
+					switch (event.type) {
+					case SWT.MouseDown:
+						if (ExpandableComposite.this.titleHeaderRegion.contains(x, y)) {
+							final var toggle= ExpandableComposite.this.toggle;
+							if (toggle != null) {
+								toggle.setFocus();
+							}
+						}
+						break;
+					case SWT.MouseUp:
+						if (ExpandableComposite.this.titleHeaderRegion.contains(x, y)
+								&& event.button == 1) {
+//							textLabel.setCursor(FormsResources.getBusyCursor());
+							programmaticToggleState();
+//							textLabel.setCursor(null);
+						}
+						break;
+					case SWT.MouseEnter:
+					case SWT.MouseMove:
+					case SWT.MouseExit:
+						if (ExpandableComposite.this.titleHeaderRegion.contains(x, y)) {
+							final var toggle= ExpandableComposite.this.toggle;
+							if (toggle != null && !toggle.getHover()) {
+								toggle.setHover(true);
+								toggle.redraw();
+							}
+						}
+						else {
+							final var toggle= ExpandableComposite.this.toggle;
+							if (toggle != null && toggle.getHover()) {
+								toggle.setHover(false);
+								toggle.redraw();
+							}
+						}
+						break;
+//					case SWT.Paint:
+//						if (toggle != null && (getExpansionStyle() & NO_TITLE_FOCUS_BOX) == 0) {
+//							paintTitleFocus(event.gc);
+//						}
+//						break;
+					}
+				}
+			};
+			addListener(SWT.MouseDown, listener);
+			addListener(SWT.MouseUp, listener);
+			addListener(SWT.MouseEnter, listener);
+			addListener(SWT.MouseMove, listener);
+			addListener(SWT.MouseExit, listener);
+			if (imageLabel != null) {
+				imageLabel.addListener(SWT.MouseDown, listener);
+				imageLabel.addListener(SWT.MouseUp, listener);
+				imageLabel.addListener(SWT.MouseEnter, listener);
+				imageLabel.addListener(SWT.MouseExit, listener);
+			}
+			if (textLabel != null) {
+				textLabel.addListener(SWT.MouseDown, listener);
+				textLabel.addListener(SWT.MouseUp, listener);
+				textLabel.addListener(SWT.MouseEnter, listener);
+				textLabel.addListener(SWT.MouseExit, listener);
+			}
+		}
+	}
+	
+	
+	@Override
+	public boolean forceFocus() {
+		return false;
+	}
+	
+	/**
+	 * Overrides 'super' to pass the menu to the text label.
+	 * 
+	 * @param menu
+	 *            the menu from the parent to attach to this control.
+	 */
+	@Override
+	public void setMenu(final @Nullable Menu menu) {
+		final var toggle= this.toggle;
+		final var imageLabel= this.imageLabel;
+		final var textLabel= this.textLabel;
+		if (toggle != null) {
+			toggle.setMenu(menu);
+		}
+		if (imageLabel != null) {
+			imageLabel.setMenu(menu);
+		}
+		if (textLabel != null) {
+			textLabel.setMenu(menu);
+		}
+		
+		super.setMenu(menu);
+	}
+	
+	/**
+	 * Prevents assignment of the layout manager - expandable composite uses its
+	 * own layout.
+	 */
+	@Override
+	public final void setLayout(final @Nullable Layout layout) {
+	}
+	
+	/**
+	 * Sets the background of all the custom controls in the expandable.
+	 */
+	@Override
+	public void setBackground(final @Nullable Color color) {
+		super.setBackground(color);
+		
+		if ((getExpansionStyle() & TITLE_BAR) == 0) {
+			final var toggle= this.toggle;
+			final var imageLabel= this.imageLabel;
+			final var textLabel= this.textLabel;
+			if (toggle != null) {
+				toggle.setBackground(color);
+			}
+			if (imageLabel != null) {
+				imageLabel.setBackground(color);
+			}
+			if (textLabel != null) {
+				textLabel.setBackground(color);
+			}
+		}
+	}
+	
+	protected Color computeToggleHoverColor() {
+		final Display display= getDisplay();
+		return display.getSystemColor(SWT.COLOR_LIST_SELECTION);
+	}
+	
+	/**
+	 * Sets the foreground of all the custom controls in the expandable.
+	 */
+	@Override
+	public void setForeground(final @Nullable Color color) {
+		super.setForeground(color);
+		
+		final var toggle= this.toggle;
+		final var textLabel= this.textLabel;
+		
+		if (toggle != null) {
+			toggle.setForeground(color);
+		}
+		if (textLabel != null) {
+			textLabel.setForeground(color);
+		}
+	}
+	
+	/**
+	 * Sets the color of the toggle control.
+	 * 
+	 * @param color
+	 *            the color object
+	 */
+	public void setToggleColor(final @Nullable Color color) {
+		final var toggle= this.toggle;
+		if (toggle != null) {
+			toggle.setDecorationColor(color);
+		}
+	}
+	
+	/**
+	 * Sets the active color of the toggle control (when the mouse enters the
+	 * toggle area).
+	 * 
+	 * @param color
+	 *            the active color object
+	 */
+	public void setToggleHoverColor(final @Nullable Color color) {
+		final var toggle= this.toggle;
+		if (toggle != null) {
+			toggle.setHoverDecorationColor(color);
+		}
+	}
+	
+	/**
+	 * Sets the fonts of all the custom controls in the expandable.
+	 */
+	@Override
+	public void setFont(final @Nullable Font font) {
+		super.setFont(font);
+		
+		final var toggle= this.toggle;
+		final var textLabel= this.textLabel;
+		if (toggle != null) {
+			toggle.setFont(font);
+		}
+		if (textLabel != null) {
+			textLabel.setFont(font);
+		}
+	}
+	
+	@Override
+	public void setEnabled(final boolean enabled) {
+		final var toggle= this.toggle;
+		final var textLabel= this.textLabel;
+		if (toggle != null) {
+			toggle.setEnabled(enabled);
+		}
+		if (textLabel != null) {
+			textLabel.setEnabled(enabled);
+		}
+		
+		super.setEnabled(enabled);
+	}
+	
+	/**
+	 * 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(final Control client) {
+		Assert.isTrue(client != null && client.getParent() == this);
+		this.client= client;
+	}
+	
+	/**
+	 * Returns the current expandable client.
+	 * 
+	 * @return the client control
+	 */
+	public Control getClient() {
+		return this.client;
+	}
+	
+	public void setImage(final @Nullable Image image) {
+		final var imageLabel= this.imageLabel;
+		if (imageLabel != null) {
+			imageLabel.setImage(image);
+		}
+	}
+	
+	public @Nullable Image getImage() {
+		final var imageLabel= this.imageLabel;
+		if (imageLabel != null) {
+			return imageLabel.getImage();
+		}
+		return null;
+	}
+	
+	/**
+	 * 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 #getText()
+	 */
+	public void setText(final String title) {
+		final var textLabel= this.textLabel;
+		if (textLabel instanceof Label) {
+			((Label)textLabel).setText(title);
+			updateLabelTrim();
+		}
+		else if (textLabel instanceof Hyperlink) {
+			((Hyperlink)textLabel).setText(title);
+		}
+		else {
+			return;
+		}
+		layout();
+	}
+	
+	/**
+	 * Returns the title string.
+	 * 
+	 * @return the title string
+	 * @see #setText(String)
+	 */
+	public String getText() {
+		final var textLabel= this.textLabel;
+		if (textLabel instanceof Label) {
+			return ((Label)textLabel).getText();
+		}
+		else if (textLabel instanceof Hyperlink) {
+			return ((Hyperlink)textLabel).getText();
+		}
+		else {
+			return ""; //$NON-NLS-1$
+		}
+	}
+	
+	@Override
+	public void setToolTipText(final @Nullable String string) {
+		super.setToolTipText(string);
+		
+		// Also set on label, otherwise it's just on the background without text.
+		final var toggle= this.toggle;
+		final var imageLabel= this.imageLabel;
+		final var textLabel= this.textLabel;
+		if (toggle != null) {
+			toggle.setToolTipText(string);
+		}
+		if (imageLabel != null) {
+			imageLabel.setToolTipText(string);
+		}
+		if (textLabel != null) {
+			textLabel.setToolTipText(string);
+		}
+	}
+	
+	/**
+	 * Tests the expanded state of the composite.
+	 * 
+	 * @return <samp>true </samp> if expanded, <samp>false </samp> if collapsed.
+	 */
+	public boolean isExpanded() {
+		return this.expanded;
+	}
+	
+	/**
+	 * Returns the bitwise-ORed style bits for the expansion control.
+	 * 
+	 * @return the bitwise-ORed style bits for the expansion control
+	 */
+	public int getExpansionStyle() {
+		return this.expansionStyle;
+	}
+	
+	/**
+	 * Programmatically changes expanded state.
+	 * 
+	 * @param expanded
+	 *            the new expanded state
+	 */
+	public void setExpanded(final boolean expanded) {
+		internalSetExpanded(expanded);
+		
+		final var toggle= this.toggle;
+		if (toggle != null) {
+			toggle.setExpanded(expanded);
+		}
+	}
+	
+	/**
+	 * Performs the expansion state change for the expandable control.
+	 * 
+	 * @param expanded
+	 *            the expansion state
+	 */
+	protected void internalSetExpanded(final boolean expanded) {
+		if (this.expanded != expanded) {
+			this.expanded= expanded;
+			
+			final var descriptionControl= getDescriptionControl();
+			if (descriptionControl != null) {
+				descriptionControl.setVisible(expanded);
+			}
+			if (this.client != null) {
+				this.client.setVisible(expanded);
+			}
+			reflow();
+		}
+	}
+	
+	/**
+	 * Adds the listener that will be notified when the expansion state changes.
+	 * 
+	 * @param listener
+	 *            the listener to add
+	 */
+	public void addExpansionListener(final IExpansionListener listener) {
+		this.listeners.add(listener);
+	}
+	
+	/**
+	 * Removes the expansion listener.
+	 * 
+	 * @param listener
+	 *            the listener to remove
+	 */
+	public void removeExpansionListener(final IExpansionListener listener) {
+		this.listeners.remove(listener);
+	}
+	
+	/**
+	 * If TITLE_BAR or SHORT_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(final PaintEvent 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 @Nullable 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 @Nullable Control getSeparatorControl() {
+		return null;
+	}
+	
+	/**
+	 * Computes the size of the expandable composite.
+	 * 
+	 * @see org.eclipse.swt.widgets.Composite#computeSize
+	 */
+	@Override
+	public Point computeSize(final int wHint, final int hHint, final boolean changed) {
+		checkWidget();
+		final Point size;
+		final var layout= (ExpandableLayout)getLayout();
+		if (wHint == SWT.DEFAULT || hHint == SWT.DEFAULT) {
+			size= layout.computeSize(this, wHint, hHint, changed);
+		} else {
+			size= new Point(wHint, hHint);
+		}
+		final 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 ((this.expansionStyle & TWISTIE) == 0);
+	}
+	
+	/**
+	 * Tests if this expandable composite renders a title bar around the text.
+	 * 
+	 * @return <code>true</code> for <code>TITLE_BAR</code> or
+	 *         <code>SHORT_TITLE_BAR</code> styles, <code>false</code>
+	 *         otherwise.
+	 */
+	protected boolean hasTitleBar() {
+		return ((getExpansionStyle() & TITLE_BAR) != 0
+				|| (getExpansionStyle() & SHORT_TITLE_BAR) != 0 );
+	}
+	
+	/**
+	 * Sets the color of the title bar foreground when TITLE_BAR style is used.
+	 * 
+	 * @param color
+	 *            the title bar foreground
+	 */
+	public void setTitleBarForeground(final @Nullable Color color) {
+		this.titleBarForeground= color;
+		
+		final var textLabel= this.textLabel;
+		if (textLabel != null) {
+			textLabel.setForeground(color);
+		}
+	}
+	
+	/**
+	 * Returns the title bar foreground when TITLE_BAR style is used.
+	 * 
+	 * @return the title bar foreground
+	 */
+	public @Nullable Color getTitleBarForeground() {
+		return this.titleBarForeground;
+	}
+	
+	// end of APIs
+	
+	private void toggleState() {
+		final boolean newState= !isExpanded();
+		fireExpanding(newState, true);
+		internalSetExpanded(newState);
+		fireExpanding(newState, false);
+		if (newState) {
+			FormToolkit.ensureVisible(this);
+		}
+	}
+	
+	private void fireExpanding(final boolean state, final boolean before) {
+		final ImIdentityList<IExpansionListener> listenerList= this.listeners.toList();
+		if (listenerList.isEmpty()) {
+			return;
+		}
+		final ExpansionEvent e= new ExpansionEvent(this, state);
+		for (final IExpansionListener listener : listenerList) {
+			if (before) {
+				listener.expansionStateChanging(e);
+			}
+			else {
+				listener.expansionStateChanged(e);
+			}
+		}
+	}
+	
+	private void verticalMove(final boolean down) {
+		final Composite parent= nonNullAssert(getParent());
+		final Control[] children= parent.getChildren();
+		for (int i= 0; i < children.length; i++) {
+			final Control child= children[i];
+			if (child == this) {
+				final ExpandableComposite sibling= getSibling(children, i, down);
+				if (sibling != null && sibling.toggle != null) {
+					sibling.setFocus();
+				}
+				break;
+			}
+		}
+	}
+	
+	private @Nullable ExpandableComposite getSibling(final Control[] children, final int index,
+			final boolean down) {
+		int loc= (down) ? index + 1 : index - 1;
+		while (loc >= 0 && loc < children.length) {
+			final Control c= children[loc];
+			if (c instanceof ExpandableComposite && c.isVisible()) {
+				return (ExpandableComposite)c;
+			}
+			loc= (down) ? loc + 1 : loc - 1;
+		}
+		return null;
+	}
+	
+	private void programmaticToggleState() {
+		final var toggle= this.toggle;
+		if (toggle != null) {
+			toggle.setExpanded(!toggle.isExpanded());
+		}
+		toggleState();
+	}
+	
+	private void paintTitleFocus(final GC gc) {
+		final var toggle= this.toggle;
+		final var textLabel= this.textLabel;
+		if (textLabel != null) {
+			final Point size= textLabel.getSize();
+			gc.setBackground(textLabel.getBackground());
+			gc.setForeground(textLabel.getForeground());
+			if (toggle != null && toggle.isFocusControl()) {
+				gc.drawFocus(0, 0, size.x, size.y);
+			}
+		}
+	}
+	
+	void reflow() {
+		Composite c= this;
+		while (c != null) {
+			c.setRedraw(false);
+			c= c.getParent();
+			if (c instanceof SharedScrolledComposite || c instanceof Shell) {
+				break;
+			}
+		}
+		try {
+			c= this;
+			while (c != null) {
+				c.requestLayout();
+				c= c.getParent();
+				if (c instanceof SharedScrolledComposite) {
+					((SharedScrolledComposite)c).reflow(true);
+					break;
+				}
+				if (c instanceof Shell) {
+					break;
+				}
+			}
+		}
+		finally {
+			c= this;
+			while (c != null) {
+				c.setRedraw(true);
+				c= c.getParent();
+				if (c instanceof SharedScrolledComposite || c instanceof Shell) {
+					break;
+				}
+			}
+		}
+	}
+	
+	private void updateLabelTrim() {
+//		if (textLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT).x > textLabel.getSize().x) {
+//			textLabel.setToolTipText(((Label)textLabel).getText());
+//		}
+//		else {
+//			textLabel.setToolTipText(null);
+//		}
+	}
+	
+}
diff --git a/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/ecommons/ui/swt/expandable/ExpandableRowsList.java b/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/ecommons/ui/swt/expandable/ExpandableRowsList.java
new file mode 100644
index 0000000..2aca247
--- /dev/null
+++ b/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/ecommons/ui/swt/expandable/ExpandableRowsList.java
@@ -0,0 +1,135 @@
+/*=============================================================================#
+ # Copyright (c) 2012, 2021 Stephan Wahlbrink and others.
+ # 
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ # 
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ # 
+ # Contributors:
+ #     Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.ecommons.ui.swt.expandable;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.forms.FormColors;
+import org.eclipse.ui.forms.events.ExpansionAdapter;
+import org.eclipse.ui.forms.events.ExpansionEvent;
+import org.eclipse.ui.forms.events.IExpansionListener;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.SharedScrolledComposite;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
+
+@NonNullByDefault
+public class ExpandableRowsList extends SharedScrolledComposite {
+	
+	
+	private static @Nullable FormToolkit gDialogsFormToolkit;
+	
+	private static FormToolkit getViewFormToolkit() {
+		FormToolkit toolkit= gDialogsFormToolkit;
+		if (toolkit == null) {
+			final FormColors colors= new FormColors(Display.getCurrent());
+			colors.setBackground(null);
+			colors.setForeground(null);
+			toolkit= new FormToolkit(colors);
+			gDialogsFormToolkit= toolkit;
+		}
+		return toolkit;
+	}
+	
+	
+	private final FormToolkit toolkit;
+	
+	private @Nullable IExpansionListener expansionListener;
+	
+	private int delayReflowCounter;
+	
+	
+	public ExpandableRowsList(final Composite parent) {
+		this(parent, SWT.H_SCROLL | SWT.V_SCROLL);
+	}
+	
+	public ExpandableRowsList(final Composite parent, final int style) {
+		super(parent, style);
+		
+		if ((style & SWT.H_SCROLL) == 0) {
+			setExpandHorizontal(true);
+		}
+		
+		setFont(parent.getFont());
+		
+		this.toolkit= getViewFormToolkit();
+		
+		setExpandHorizontal(true);
+		setExpandVertical(true);
+		
+		final Composite body= new Composite(this, SWT.NONE);
+		setContent(body);
+		
+		setFont(parent.getFont());
+		setBackgroundMode(SWT.INHERIT_FORCE);
+		setBackground(getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
+		setForeground(getDisplay().getSystemColor(SWT.COLOR_LIST_FOREGROUND));
+	}
+	
+	
+	@Override
+	@SuppressWarnings("null")
+	public Composite getContent() {
+		return (Composite)super.getContent();
+	}
+	
+	@Override
+	public void setDelayedReflow(final boolean delayedReflow) {
+		if (delayedReflow) {
+			this.delayReflowCounter++;
+		}
+		else {
+			this.delayReflowCounter--;
+		}
+		super.setDelayedReflow(this.delayReflowCounter > 0);
+	}
+	
+	public void adaptChild(final Control childControl) {
+		if (childControl instanceof ExpandableComposite) {
+			final var expandableComposite= (ExpandableComposite)childControl;
+			this.toolkit.adapt(expandableComposite, true, false);
+			var expansionListener= this.expansionListener;
+			if (expansionListener == null) {
+				expansionListener= new ExpansionAdapter() {
+					@Override
+					public void expansionStateChanged(final ExpansionEvent e) {
+						expandedStateChanged();
+					}
+				};
+				this.expansionListener= expansionListener;
+			}
+			expandableComposite.addExpansionListener(expansionListener);
+		}
+		else if (childControl instanceof Composite) {
+			final var composite= (Composite)childControl;
+			this.toolkit.adapt(composite, false, false);
+			for (final Control child : composite.getChildren()) {
+				this.toolkit.adapt(child, true, false);
+			}
+		}
+		else {
+			this.toolkit.adapt(childControl, true, false);
+		}
+	}
+	
+	protected void expandedStateChanged() {
+		reflow(true);
+	}
+	
+}
diff --git a/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/internal/ecommons/ui/swt/css/dom/ECommonsSwtElementProvider.java b/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/internal/ecommons/ui/swt/css/dom/ECommonsSwtElementProvider.java
new file mode 100644
index 0000000..a3528b5
--- /dev/null
+++ b/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/internal/ecommons/ui/swt/css/dom/ECommonsSwtElementProvider.java
@@ -0,0 +1,42 @@
+/*=============================================================================#
+ # Copyright (c) 2017 Stephan Wahlbrink and others.
+ # All rights reserved. This program and the accompanying materials
+ # are made available under the terms of the Eclipse Public License v1.0
+ # which accompanies this distribution, and is available at
+ # http://www.eclipse.org/legal/epl-v10.html
+ #
+ # Contributors:
+ #     Stephan Wahlbrink <stephan.wahlbrink@walware.de> - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.internal.ecommons.ui.swt.css.dom;
+
+import org.eclipse.e4.ui.css.core.dom.IElementProvider;
+import org.eclipse.e4.ui.css.core.engine.CSSEngine;
+
+import org.w3c.dom.Element;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
+import org.eclipse.statet.ecommons.ui.swt.expandable.ExpandableComposite;
+
+
+@NonNullByDefault
+@SuppressWarnings("restriction")
+public class ECommonsSwtElementProvider implements IElementProvider {
+	
+	
+	public ECommonsSwtElementProvider() {
+	}
+	
+	
+	@Override
+	public @Nullable Element getElement(final Object element, final CSSEngine engine) {
+		if (element instanceof ExpandableComposite) {
+			return new ExpandableCompositeElement((ExpandableComposite)element, engine);
+		}
+		return null;
+	}
+	
+}
diff --git a/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/internal/ecommons/ui/swt/css/dom/ExpandableCompositeElement.java b/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/internal/ecommons/ui/swt/css/dom/ExpandableCompositeElement.java
new file mode 100644
index 0000000..b3514d0
--- /dev/null
+++ b/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/internal/ecommons/ui/swt/css/dom/ExpandableCompositeElement.java
@@ -0,0 +1,52 @@
+/*=============================================================================#
+ # Copyright (c) 2017 Stephan Wahlbrink and others.
+ # All rights reserved. This program and the accompanying materials
+ # are made available under the terms of the Eclipse Public License v1.0
+ # which accompanies this distribution, and is available at
+ # http://www.eclipse.org/legal/epl-v10.html
+ #
+ # Contributors:
+ #     Stephan Wahlbrink <stephan.wahlbrink@walware.de> - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.internal.ecommons.ui.swt.css.dom;
+
+import org.eclipse.e4.ui.css.core.engine.CSSEngine;
+import org.eclipse.e4.ui.css.swt.dom.CompositeElement;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+
+import org.eclipse.statet.ecommons.ui.swt.expandable.ExpandableComposite;
+
+
+@NonNullByDefault
+@SuppressWarnings("restriction")
+public class ExpandableCompositeElement extends CompositeElement {
+	
+	
+	public ExpandableCompositeElement(final ExpandableComposite composite, final CSSEngine engine) {
+		super(composite, engine);
+		this.hasMouseHover= true;
+	}
+	
+	
+	@Override
+	protected String computeLocalName() {
+		return "ExpandableComposite"; //$NON-NLS-1$
+	}
+	
+	@Override
+	public ExpandableComposite getComposite() {
+		return (ExpandableComposite)getNativeWidget();
+	}
+	
+	@Override
+	public void reset() {
+		super.reset();
+		final var composite= getComposite();
+		composite.setTitleBarForeground(null);
+		composite.setToggleColor(null);
+		composite.setToggleHoverColor(null);
+	}
+	
+}
diff --git a/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/internal/ecommons/ui/swt/css/properties/ExpandableCompositeHandler.java b/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/internal/ecommons/ui/swt/css/properties/ExpandableCompositeHandler.java
new file mode 100644
index 0000000..7d749d0
--- /dev/null
+++ b/ecommons/org.eclipse.statet.ecommons.uimisc/src/org/eclipse/statet/internal/ecommons/ui/swt/css/properties/ExpandableCompositeHandler.java
@@ -0,0 +1,77 @@
+/*=============================================================================#
+ # Copyright (c) 2017, 2018 Stephan Wahlbrink and others.
+ # 
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ # 
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ # 
+ # Contributors:
+ #     Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.internal.ecommons.ui.swt.css.properties;
+
+import org.eclipse.e4.ui.css.core.engine.CSSEngine;
+import org.eclipse.e4.ui.css.swt.properties.AbstractCSSPropertySWTHandler;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.widgets.Control;
+
+import org.w3c.dom.css.CSSValue;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
+import org.eclipse.statet.ecommons.ui.swt.expandable.ExpandableComposite;
+
+
+@NonNullByDefault
+@SuppressWarnings("restriction")
+public class ExpandableCompositeHandler extends AbstractCSSPropertySWTHandler {
+	
+	
+	private static final String TITLE_BAR_FOREGROUND= "swt-titlebar-color"; //$NON-NLS-1$
+	
+	
+	@Override
+	protected @Nullable String retrieveCSSProperty(final Control control, final String property,
+			final @Nullable String pseudo, final CSSEngine engine) throws Exception {
+		return null;
+	}
+	
+	@Override
+	protected void applyCSSProperty(final Control control, final String property,
+			final CSSValue value,
+			final @Nullable String pseudo, final CSSEngine engine) throws Exception {
+		if (!(control instanceof ExpandableComposite)
+				|| property == null
+				|| value.getCssValueType() != CSSValue.CSS_PRIMITIVE_VALUE) {
+			return;
+		}
+		final var expandableComposite= (ExpandableComposite)control;
+		
+		switch (property.toLowerCase()) {
+		case TITLE_BAR_FOREGROUND:
+			if (pseudo == null) {
+				expandableComposite.setTitleBarForeground(
+						(Color)engine.convert(value, Color.class, control.getDisplay()) );
+			}
+			return;
+//		case CSSPropertyFormHandler.TB_TOGGLE:
+//			if (pseudo == null) {
+//				expandableComposite.setToggleColor(
+//						(Color)engine.convert(value, Color.class, control.getDisplay()) );
+//			}
+//			else if (pseudo.equalsIgnoreCase("hover")) { //$NON-NLS-1$
+//				expandableComposite.setToggleHoverColor(
+//						(Color)engine.convert(value, Color.class, control.getDisplay()) );
+//			}
+//			return;
+		default:
+			return;
+		}
+	}
+	
+}