Bug 546654: [UIBuilder] provide a module to dynamically build UIs

Change-Id: If616b1cb47e012ab1946052bf7ea7e061b7a9895
diff --git a/plugins/org.eclipse.ease.modules.charting/src/org/eclipse/ease/modules/charting/ChartingModule.java b/plugins/org.eclipse.ease.modules.charting/src/org/eclipse/ease/modules/charting/ChartingModule.java
index 73c00f1..8e14ebe 100644
--- a/plugins/org.eclipse.ease.modules.charting/src/org/eclipse/ease/modules/charting/ChartingModule.java
+++ b/plugins/org.eclipse.ease.modules.charting/src/org/eclipse/ease/modules/charting/ChartingModule.java
@@ -28,6 +28,8 @@
 import org.eclipse.nebula.visualization.xygraph.figures.Axis;
 import org.eclipse.nebula.visualization.xygraph.figures.Trace;
 import org.eclipse.nebula.visualization.xygraph.figures.XYGraph;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Display;
 import org.eclipse.ui.IWorkbenchPage;
 
@@ -69,6 +71,27 @@
 	}
 
 	/**
+	 * Create a new chart on a given parent composite.
+	 *
+	 * @param parent
+	 *            composite to create chart into
+	 * @param figureId
+	 *            name of the figure to be created
+	 * @return new chart instance
+	 */
+	@WrapToScript
+	public Chart createChart(Composite parent, @ScriptParameter(defaultValue = ScriptParameter.NULL) final String figureId) {
+		final String secondaryId = (figureId == null) ? "Figure " + Integer.toString(fFigureIterator++) : figureId;
+
+		Display.getDefault().syncExec(() -> {
+			fChart = new Chart(parent, SWT.NONE);
+			fXYGraph = fChart.setPlotTitle(secondaryId);
+		});
+
+		return fChart;
+	}
+
+	/**
 	 * Series will be created with the name seriesName. If the name is not given then Series will have the name "Series id" where id is the current number of
 	 * the Series Iterator. Series Iterator start at 1 and with every new Series is incremented by one. If Series is already created with seriesName then that
 	 * series will be set as currently active Series and on that Series methods will be performed. If there is no active Figure then Figure will be created and
diff --git a/plugins/org.eclipse.ease.modules.platform/META-INF/MANIFEST.MF b/plugins/org.eclipse.ease.modules.platform/META-INF/MANIFEST.MF
index 17b8005..4ee454b 100644
--- a/plugins/org.eclipse.ease.modules.platform/META-INF/MANIFEST.MF
+++ b/plugins/org.eclipse.ease.modules.platform/META-INF/MANIFEST.MF
@@ -18,7 +18,9 @@
  org.eclipse.equinox.p2.metadata;bundle-version="[2.2.0,3.0.0)",
  org.eclipse.equinox.p2.engine;bundle-version="[2.3.0,3.0.0)",
  org.eclipse.e4.core.services;bundle-version="[1.2.1,3.0.0)",
- org.eclipse.osgi.services;bundle-version="[3.4.0,4.0.0)"
+ org.eclipse.osgi.services;bundle-version="[3.4.0,4.0.0)",
+ org.eclipse.e4.ui.model.workbench,
+ org.eclipse.e4.ui.workbench
 Bundle-Version: 0.7.0.qualifier
 Bundle-Name: Platform modules (Incubation)
 Bundle-ManifestVersion: 2
diff --git a/plugins/org.eclipse.ease.modules.platform/plugin.xml b/plugins/org.eclipse.ease.modules.platform/plugin.xml
index df86009..b385ce4 100644
--- a/plugins/org.eclipse.ease.modules.platform/plugin.xml
+++ b/plugins/org.eclipse.ease.modules.platform/plugin.xml
@@ -30,6 +30,13 @@
             name="Build"
             visible="true">
       </module>
+      <module
+            category="org.eclipse.ease.category.system"
+            class="org.eclipse.ease.modules.platform.uibuilder.UIBuilderModule"
+            id="org.eclipse.ease.modules.platform.uibuilder"
+            name="UI Builder"
+            visible="true">
+      </module>
    </extension>
    <extension
          point="org.eclipse.ease.ui.shell">
diff --git a/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/UIModule.java b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/UIModule.java
index e7107a6..b1cd990 100644
--- a/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/UIModule.java
+++ b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/UIModule.java
@@ -530,7 +530,7 @@
 	 *            name of view
 	 * @return view ID or <code>null</code>
 	 */
-	private static String getIDForName(final String name) {
+	public static String getIDForName(final String name) {
 		String id = null;
 
 		final IViewRegistry viewRegistry = PlatformUI.getWorkbench().getViewRegistry();
diff --git a/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/CompositeRenderer.java b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/CompositeRenderer.java
new file mode 100644
index 0000000..6ff4127
--- /dev/null
+++ b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/CompositeRenderer.java
@@ -0,0 +1,150 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Christian Pontesegger 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:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.modules.platform.uibuilder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+
+public class CompositeRenderer implements IRenderer, PaintListener {
+
+	private final Composite fParent;
+	private ViewModel fViewModel;
+	private boolean fShowGrid = false;
+
+	public CompositeRenderer(Composite parent) {
+		fParent = parent;
+	}
+
+	@Override
+	public void setViewModel(ViewModel viewModel) {
+		fViewModel = viewModel;
+	}
+
+	@Override
+	public IPlaceHolder createPlaceHolder(Point position) {
+		final LabelPlaceHolder placeHolder = new LabelPlaceHolder(fParent, position.y, position.x);
+		placeHolder.setVisible(fShowGrid);
+		return placeHolder;
+	}
+
+	public Composite getParent() {
+		return fParent;
+	}
+
+	private GridLayout getLayout() {
+		return (GridLayout) getParent().getLayout();
+	}
+
+	public void update() {
+		Control predecessor = null;
+		for (final Object element : fViewModel.getElements()) {
+			if (element instanceof Control) {
+				((Control) element).moveBelow(predecessor);
+				predecessor = (Control) element;
+			}
+		}
+
+		getLayout().numColumns = fViewModel.getColumnCount();
+
+		getParent().layout(true);
+	}
+
+	public void setShowGrid(boolean showGrid) {
+		fShowGrid = showGrid;
+
+		if (showGrid)
+			getParent().addPaintListener(this);
+		else
+			getParent().removePaintListener(this);
+
+		for (final Object element : fViewModel.getElements()) {
+			if (element instanceof LabelPlaceHolder)
+				((LabelPlaceHolder) element).setVisible(showGrid);
+		}
+
+		getParent().layout(true);
+	}
+
+	@Override
+	public void paintControl(PaintEvent e) {
+		e.gc.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_RED));
+
+		final List<Rectangle> columns = new ArrayList<>();
+		while (columns.size() < fViewModel.getColumnCount())
+			columns.add(new Rectangle(0, 0, 0, 0));
+
+		final List<Rectangle> rows = new ArrayList<>();
+		while (rows.size() < fViewModel.getRowCount())
+			rows.add(new Rectangle(0, 0, 0, 0));
+
+		for (final Object element : fViewModel.getElements()) {
+			if (element instanceof Control) {
+				final Rectangle bounds = ((Control) element).getBounds();
+				final Point columnRowLocation = fViewModel.indexToPoint(fViewModel.getElements().indexOf(element));
+
+				final Rectangle column = columns.get(columnRowLocation.x - 1);
+				final Rectangle row = rows.get(columnRowLocation.y - 1);
+
+				if (((GridData) ((Control) element).getLayoutData()).horizontalSpan == 1)
+					merge(bounds, column);
+
+				if (((GridData) ((Control) element).getLayoutData()).verticalSpan == 1)
+					merge(bounds, row);
+			}
+		}
+
+		final Rectangle firstRow = rows.get(0);
+		final Rectangle lastRow = rows.get(rows.size() - 1);
+		for (final Rectangle rectangle : columns) {
+			rectangle.y = firstRow.y;
+			rectangle.height = (lastRow.y + lastRow.height) - firstRow.y;
+
+			e.gc.drawRectangle(rectangle.x - 3, rectangle.y - 3, rectangle.width + 5, rectangle.height + 5);
+		}
+
+		final Rectangle firstColumn = columns.get(0);
+		final Rectangle lastColumn = columns.get(columns.size() - 1);
+		for (final Rectangle rectangle : rows) {
+			rectangle.x = firstColumn.x;
+			rectangle.width = (lastColumn.x + lastColumn.width) - firstColumn.x;
+
+			e.gc.drawRectangle(rectangle.x - 3, rectangle.y - 3, rectangle.width + 5, rectangle.height + 5);
+		}
+	}
+
+	private void merge(Rectangle source, Rectangle target) {
+		if (target.x == 0)
+			target.x = source.x;
+
+		if (target.y == 0)
+			target.y = source.y;
+
+		if (target.width == 0)
+			target.width = source.width;
+
+		if (target.height == 0)
+			target.height = source.height;
+
+		target.add(source);
+	}
+}
diff --git a/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/DialogRunnable.java b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/DialogRunnable.java
new file mode 100644
index 0000000..446a612
--- /dev/null
+++ b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/DialogRunnable.java
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Christian Pontesegger 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:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.ease.modules.platform.uibuilder;
+
+import org.eclipse.swt.widgets.Composite;
+
+public abstract class DialogRunnable implements Runnable {
+
+	private Composite fComposite;
+
+	private ScriptableDialog fDialog;
+
+	public void setDialog(ScriptableDialog dialog) {
+		fDialog = dialog;
+	}
+
+	public ScriptableDialog getDialog() {
+		return fDialog;
+	}
+
+	public void setComposite(Composite composite) {
+		fComposite = composite;
+	}
+
+	public Composite getComposite() {
+		return fComposite;
+	}
+}
diff --git a/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/IPlaceHolder.java b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/IPlaceHolder.java
new file mode 100644
index 0000000..e463c59
--- /dev/null
+++ b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/IPlaceHolder.java
@@ -0,0 +1,18 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Christian Pontesegger 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:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.modules.platform.uibuilder;
+
+/**
+ * Pure marker interface for empty layout cells.
+ */
+public interface IPlaceHolder {
+}
diff --git a/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/IRenderer.java b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/IRenderer.java
new file mode 100644
index 0000000..09ebb16
--- /dev/null
+++ b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/IRenderer.java
@@ -0,0 +1,21 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Christian Pontesegger 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:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.modules.platform.uibuilder;
+
+import org.eclipse.swt.graphics.Point;
+
+public interface IRenderer {
+
+	public IPlaceHolder createPlaceHolder(Point position);
+
+	public void setViewModel(ViewModel viewModel);
+}
diff --git a/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/LabelPlaceHolder.java b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/LabelPlaceHolder.java
new file mode 100644
index 0000000..b91cd61
--- /dev/null
+++ b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/LabelPlaceHolder.java
@@ -0,0 +1,33 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Christian Pontesegger 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:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.modules.platform.uibuilder;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+
+public class LabelPlaceHolder extends Label implements IPlaceHolder {
+
+	public LabelPlaceHolder(Composite composite, int row, int column) {
+		super(composite, SWT.NONE);
+		setText(column + "/" + row);
+		setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, false, false));
+
+		setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_DARK_GRAY));
+	}
+
+	@Override
+	protected void checkSubclass() {
+	}
+}
\ No newline at end of file
diff --git a/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/Location.java b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/Location.java
new file mode 100644
index 0000000..75078ad
--- /dev/null
+++ b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/Location.java
@@ -0,0 +1,118 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Christian Pontesegger 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:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.modules.platform.uibuilder;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+
+public class Location {
+	private static final Pattern LAYOUT_PATTERN = Pattern.compile("(?:(\\d+)(?:-(\\d+))?/(\\d+)(?:-(\\d+))?)?(?:\\s*([<>ox])(!?))?(?:\\s*([\\^vox])(!?))?");
+
+	public static int DYNAMIC_POSITION = -1;
+
+	private final Point fPosition = new Point(DYNAMIC_POSITION, DYNAMIC_POSITION);
+	private final GridData fGridData = new GridData();
+
+	public Location() {
+		this(null);
+	}
+
+	public Location(String layout) {
+		if (layout == null)
+			layout = "<^";
+
+		final Matcher matcher = LAYOUT_PATTERN.matcher(layout);
+		if (matcher.matches()) {
+			if (matcher.group(1) != null) {
+
+				fPosition.x = Integer.parseInt(matcher.group(1));
+				fPosition.y = Integer.parseInt(matcher.group(3));
+
+				fGridData.horizontalSpan = (matcher.group(2) != null) ? ((Integer.parseInt(matcher.group(2)) - fPosition.x) + 1) : 1;
+				fGridData.verticalSpan = (matcher.group(4) != null) ? ((Integer.parseInt(matcher.group(4)) - fPosition.y) + 1) : 1;
+
+				// sanity checks
+				if (fPosition.x < 1)
+					throw new IllegalArgumentException("Column cannot be < 1");
+
+				if (fPosition.y < 1)
+					throw new IllegalArgumentException("Row cannot be < 1");
+
+				if (fGridData.horizontalSpan < 1)
+					throw new IllegalArgumentException("Column span cannot be < 1");
+
+				if (fGridData.verticalSpan < 1)
+					throw new IllegalArgumentException("Row span cannot be < 1");
+			}
+
+			if (matcher.group(5) != null) {
+				switch (matcher.group(5)) {
+				case "x":
+					fGridData.horizontalAlignment = SWT.CENTER;
+					break;
+				case ">":
+					fGridData.horizontalAlignment = SWT.RIGHT;
+					break;
+				case "o":
+					fGridData.horizontalAlignment = SWT.FILL;
+					break;
+				case "<":
+					// fall through
+				default:
+					fGridData.horizontalAlignment = SWT.LEFT;
+					break;
+				}
+
+			} else
+				fGridData.horizontalAlignment = SWT.LEFT;
+
+			fGridData.grabExcessHorizontalSpace = ("!".equals(matcher.group(6)));
+
+			if (matcher.group(7) != null) {
+				switch (matcher.group(7)) {
+				case "x":
+					fGridData.verticalAlignment = SWT.CENTER;
+					break;
+				case "v":
+					fGridData.verticalAlignment = SWT.BOTTOM;
+					break;
+				case "o":
+					fGridData.verticalAlignment = SWT.FILL;
+					break;
+				case "^":
+					// fall through
+				default:
+					fGridData.verticalAlignment = SWT.TOP;
+					break;
+				}
+
+			} else
+				fGridData.verticalAlignment = SWT.TOP;
+
+			fGridData.grabExcessVerticalSpace = ("!".equals(matcher.group(8)));
+
+		} else
+			throw new IllegalArgumentException("Cannot parse location: " + layout);
+	}
+
+	public Point getPosition() {
+		return fPosition;
+	}
+
+	public GridData getLayoutData() {
+		return fGridData;
+	}
+}
diff --git a/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/Reference.java b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/Reference.java
new file mode 100644
index 0000000..b0b1af8
--- /dev/null
+++ b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/Reference.java
@@ -0,0 +1,54 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Christian Pontesegger 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:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.modules.platform.uibuilder;
+
+public class Reference {
+	private final Object fDelegate;
+
+	public Reference(Object delegate) {
+		fDelegate = delegate;
+	}
+
+	public Object getDelegate() {
+		return fDelegate;
+	}
+
+	@Override
+	public String toString() {
+		return "@" + fDelegate.toString();
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = (prime * result) + ((fDelegate == null) ? 0 : fDelegate.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		final Reference other = (Reference) obj;
+		if (fDelegate == null) {
+			if (other.fDelegate != null)
+				return false;
+		} else if (!fDelegate.equals(other.fDelegate))
+			return false;
+		return true;
+	}
+}
diff --git a/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/ScriptableDialog.java b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/ScriptableDialog.java
new file mode 100644
index 0000000..57bc1a8
--- /dev/null
+++ b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/ScriptableDialog.java
@@ -0,0 +1,152 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Christian Pontesegger 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:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.ease.modules.platform.uibuilder;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.jface.viewers.StructuredViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.List;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+public class ScriptableDialog extends TitleAreaDialog {
+
+	private Composite fContainer;
+	private final DialogRunnable fRunnableOnCreation;
+
+	private final Map<Object, Object> fData = new HashMap<>();
+	private final Map<Control, StructuredViewer> fViewers = new HashMap<>();
+
+	private String fMessageText = null;
+	private String fTitleText = null;
+	private Point fInitialSize = new Point(450, 450);
+
+	/**
+	 * Create the dialog.
+	 *
+	 * @param parentShell
+	 */
+	public ScriptableDialog(Shell parentShell, DialogRunnable runnableOnCreation) {
+		super(parentShell);
+		fRunnableOnCreation = runnableOnCreation;
+	}
+
+	/**
+	 * Create contents of the dialog.
+	 *
+	 * @param parent
+	 */
+	@Override
+	protected Control createDialogArea(Composite parent) {
+		final Composite area = (Composite) super.createDialogArea(parent);
+		fContainer = new Composite(area, SWT.NONE);
+		fContainer.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+		if (fTitleText != null)
+			setTitle(fTitleText);
+
+		if (fMessageText != null)
+			setMessage(fMessageText);
+
+		fRunnableOnCreation.setComposite(fContainer);
+		fRunnableOnCreation.setDialog(this);
+		fRunnableOnCreation.run();
+
+		return area;
+	}
+
+	public Composite getContainer() {
+		return fContainer;
+	}
+
+	/**
+	 * Create contents of the button bar.
+	 *
+	 * @param parent
+	 */
+	@Override
+	protected void createButtonsForButtonBar(Composite parent) {
+		createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true);
+		createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
+	}
+
+	/**
+	 * Return the initial size of the dialog.
+	 */
+	@Override
+	protected Point getInitialSize() {
+		return fInitialSize;
+	}
+
+	public void setInitialSize(int x, int y) {
+		fInitialSize = new Point(x, y);
+	}
+
+	@Override
+	protected void okPressed() {
+		storeChildren(fContainer);
+
+		super.okPressed();
+	}
+
+	private void storeChildren(Composite container) {
+		for (final Control child : container.getChildren()) {
+
+			if (child instanceof Text)
+				fData.put(child, ((Text) child).getText());
+
+			else if (child instanceof Button)
+				fData.put(child, ((Button) child).getSelection());
+
+			else if (child instanceof Combo) {
+
+				final StructuredViewer viewer = fViewers.get(child);
+				if (viewer != null)
+					fData.put(viewer, viewer.getStructuredSelection().getFirstElement());
+
+			} else if (child instanceof List) {
+
+				final StructuredViewer viewer = fViewers.get(child);
+				if (viewer != null)
+					fData.put(viewer, viewer.getStructuredSelection().toArray());
+
+			} else if (child instanceof Composite)
+				// do this after all others as eg combo is a composite
+				storeChildren((Composite) child);
+		}
+	}
+
+	public Object getData(Object control) {
+		return fData.get(control);
+	}
+
+	public void setMessageText(String messageText) {
+		fMessageText = messageText;
+	}
+
+	public void setTitleText(String titleText) {
+		fTitleText = titleText;
+	}
+
+	public void registerViewer(Control control, StructuredViewer viewer) {
+		fViewers.put(control, viewer);
+	}
+}
diff --git a/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/UIBuilderModule.java b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/UIBuilderModule.java
new file mode 100644
index 0000000..b9c7e6ee
--- /dev/null
+++ b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/UIBuilderModule.java
@@ -0,0 +1,529 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Christian Pontesegger 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:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.modules.platform.uibuilder;
+
+import java.util.List;
+
+import org.eclipse.e4.ui.model.application.MApplication;
+import org.eclipse.e4.ui.model.application.ui.basic.MBasicFactory;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.model.application.ui.basic.MPartStack;
+import org.eclipse.e4.ui.workbench.IWorkbench;
+import org.eclipse.e4.ui.workbench.modeling.EModelService;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.e4.ui.workbench.modeling.EPartService.PartState;
+import org.eclipse.ease.modules.AbstractScriptModule;
+import org.eclipse.ease.modules.ScriptParameter;
+import org.eclipse.ease.modules.WrapToScript;
+import org.eclipse.ease.modules.platform.UIModule;
+import org.eclipse.ease.tools.RunnableWithResult;
+import org.eclipse.jface.viewers.ArrayContentProvider;
+import org.eclipse.jface.viewers.ComboViewer;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.ListViewer;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IWorkbenchListener;
+import org.eclipse.ui.PlatformUI;
+
+/**
+ * Create dynamic views by adding SWT controls via script commands to a dynamic {@link GridLayout}. Each element allow to provide layout data, which accepts a
+ * fairly simple syntax: &lt;coordinates&gt; &lt;horizontal align&gt; &lt;vertical align&gt;
+ *
+ * <dl>
+ * <dd>coordinates</dd>
+ * <dt>Provides X/Y coordinates for elements in the grid where 1/1 denotes the upper left corner. 2/1 would create the element in the second column of the first
+ * row. Coordinates may also provide rowSpan/columnSpan information: 1-3/2-3 would create the element spanning columns 1 to 3 in rows 2 to 3.
+ * {@module #showGrid(boolean)} adds visual indications to help in layouting.</dt>
+ *
+ * <dd>horizontal alignment</dd>
+ * <dt>
+ * <ul>
+ * <li>&lt; ... align left</li>
+ * <li>x ... align center</li>
+ * <li>&gt; ... align right</li>
+ * <li>o ... fill</li>
+ * <li>o! ... grab horizontal space (! may be added to any alignment)</li>
+ * </ul>
+ * </dt>
+ *
+ * <dd>vertical alignment</dd>
+ * <dt>
+ * <ul>
+ * <li>^ ... align top</li>
+ * <li>x ... align middle</li>
+ * <li>v ... align bottom</li>
+ * <li>o ... fill</li>
+ * <li>o! ... grab vertical space (! may be added to any alignment)</li>
+ * </ul>
+ * </dt> Most methods of this module deal with SWT components. To access them the code needs to be run in the UI thread. See {@link UIModule} for more
+ * information.
+ * </dl>
+ */
+public class UIBuilderModule extends AbstractScriptModule {
+
+	private ViewModel fViewModel;
+	private CompositeRenderer fRenderer;
+	private ScriptableDialog fScriptableDialog;
+
+	/**
+	 * Create a view with scripted content. Automatically sets the active composite for further commands. This view will not be stored when the workbench gets
+	 * closed.
+	 *
+	 * @param title
+	 *            view title
+	 * @param onTopOfView
+	 *            name/ID of an existing view to put this view on top of
+	 * @return view instance
+	 * @throws Throwable
+	 *             when the view cannot be created
+	 */
+	@WrapToScript
+	public MPart createView(String title, @ScriptParameter(defaultValue = ScriptParameter.NULL) String onTopOfView) throws Throwable {
+		final RunnableWithResult<MPart> runnable = new RunnableWithResult<MPart>() {
+
+			@Override
+			public void runWithTry() throws Throwable {
+				final EPartService partService = PlatformUI.getWorkbench().getService(EPartService.class);
+				final IWorkbench workbench = PlatformUI.getWorkbench().getService(IWorkbench.class);
+				final MApplication mApplication = workbench.getApplication();
+
+				final EModelService modelService = PlatformUI.getWorkbench().getService(EModelService.class);
+
+				// create part
+				final MPart part = MBasicFactory.INSTANCE.createPart();
+				part.setLabel(title);
+				part.setElementId("org.eclipse.ease.view.dynamic:1");
+				part.setCloseable(true);
+				part.getPersistedState().put(IWorkbench.PERSIST_STATE, Boolean.FALSE.toString());
+
+				// find stack to put it into
+				final List<MPartStack> stacks = modelService.findElements(mApplication, null, MPartStack.class, null);
+				stacks.get(0).getChildren().add(part);
+				partService.showPart(part, PartState.ACTIVATE);
+
+				setResult(part);
+
+				setComposite((Composite) part.getWidget());
+
+				// make sure to close this window before we terminate. Eclipse would store it in its layout actually destroying all layout data.
+				PlatformUI.getWorkbench().addWorkbenchListener(new IWorkbenchListener() {
+
+					@Override
+					public boolean preShutdown(org.eclipse.ui.IWorkbench workbench, boolean forced) {
+						partService.hidePart(part, true);
+						return true;
+					}
+
+					@Override
+					public void postShutdown(org.eclipse.ui.IWorkbench workbench) {
+					}
+				});
+			}
+		};
+
+		Display.getDefault().syncExec(runnable);
+
+		return runnable.getResultFromTry();
+	}
+
+	/**
+	 * Create a dialog with scripted content. To populate the dialog we need to provide a callback method that creates UI elements for the dialog area. After
+	 * the setup the dialog can be shown by calling dialog.open(). To retrieve input data after the dialog got closed call dialog.getData(uiComponent).
+	 *
+	 * @param layoutCode
+	 *            script code to be called when dialog window is created
+	 * @param title
+	 *            dialog title text displayed in header area
+	 * @param message
+	 *            dialog info message displayed in header area
+	 * @return dialog instance
+	 * @throws Throwable
+	 *             when dialog cannot be created.
+	 */
+	@WrapToScript
+	public ScriptableDialog createDialog(Object layoutCode, @ScriptParameter(defaultValue = "Dialog") String title,
+			@ScriptParameter(defaultValue = ScriptParameter.NULL) String message) throws Throwable {
+		return runInUIThread(new RunnableWithResult<ScriptableDialog>() {
+			@Override
+			public void runWithTry() throws Throwable {
+				final ScriptableDialog dialog = new ScriptableDialog(Display.getDefault().getActiveShell(), new DialogRunnable() {
+
+					@Override
+					public void run() {
+						UIBuilderModule.this.setComposite(getComposite());
+						fScriptableDialog = getDialog();
+						getScriptEngine().inject(layoutCode);
+						fScriptableDialog = null;
+					}
+				});
+
+				dialog.setTitleText(title);
+				dialog.setMessageText(message);
+
+				setResult(dialog);
+			}
+		});
+	}
+
+	/**
+	 * Sets the active composite for further commands. The composite layout will be set to {@link GridLayout} if not already done.
+	 *
+	 * @param composite
+	 *            composite to be used
+	 */
+	@WrapToScript
+	public void setComposite(Composite composite) {
+		if ((composite.getLayout() == null) || !(composite.getLayout() instanceof GridLayout)) {
+			final GridLayout gridLayout = new GridLayout();
+			gridLayout.numColumns = 1;
+
+			composite.setLayout(gridLayout);
+		}
+
+		fRenderer = new CompositeRenderer(composite);
+		fViewModel = new ViewModel(fRenderer);
+	}
+
+	/**
+	 * Get the active composite.
+	 *
+	 * @return active composite or <code>null</code>
+	 */
+	@WrapToScript
+	public Composite getComposite() {
+		return fRenderer.getParent();
+	}
+
+	/**
+	 * Set the minimum column count. If the current column count is already equal or higher then this method does nothing.
+	 *
+	 * @param columns
+	 *            minimum amount of columns
+	 */
+	@WrapToScript
+	public void setColumnCount(int columns) {
+		Display.getDefault().syncExec(() -> {
+			fViewModel.setColumnCount(columns);
+			fRenderer.update();
+		});
+	}
+
+	/**
+	 * Create a label.
+	 *
+	 * @param label
+	 *            label text
+	 * @param layout
+	 *            layout data (see module documentation)
+	 * @return label instance
+	 * @throws Throwable
+	 *             on any SWT error
+	 */
+	@WrapToScript
+	public Label createLabel(String label, @ScriptParameter(defaultValue = "<") String layout) throws Throwable {
+		return runInUIThread(new RunnableWithResult<Label>() {
+			@Override
+			public void runWithTry() throws Throwable {
+				final Label swtLabel = new Label(fRenderer.getParent(), SWT.NONE);
+				swtLabel.setText(label);
+
+				fViewModel.insertElement(swtLabel, new Location(layout));
+				fRenderer.update();
+
+				setResult(swtLabel);
+			}
+		});
+	}
+
+	/**
+	 * Create a separator.
+	 *
+	 * @param horizontal
+	 *            <code>true</code> for horizontal, <code>false</code> for vertical
+	 * @param layout
+	 *            layout data (see module documentation)
+	 * @return separator instance
+	 * @throws Throwable
+	 *             on any SWT error
+	 */
+	@WrapToScript
+	public Label createSeparator(@ScriptParameter(defaultValue = "true") boolean horizontal, @ScriptParameter(defaultValue = "o") String layout)
+			throws Throwable {
+		return runInUIThread(new RunnableWithResult<Label>() {
+			@Override
+			public void runWithTry() throws Throwable {
+				final Label swtLabel = new Label(fRenderer.getParent(), SWT.SEPARATOR | (horizontal ? SWT.HORIZONTAL : SWT.VERTICAL));
+
+				fViewModel.insertElement(swtLabel, new Location(layout));
+				fRenderer.update();
+
+				setResult(swtLabel);
+			}
+		});
+	}
+
+	/**
+	 * Create a single line text input.
+	 *
+	 * @param layout
+	 *            layout data (see module documentation)
+	 * @return text instance
+	 * @throws Throwable
+	 *             on any SWT error
+	 */
+	@WrapToScript
+	public Text createText(@ScriptParameter(defaultValue = "o!") String layout) throws Throwable {
+		return runInUIThread(new RunnableWithResult<Text>() {
+			@Override
+			public void runWithTry() throws Throwable {
+				final Text text = new Text(fRenderer.getParent(), SWT.BORDER);
+
+				fViewModel.insertElement(text, new Location(layout));
+				fRenderer.update();
+
+				setResult(text);
+			}
+		});
+	}
+
+	/**
+	 * Create a multi line text input.
+	 *
+	 * @param layout
+	 *            layout data (see module documentation)
+	 * @return text instance
+	 * @throws Throwable
+	 *             on any SWT error
+	 */
+	@WrapToScript
+	public Text createTextBox(@ScriptParameter(defaultValue = "o!") String layout) throws Throwable {
+		return runInUIThread(new RunnableWithResult<Text>() {
+			@Override
+			public void runWithTry() throws Throwable {
+				final Text text = new Text(fRenderer.getParent(), SWT.BORDER | SWT.MULTI | SWT.WRAP);
+
+				fViewModel.insertElement(text, new Location(layout));
+				fRenderer.update();
+
+				setResult(text);
+			}
+		});
+	}
+
+	/**
+	 * Create a push button.
+	 *
+	 * @param label
+	 *            button text
+	 * @param callback
+	 *            callback code when button gets pressed
+	 * @param layout
+	 *            layout data (see module documentation)
+	 * @return button instance
+	 * @throws Throwable
+	 *             on any SWT error
+	 */
+	@WrapToScript
+	public Button createButton(String label, Object callback, @ScriptParameter(defaultValue = ScriptParameter.NULL) String layout) throws Throwable {
+		return runInUIThread(new RunnableWithResult<Button>() {
+			@Override
+			public void runWithTry() throws Throwable {
+				final Button button = new Button(fRenderer.getParent(), SWT.NONE);
+				button.setText(label);
+
+				button.addSelectionListener(new SelectionAdapter() {
+
+					@Override
+					public void widgetSelected(SelectionEvent e) {
+						getScriptEngine().inject(callback);
+					}
+				});
+
+				fViewModel.insertElement(button, new Location(layout));
+				fRenderer.update();
+
+				setResult(button);
+			}
+		});
+	}
+
+	/**
+	 * Create a checkbox.
+	 *
+	 * @param label
+	 *            checkbox text
+	 * @param callback
+	 *            callback code when the checkbox gets ticked/unticked
+	 * @param layout
+	 *            layout data (see module documentation)
+	 * @return checkbox instance
+	 * @throws Throwable
+	 *             on any SWT error
+	 */
+	@WrapToScript
+	public Button createCheckBox(String label, @ScriptParameter(defaultValue = ScriptParameter.NULL) Object callback,
+			@ScriptParameter(defaultValue = ScriptParameter.NULL) String layout) throws Throwable {
+		return runInUIThread(new RunnableWithResult<Button>() {
+			@Override
+			public void runWithTry() throws Throwable {
+				final Button button = new Button(fRenderer.getParent(), SWT.CHECK);
+				button.setText(label);
+
+				if (callback != null) {
+					button.addSelectionListener(new SelectionAdapter() {
+
+						@Override
+						public void widgetSelected(SelectionEvent e) {
+							getScriptEngine().inject(callback);
+						}
+					});
+				}
+
+				fViewModel.insertElement(button, new Location(layout));
+				fRenderer.update();
+
+				setResult(button);
+			}
+		});
+	}
+
+	/**
+	 * Create a combo box. The first element will automatically be selected.
+	 *
+	 * @param elements
+	 *            combo elements to display.
+	 * @param callback
+	 *            callback code when selection changes
+	 * @param layout
+	 *            layout data (see module documentation)
+	 * @return combo viewer instance
+	 * @throws Throwable
+	 *             on any SWT error
+	 */
+	@WrapToScript
+	public ComboViewer createCombo(String[] elements, @ScriptParameter(defaultValue = ScriptParameter.NULL) Object callback,
+			@ScriptParameter(defaultValue = ScriptParameter.NULL) String layout) throws Throwable {
+		return runInUIThread(new RunnableWithResult<ComboViewer>() {
+			@Override
+			public void runWithTry() throws Throwable {
+				final ComboViewer comboViewer = new ComboViewer(fRenderer.getParent());
+				comboViewer.setLabelProvider(new LabelProvider());
+				comboViewer.setContentProvider(ArrayContentProvider.getInstance());
+				comboViewer.setInput(elements);
+
+				comboViewer.setSelection(new StructuredSelection(elements[0]));
+
+				if (callback != null) {
+					comboViewer.addSelectionChangedListener(event -> getScriptEngine().inject(callback));
+				}
+
+				fViewModel.insertElement(comboViewer.getControl(), new Location(layout));
+				fRenderer.update();
+
+				if (fScriptableDialog != null)
+					fScriptableDialog.registerViewer(comboViewer.getControl(), comboViewer);
+
+				setResult(comboViewer);
+			}
+		});
+	}
+
+	/**
+	 * Create a list viewer.
+	 *
+	 * @param elements
+	 *            list elements to display
+	 * @param callback
+	 *            callback when selection changes
+	 * @param layout
+	 *            layout data (see module documentation)
+	 * @return list viewer instance
+	 * @throws Throwable
+	 *             on any SWT error
+	 */
+	@WrapToScript
+	public ListViewer createList(String[] elements, @ScriptParameter(defaultValue = ScriptParameter.NULL) Object callback,
+			@ScriptParameter(defaultValue = ScriptParameter.NULL) String layout) throws Throwable {
+		return runInUIThread(new RunnableWithResult<ListViewer>() {
+			@Override
+			public void runWithTry() throws Throwable {
+				final ListViewer listViewer = new ListViewer(fRenderer.getParent());
+				listViewer.setLabelProvider(new LabelProvider());
+				listViewer.setContentProvider(ArrayContentProvider.getInstance());
+				listViewer.setInput(elements);
+
+				if (callback != null) {
+					listViewer.addSelectionChangedListener(event -> getScriptEngine().inject(callback));
+				}
+
+				fViewModel.insertElement(listViewer.getControl(), new Location(layout));
+				fRenderer.update();
+
+				if (fScriptableDialog != null)
+					fScriptableDialog.registerViewer(listViewer.getControl(), listViewer);
+
+				setResult(listViewer);
+			}
+		});
+	}
+
+	/**
+	 * Add a generic control.
+	 *
+	 * @param control
+	 *            control to add
+	 * @param layout
+	 *            layout data (see module documentation)
+	 * @return control instance
+	 * @throws Throwable
+	 *             when control cannot be added
+	 */
+	@WrapToScript
+	public Control addControl(Control control, @ScriptParameter(defaultValue = ScriptParameter.NULL) String layout) throws Throwable {
+		return runInUIThread(new RunnableWithResult<Control>() {
+			@Override
+			public void runWithTry() throws Throwable {
+				fViewModel.insertElement(control, new Location(layout));
+				fRenderer.update();
+
+				setResult(control);
+			}
+		});
+	}
+
+	/**
+	 * Display a grid aiding in layouting. When the grid is displayed, empty cells do show a label with their location coordinates.
+	 *
+	 * @param showGrid
+	 *            <code>true</code> to display.
+	 */
+	@WrapToScript
+	public void showGrid(@ScriptParameter(defaultValue = "true") boolean showGrid) {
+		Display.getDefault().syncExec(() -> fRenderer.setShowGrid(showGrid));
+	}
+
+	private <T> T runInUIThread(RunnableWithResult<T> runnable) throws Throwable {
+		Display.getDefault().syncExec(runnable);
+
+		return runnable.getResultFromTry();
+	}
+}
diff --git a/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/ViewModel.java b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/ViewModel.java
new file mode 100644
index 0000000..453b5b2
--- /dev/null
+++ b/plugins/org.eclipse.ease.modules.platform/src/org/eclipse/ease/modules/platform/uibuilder/ViewModel.java
@@ -0,0 +1,174 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Christian Pontesegger 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:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.modules.platform.uibuilder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Control;
+
+public class ViewModel {
+
+	private final List<Object> fElements = new ArrayList<>();
+	private int fRows = 0;
+	private int fColumns = 0;
+
+	private final IRenderer fRenderer;
+
+	public ViewModel(IRenderer renderer) {
+		fRenderer = renderer;
+		fRenderer.setViewModel(this);
+	}
+
+	public List<Object> getElements() {
+		return fElements;
+	}
+
+	public void insertElement(Object element, Location location) {
+
+		if ((location.getPosition().x == Location.DYNAMIC_POSITION) || (location.getPosition().y == Location.DYNAMIC_POSITION)) {
+			// find position to insert
+
+			// default
+			location.getPosition().x = 1;
+			location.getPosition().y = 1;
+
+			for (int index = fElements.size() - 1; index >= 0; index--) {
+				if (!(fElements.get(index) instanceof IPlaceHolder) && !(fElements.get(index) instanceof Reference)) {
+					final Point target = indexToPoint(index + 1);
+					location.getPosition().x = target.x;
+					location.getPosition().y = target.y;
+					break;
+				}
+			}
+		}
+
+		// now we got a fixed position to insert
+		ensureColumnCount((location.getPosition().x + location.getLayoutData().horizontalSpan) - 1);
+		ensureRowCount((location.getPosition().y + location.getLayoutData().verticalSpan) - 1);
+
+		splitSpanElements(new Rectangle(location.getPosition().x, location.getPosition().y, location.getLayoutData().horizontalSpan,
+				location.getLayoutData().verticalSpan));
+
+		replaceElementAt(location.getPosition(), element);
+		if ((location.getLayoutData().horizontalSpan > 1) || (location.getLayoutData().verticalSpan > 1)) {
+			final Reference reference = new Reference(element);
+			for (int width = 0; width < location.getLayoutData().horizontalSpan; width++) {
+				for (int height = 0; height < location.getLayoutData().verticalSpan; height++) {
+					if ((width > 0) || (height > 0))
+						replaceElementAt(new Point(location.getPosition().x + width, location.getPosition().y + height), reference);
+				}
+			}
+		}
+
+		if (element instanceof Control)
+			((Control) element).setLayoutData(location.getLayoutData());
+	}
+
+	public int getColumnCount() {
+		return fColumns;
+	}
+
+	public int getRowCount() {
+		return fRows;
+	}
+
+	private void replaceElementAt(Point position, Object element) {
+		final int index = pointToIndex(position);
+		final Object oldElement = fElements.remove(index);
+		if (oldElement instanceof Control)
+			((Control) oldElement).dispose();
+
+		fElements.add(index, element);
+	}
+
+	private void splitSpanElements(Rectangle rectangle) {
+		for (int column = rectangle.x; column < (rectangle.x + rectangle.width); column++) {
+			for (int row = rectangle.y; row < (rectangle.y + rectangle.height); row++) {
+				final Object element = fElements.get(pointToIndex(new Point(column, row)));
+				if (element instanceof Reference)
+					splitElement(((Reference) element).getDelegate());
+			}
+		}
+	}
+
+	private void splitElement(Object elementToSplit) {
+		for (int index = 0; index < fElements.size(); index++) {
+			if (fElements.get(index).equals(elementToSplit)) {
+
+				final Point position = indexToPoint(index);
+				replaceElementAt(position, fRenderer.createPlaceHolder(position));
+
+			} else if ((fElements.get(index) instanceof Reference) && (((Reference) fElements.get(index)).getDelegate().equals(elementToSplit))) {
+				final Point position = indexToPoint(index);
+				replaceElementAt(position, fRenderer.createPlaceHolder(position));
+			}
+		}
+	}
+
+	public Point indexToPoint(int index) {
+		return new Point((index % fColumns) + 1, (index / fColumns) + 1);
+	}
+
+	private int pointToIndex(Point point) {
+		return (((point.y - 1) * fColumns) + point.x) - 1;
+	}
+
+	private void ensureColumnCount(int columns) {
+		while (fColumns < columns)
+			addColumn();
+	}
+
+	private void addColumn() {
+		if (fElements.isEmpty()) {
+			final IPlaceHolder placeHolder = fRenderer.createPlaceHolder(new Point(1, 1));
+			fElements.add(placeHolder);
+			fRows = 1;
+			fColumns = 1;
+
+		} else {
+			for (int row = fRows - 1; row >= 0; row--) {
+				final Point position = new Point(fColumns + 1, row + 1);
+				final IPlaceHolder placeHolder = fRenderer.createPlaceHolder(position);
+				fElements.add(pointToIndex(position), placeHolder);
+			}
+			fColumns++;
+		}
+	}
+
+	private void ensureRowCount(int rows) {
+		while (fRows < rows)
+			addRow();
+	}
+
+	private void addRow() {
+		if (fElements.isEmpty()) {
+			final IPlaceHolder placeHolder = fRenderer.createPlaceHolder(new Point(1, 1));
+			fElements.add(placeHolder);
+			fRows = 1;
+			fColumns = 1;
+
+		} else {
+			fRows++;
+			for (int column = 1; column <= fColumns; column++) {
+				final IPlaceHolder placeHolder = fRenderer.createPlaceHolder(new Point(column, fRows));
+				fElements.add(placeHolder);
+			}
+		}
+	}
+
+	public void setColumnCount(int columns) {
+		ensureColumnCount(columns);
+	}
+}
diff --git a/tests/org.eclipse.ease.modules.platform.test/src/org/eclipse/ease/modules/platform/uibuilder/LocationTest.java b/tests/org.eclipse.ease.modules.platform.test/src/org/eclipse/ease/modules/platform/uibuilder/LocationTest.java
new file mode 100644
index 0000000..072313f
--- /dev/null
+++ b/tests/org.eclipse.ease.modules.platform.test/src/org/eclipse/ease/modules/platform/uibuilder/LocationTest.java
@@ -0,0 +1,187 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Christian Pontesegger 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:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.modules.platform.uibuilder;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.eclipse.swt.SWT;
+import org.junit.Test;
+
+public class LocationTest {
+
+	@Test
+	public void nullLocation() {
+		final Location location = new Location(null);
+		assertEquals(Location.DYNAMIC_POSITION, location.getPosition().x);
+		assertEquals(Location.DYNAMIC_POSITION, location.getPosition().y);
+
+		assertEquals(SWT.LEFT, location.getLayoutData().horizontalAlignment);
+		assertEquals(1, location.getLayoutData().horizontalSpan);
+		assertFalse(location.getLayoutData().grabExcessHorizontalSpace);
+
+		assertEquals(SWT.TOP, location.getLayoutData().verticalAlignment);
+		assertEquals(1, location.getLayoutData().verticalSpan);
+		assertFalse(location.getLayoutData().grabExcessVerticalSpace);
+	}
+
+	@Test
+	public void emptyLocation() {
+		final Location location = new Location("");
+		assertEquals(Location.DYNAMIC_POSITION, location.getPosition().x);
+		assertEquals(Location.DYNAMIC_POSITION, location.getPosition().y);
+
+		assertEquals(SWT.LEFT, location.getLayoutData().horizontalAlignment);
+		assertEquals(1, location.getLayoutData().horizontalSpan);
+		assertFalse(location.getLayoutData().grabExcessHorizontalSpace);
+
+		assertEquals(SWT.TOP, location.getLayoutData().verticalAlignment);
+		assertEquals(1, location.getLayoutData().verticalSpan);
+		assertFalse(location.getLayoutData().grabExcessVerticalSpace);
+	}
+
+	@Test
+	public void bounds1x1() {
+		final Location location = new Location("1/1");
+		assertEquals(1, location.getPosition().x);
+		assertEquals(1, location.getPosition().y);
+
+		assertEquals(1, location.getLayoutData().horizontalSpan);
+		assertEquals(1, location.getLayoutData().verticalSpan);
+	}
+
+	@Test
+	public void bounds2x1() {
+		final Location location = new Location("2/1");
+		assertEquals(2, location.getPosition().x);
+		assertEquals(1, location.getPosition().y);
+
+		assertEquals(1, location.getLayoutData().horizontalSpan);
+		assertEquals(1, location.getLayoutData().verticalSpan);
+	}
+
+	@Test
+	public void bounds1x2() {
+		final Location location = new Location("1/2");
+		assertEquals(1, location.getPosition().x);
+		assertEquals(2, location.getPosition().y);
+
+		assertEquals(1, location.getLayoutData().horizontalSpan);
+		assertEquals(1, location.getLayoutData().verticalSpan);
+	}
+
+	@Test
+	public void boundsSpan() {
+		final Location location = new Location("3-12/4-7");
+		assertEquals(3, location.getPosition().x);
+		assertEquals(4, location.getPosition().y);
+
+		assertEquals(10, location.getLayoutData().horizontalSpan);
+		assertEquals(4, location.getLayoutData().verticalSpan);
+	}
+
+	@Test
+	public void horizontalLayoutLeft() {
+		final Location location = new Location("<");
+
+		assertEquals(SWT.LEFT, location.getLayoutData().horizontalAlignment);
+		assertEquals(SWT.TOP, location.getLayoutData().verticalAlignment);
+		assertFalse(location.getLayoutData().grabExcessHorizontalSpace);
+	}
+
+	@Test
+	public void horizontalLayoutCenter() {
+		final Location location = new Location("x");
+
+		assertEquals(SWT.CENTER, location.getLayoutData().horizontalAlignment);
+		assertEquals(SWT.TOP, location.getLayoutData().verticalAlignment);
+		assertFalse(location.getLayoutData().grabExcessHorizontalSpace);
+	}
+
+	@Test
+	public void horizontalLayoutRight() {
+		final Location location = new Location(">");
+
+		assertEquals(SWT.RIGHT, location.getLayoutData().horizontalAlignment);
+		assertEquals(SWT.TOP, location.getLayoutData().verticalAlignment);
+		assertFalse(location.getLayoutData().grabExcessHorizontalSpace);
+	}
+
+	@Test
+	public void horizontalLayoutFill() {
+		final Location location = new Location("o");
+
+		assertEquals(SWT.FILL, location.getLayoutData().horizontalAlignment);
+		assertEquals(SWT.TOP, location.getLayoutData().verticalAlignment);
+		assertFalse(location.getLayoutData().grabExcessHorizontalSpace);
+	}
+
+	@Test
+	public void horizontalLayoutGrab() {
+		final Location location = new Location("o!");
+
+		assertEquals(SWT.FILL, location.getLayoutData().horizontalAlignment);
+		assertEquals(SWT.TOP, location.getLayoutData().verticalAlignment);
+		assertTrue(location.getLayoutData().grabExcessHorizontalSpace);
+	}
+
+	@Test
+	public void verticalLayoutTop() {
+		final Location location = new Location("^");
+
+		assertEquals(SWT.LEFT, location.getLayoutData().horizontalAlignment);
+		assertEquals(SWT.TOP, location.getLayoutData().verticalAlignment);
+		assertFalse(location.getLayoutData().grabExcessVerticalSpace);
+	}
+
+	@Test
+	public void verticalLayoutMiddle() {
+		final Location location = new Location("< x");
+
+		assertEquals(SWT.LEFT, location.getLayoutData().horizontalAlignment);
+		assertEquals(SWT.CENTER, location.getLayoutData().verticalAlignment);
+		assertFalse(location.getLayoutData().grabExcessVerticalSpace);
+	}
+
+	@Test
+	public void verticalLayoutBottom() {
+		final Location location = new Location("v");
+
+		assertEquals(SWT.LEFT, location.getLayoutData().horizontalAlignment);
+		assertEquals(SWT.BOTTOM, location.getLayoutData().verticalAlignment);
+		assertFalse(location.getLayoutData().grabExcessVerticalSpace);
+	}
+
+	@Test
+	public void verticalLayoutFill() {
+		final Location location = new Location("< o");
+
+		assertEquals(SWT.LEFT, location.getLayoutData().horizontalAlignment);
+		assertEquals(SWT.FILL, location.getLayoutData().verticalAlignment);
+		assertFalse(location.getLayoutData().grabExcessVerticalSpace);
+	}
+
+	@Test
+	public void verticalLayoutGrab() {
+		final Location location = new Location("< o!");
+
+		assertEquals(SWT.LEFT, location.getLayoutData().horizontalAlignment);
+		assertEquals(SWT.FILL, location.getLayoutData().verticalAlignment);
+		assertTrue(location.getLayoutData().grabExcessVerticalSpace);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void invalidColumn() {
+		new Location("0");
+	}
+}
diff --git a/tests/org.eclipse.ease.modules.platform.test/src/org/eclipse/ease/modules/platform/uibuilder/ViewModelTest.java b/tests/org.eclipse.ease.modules.platform.test/src/org/eclipse/ease/modules/platform/uibuilder/ViewModelTest.java
new file mode 100644
index 0000000..3a2d38c
--- /dev/null
+++ b/tests/org.eclipse.ease.modules.platform.test/src/org/eclipse/ease/modules/platform/uibuilder/ViewModelTest.java
@@ -0,0 +1,252 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Christian Pontesegger 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:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.modules.platform.uibuilder;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.eclipse.swt.graphics.Point;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class ViewModelTest {
+
+	private static IRenderer fRenderer;
+
+	private static class TextPlaceHolder implements IPlaceHolder {
+		private final Point fPosition;
+
+		public TextPlaceHolder(Point position) {
+			fPosition = position;
+		}
+
+		public TextPlaceHolder(int column, int row) {
+			this(new Point(column, row));
+		}
+
+		@Override
+		public String toString() {
+			return fPosition.x + "/" + fPosition.y;
+		}
+
+		@Override
+		public int hashCode() {
+			final int prime = 31;
+			int result = 1;
+			result = (prime * result) + ((fPosition == null) ? 0 : fPosition.hashCode());
+			return result;
+		}
+
+		@Override
+		public boolean equals(Object obj) {
+			if (this == obj)
+				return true;
+			if (obj == null)
+				return false;
+			if (getClass() != obj.getClass())
+				return false;
+			final TextPlaceHolder other = (TextPlaceHolder) obj;
+			if (fPosition == null) {
+				if (other.fPosition != null)
+					return false;
+			} else if (!fPosition.equals(other.fPosition))
+				return false;
+			return true;
+		}
+	}
+
+	@BeforeClass
+	public static void classSetup() {
+		fRenderer = new IRenderer() {
+
+			@Override
+			public void setViewModel(ViewModel viewModel) {
+			}
+
+			@Override
+			public IPlaceHolder createPlaceHolder(Point position) {
+				return new TextPlaceHolder(position);
+			}
+		};
+	}
+
+	@Test
+	public void emptyModel() {
+		final ViewModel viewModel = new ViewModel(fRenderer);
+		assertTrue(viewModel.getElements().isEmpty());
+		assertEquals(0, viewModel.getColumnCount());
+		assertEquals(0, viewModel.getRowCount());
+	}
+
+	@Test
+	public void singleElementOn1x1() {
+		final ViewModel viewModel = new ViewModel(fRenderer);
+		viewModel.insertElement("Element1x1", new Location("1/1"));
+		assertEquals(1, viewModel.getElements().size());
+		assertArrayEquals(new Object[] { "Element1x1" }, viewModel.getElements().toArray());
+
+		assertEquals(1, viewModel.getColumnCount());
+		assertEquals(1, viewModel.getRowCount());
+	}
+
+	@Test
+	public void singleColumn1x3() {
+		final ViewModel viewModel = new ViewModel(fRenderer);
+		viewModel.insertElement("Element1x1", new Location(""));
+		viewModel.insertElement("Element1x2", new Location(""));
+		viewModel.insertElement("Element1x3", new Location(""));
+		assertEquals(3, viewModel.getElements().size());
+		assertArrayEquals(new Object[] { "Element1x1", "Element1x2", "Element1x3" }, viewModel.getElements().toArray());
+
+		assertEquals(1, viewModel.getColumnCount());
+		assertEquals(3, viewModel.getRowCount());
+	}
+
+	@Test
+	public void singleRow3x1() {
+		final ViewModel viewModel = new ViewModel(fRenderer);
+		viewModel.setColumnCount(3);
+		viewModel.insertElement("Element1x1", new Location(""));
+		viewModel.insertElement("Element2x1", new Location(""));
+		viewModel.insertElement("Element3x1", new Location(""));
+		assertEquals(3, viewModel.getElements().size());
+		assertArrayEquals(new Object[] { "Element1x1", "Element2x1", "Element3x1" }, viewModel.getElements().toArray());
+
+		assertEquals(3, viewModel.getColumnCount());
+		assertEquals(1, viewModel.getRowCount());
+	}
+
+	@Test
+	public void insertionGaps() {
+		final ViewModel viewModel = new ViewModel(fRenderer);
+		viewModel.setColumnCount(2);
+		viewModel.insertElement("Element2x3", new Location("2/3"));
+
+		assertEquals(6, viewModel.getElements().size());
+		assertArrayEquals(new Object[] { new TextPlaceHolder(1, 1), new TextPlaceHolder(2, 1), new TextPlaceHolder(1, 2), new TextPlaceHolder(2, 2),
+				new TextPlaceHolder(1, 3), "Element2x3" }, viewModel.getElements().toArray());
+
+		assertEquals(2, viewModel.getColumnCount());
+		assertEquals(3, viewModel.getRowCount());
+	}
+
+	@Test
+	public void insertRandomOrder() {
+		final ViewModel viewModel = new ViewModel(fRenderer);
+		viewModel.setColumnCount(3);
+		viewModel.insertElement("Element2x2", new Location("2/2"));
+		viewModel.insertElement("Element1x1", new Location("1/1"));
+		viewModel.insertElement("Element3x1", new Location("3/1"));
+		viewModel.insertElement("Element1x2", new Location("1/2"));
+		viewModel.insertElement("Element3x2", new Location("3/2"));
+		viewModel.insertElement("Element2x1", new Location("2/1"));
+
+		assertEquals(6, viewModel.getElements().size());
+		assertArrayEquals(new Object[] { "Element1x1", "Element2x1", "Element3x1", "Element1x2", "Element2x2", "Element3x2" },
+				viewModel.getElements().toArray());
+
+		assertEquals(3, viewModel.getColumnCount());
+		assertEquals(2, viewModel.getRowCount());
+	}
+
+	@Test
+	public void replaceElement() {
+		final ViewModel viewModel = new ViewModel(fRenderer);
+		viewModel.insertElement("Element1x1", new Location("1/1"));
+		viewModel.insertElement("Replacement", new Location("1/1"));
+
+		assertEquals(1, viewModel.getElements().size());
+		assertArrayEquals(new Object[] { "Replacement" }, viewModel.getElements().toArray());
+
+		assertEquals(1, viewModel.getColumnCount());
+		assertEquals(1, viewModel.getRowCount());
+	}
+
+	@Test
+	public void extendColumns() {
+		final ViewModel viewModel = new ViewModel(fRenderer);
+		viewModel.setColumnCount(2);
+		viewModel.insertElement("Element1x1", new Location("1/1"));
+		viewModel.insertElement("Element2x1", new Location("2/1"));
+		viewModel.insertElement("Element1x2", new Location("1/2"));
+		viewModel.insertElement("Element2x2", new Location("2/2"));
+
+		viewModel.setColumnCount(3);
+
+		assertEquals(6, viewModel.getElements().size());
+		assertArrayEquals(new Object[] { "Element1x1", "Element2x1", new TextPlaceHolder(3, 1), "Element1x2", "Element2x2", new TextPlaceHolder(3, 2) },
+				viewModel.getElements().toArray());
+
+		assertEquals(3, viewModel.getColumnCount());
+		assertEquals(2, viewModel.getRowCount());
+	}
+
+	@Test
+	public void horizontalSpanElement() {
+		final ViewModel viewModel = new ViewModel(fRenderer);
+		viewModel.setColumnCount(3);
+		viewModel.insertElement("Element1x1", new Location("1/1"));
+		viewModel.insertElement("Element2-3x1", new Location("2-3/1"));
+		viewModel.insertElement("Element1x2", new Location("1/2"));
+		viewModel.insertElement("Element2x2", new Location("2/2"));
+		viewModel.insertElement("Element3x2", new Location("3/2"));
+
+		assertEquals(6, viewModel.getElements().size());
+		assertArrayEquals(new Object[] { "Element1x1", "Element2-3x1", new Reference("Element2-3x1"), "Element1x2", "Element2x2", "Element3x2" },
+				viewModel.getElements().toArray());
+
+		assertEquals(3, viewModel.getColumnCount());
+		assertEquals(2, viewModel.getRowCount());
+	}
+
+	@Test
+	public void verticalSpanElement() {
+		final ViewModel viewModel = new ViewModel(fRenderer);
+		viewModel.setColumnCount(2);
+		viewModel.insertElement("Element1x1", new Location("1/1"));
+		viewModel.insertElement("Element2x1", new Location("2/1"));
+		viewModel.insertElement("Element1x2-3", new Location("1/2-3"));
+		viewModel.insertElement("Element2x2", new Location("2/2"));
+		viewModel.insertElement("Element2x3", new Location("2/3"));
+
+		assertEquals(6, viewModel.getElements().size());
+		assertArrayEquals(new Object[] { "Element1x1", "Element2x1", "Element1x2-3", "Element2x2", new Reference("Element1x2-3"), "Element2x3" },
+				viewModel.getElements().toArray());
+
+		assertEquals(2, viewModel.getColumnCount());
+		assertEquals(3, viewModel.getRowCount());
+	}
+
+	@Test
+	public void replaceSpanElement() {
+		final ViewModel viewModel = new ViewModel(fRenderer);
+		viewModel.setColumnCount(3);
+		viewModel.insertElement("Element1x1", new Location("1/1"));
+		viewModel.insertElement("Element2x1", new Location("2/1"));
+		viewModel.insertElement("Element3x1", new Location("3/1"));
+		viewModel.insertElement("Element1x2", new Location("1/2"));
+		viewModel.insertElement("Element2-3x2-3", new Location("2-3/2-3"));
+		viewModel.insertElement("Element1x3", new Location("1/3"));
+
+		viewModel.insertElement("Replacement", new Location("3-4/2"));
+
+		assertEquals(12, viewModel.getElements().size());
+		assertArrayEquals(
+				new Object[] { "Element1x1", "Element2x1", "Element3x1", new TextPlaceHolder(4, 1), "Element1x2", new TextPlaceHolder(2, 2), "Replacement",
+						new Reference("Replacement"), "Element1x3", new TextPlaceHolder(2, 3), new TextPlaceHolder(3, 3), new TextPlaceHolder(4, 3) },
+				viewModel.getElements().toArray());
+
+		assertEquals(4, viewModel.getColumnCount());
+		assertEquals(3, viewModel.getRowCount());
+	}
+}