Bug 572553: [Script Shell] Disable updating modules dropin when hidden

  refactor modules dropin for better Unit test support
  disable dropin updates when composite is hidden

Change-Id: Ibbf3d5c96479e5a3c5f68f3c272b6b0452e78170
diff --git a/plugins/org.eclipse.ease.ui/plugin.xml b/plugins/org.eclipse.ease.ui/plugin.xml
index 739abfe..6c3d3f6 100644
--- a/plugins/org.eclipse.ease.ui/plugin.xml
+++ b/plugins/org.eclipse.ease.ui/plugin.xml
@@ -486,7 +486,7 @@
       <dropHandler
             class="org.eclipse.ease.ui.dnd.JarDropHandler"></dropHandler>
       <dropin
-            class="org.eclipse.ease.ui.view.ModuleStackDropin"
+            class="org.eclipse.ease.ui.views.shell.dropins.modules.ModulesStackDropin"
             priority="-1">
       </dropin>
       <dropHandler
diff --git a/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/view/ModuleStackDropin.java b/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/view/ModuleStackDropin.java
deleted file mode 100644
index 70ad12f..0000000
--- a/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/view/ModuleStackDropin.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2016 Christian Pontesegger and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v2.0
- * which accompanies this distribution, and is available at
- * https://www.eclipse.org/legal/epl-2.0/
- *
- * Contributors:
- *     Christian Pontesegger - initial API and implementation
- *******************************************************************************/
-package org.eclipse.ease.ui.view;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.eclipse.ease.IExecutionListener;
-import org.eclipse.ease.IReplEngine;
-import org.eclipse.ease.IScriptEngine;
-import org.eclipse.ease.Script;
-import org.eclipse.ease.modules.IEnvironment;
-import org.eclipse.ease.modules.ModuleDefinition;
-import org.eclipse.ease.service.IScriptService;
-import org.eclipse.ease.ui.Activator;
-import org.eclipse.ease.ui.Messages;
-import org.eclipse.ease.ui.modules.ui.ModulesDragListener;
-import org.eclipse.ease.ui.views.shell.dropins.IShellDropin;
-import org.eclipse.jface.layout.TableColumnLayout;
-import org.eclipse.jface.resource.ImageDescriptor;
-import org.eclipse.jface.util.LocalSelectionTransfer;
-import org.eclipse.jface.viewers.ColumnLabelProvider;
-import org.eclipse.jface.viewers.ColumnWeightData;
-import org.eclipse.jface.viewers.IStructuredContentProvider;
-import org.eclipse.jface.viewers.TableViewer;
-import org.eclipse.jface.viewers.TableViewerColumn;
-import org.eclipse.swt.SWT;
-import org.eclipse.swt.dnd.DND;
-import org.eclipse.swt.dnd.DragSourceEvent;
-import org.eclipse.swt.dnd.TextTransfer;
-import org.eclipse.swt.dnd.Transfer;
-import org.eclipse.swt.graphics.Image;
-import org.eclipse.swt.widgets.Composite;
-import org.eclipse.swt.widgets.Display;
-import org.eclipse.swt.widgets.Table;
-import org.eclipse.swt.widgets.TableColumn;
-import org.eclipse.ui.IWorkbenchPartSite;
-import org.eclipse.ui.PlatformUI;
-
-public class ModuleStackDropin implements IShellDropin, IExecutionListener {
-
-	private TableViewer fModulesTable;
-	private IReplEngine fEngine;
-
-	@Override
-	public void setScriptEngine(IReplEngine engine) {
-		if (fEngine != null)
-			fEngine.removeExecutionListener(this);
-
-		fEngine = engine;
-
-		if (fEngine != null)
-			fEngine.addExecutionListener(this);
-
-		// set tree input
-		if (fModulesTable != null) {
-			fModulesTable.setInput(fEngine);
-			Display.getDefault().asyncExec(() -> fModulesTable.refresh());
-		}
-	}
-
-	@Override
-	public Composite createPartControl(IWorkbenchPartSite site, Composite parent) {
-		final Composite composite = new Composite(parent, SWT.NONE);
-		final TableColumnLayout tableColumnLayout = new TableColumnLayout();
-		composite.setLayout(tableColumnLayout);
-
-		fModulesTable = new TableViewer(composite, SWT.BORDER);
-		final Table table = fModulesTable.getTable();
-		table.setHeaderVisible(true);
-		table.setLinesVisible(true);
-
-		fModulesTable.setContentProvider((IStructuredContentProvider) inputElement -> {
-			if (inputElement instanceof IScriptEngine) {
-				final IEnvironment environment = IEnvironment.getEnvironment((IScriptEngine) inputElement);
-				if (environment != null) {
-					final List<Object> loadedModules = new ArrayList<>();
-
-					for (final Object instance : new ArrayList<>(environment.getModules())) {
-						final ModuleDefinition definition = getDefinition(instance);
-						loadedModules.add((definition != null) ? definition : instance);
-					}
-
-					return loadedModules.toArray();
-				}
-			}
-
-			return new Object[0];
-		});
-
-		final TableViewerColumn tableViewerColumn = new TableViewerColumn(fModulesTable, SWT.NONE);
-		final TableColumn column = tableViewerColumn.getColumn();
-		tableColumnLayout.setColumnData(column, new ColumnWeightData(1));
-		column.setText(Messages.ModuleStackDropin_module);
-		tableViewerColumn.setLabelProvider(new ColumnLabelProvider() {
-			@Override
-			public String getText(final Object element) {
-				if (element instanceof ModuleDefinition)
-					return ((ModuleDefinition) element).getName();
-
-				if (element != null)
-					return element.getClass().getCanonicalName();
-
-				return super.getText(element);
-			}
-
-			@Override
-			public Image getImage(final Object element) {
-				if (element instanceof ModuleDefinition) {
-					final ImageDescriptor icon = ((ModuleDefinition) element).getImageDescriptor();
-					if (icon != null)
-						return icon.createImage();
-
-					return Activator.getImage(Activator.PLUGIN_ID, "/icons/eobj16/module.png", true);
-				}
-
-				if (element != null)
-					return Activator.getImage(Activator.PLUGIN_ID, "/icons/eobj16/debug_java_class.png", true);
-
-				return super.getImage(element);
-			}
-		});
-
-		fModulesTable.setInput(fEngine);
-
-		fModulesTable.addDragSupport(DND.DROP_MOVE | DND.DROP_COPY, new Transfer[] { LocalSelectionTransfer.getTransfer(), TextTransfer.getInstance() },
-				new ModulesDragListener(fModulesTable) {
-					@Override
-					public void dragStart(DragSourceEvent event) {
-						super.dragStart(event);
-
-						final Object firstElement = getSelection().getFirstElement();
-						event.doit = (firstElement instanceof ModuleDefinition);
-					}
-				});
-
-		return composite;
-	}
-
-	/**
-	 * Get a module definition for a given module instance.
-	 *
-	 * @param element
-	 *            module instance
-	 * @return module definition or <code>null</code>
-	 */
-	private ModuleDefinition getDefinition(Object element) {
-		final IScriptService scriptService = PlatformUI.getWorkbench().getService(IScriptService.class);
-		final List<ModuleDefinition> modules = new ArrayList<>(scriptService.getAvailableModules());
-
-		for (final ModuleDefinition definition : modules) {
-			if (definition.getModuleClass().equals(element.getClass())) {
-				return definition;
-			}
-		}
-
-		return null;
-	}
-
-	@Override
-	public String getTitle() {
-		return Messages.ModuleStackDropin_moduleStack;
-	}
-
-	@Override
-	public void notify(IScriptEngine engine, Script script, int status) {
-		switch (status) {
-		case IExecutionListener.SCRIPT_END:
-			// fall through
-		case IExecutionListener.SCRIPT_INJECTION_END:
-			Display.getDefault().asyncExec(() -> fModulesTable.refresh());
-			break;
-
-		case IExecutionListener.ENGINE_END:
-			engine.removeExecutionListener(this);
-			break;
-		}
-	}
-}
diff --git a/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/AbstractDropin.java b/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/AbstractDropin.java
new file mode 100644
index 0000000..0c53039
--- /dev/null
+++ b/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/AbstractDropin.java
@@ -0,0 +1,87 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Christian Pontesegger and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.ui.views.shell.dropins;
+
+import java.time.Duration;
+
+import org.eclipse.ease.IExecutionListener;
+import org.eclipse.ease.IReplEngine;
+import org.eclipse.ease.IScriptEngine;
+import org.eclipse.ease.Script;
+import org.eclipse.jface.util.Throttler;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IWorkbenchPartSite;
+
+public abstract class AbstractDropin implements IShellDropin, IExecutionListener {
+
+	private IReplEngine fEngine;
+
+	private final Throttler fUiUpdater = new Throttler(Display.getDefault(), Duration.ofMillis(500), this::update);
+
+	private boolean fIsActive = false;
+
+	@Override
+	public void setScriptEngine(IReplEngine engine) {
+		if (fEngine != null)
+			fEngine.removeExecutionListener(this);
+
+		fEngine = engine;
+
+		if (fEngine != null)
+			fEngine.addExecutionListener(this);
+	}
+
+	@Override
+	public Composite createPartControl(final IWorkbenchPartSite site, final Composite parent) {
+		final Composite composite = createComposite(site, parent);
+
+		fIsActive = true;
+
+		composite.addListener(SWT.Hide, event -> fIsActive = false);
+		composite.addListener(SWT.Show, event -> {
+			fIsActive = true;
+			update();
+		});
+
+		return composite;
+	}
+
+	@Override
+	public void notify(IScriptEngine engine, Script script, int status) {
+		switch (status) {
+		case IExecutionListener.SCRIPT_END:
+			fUiUpdater.throttledExec();
+			break;
+
+		case IExecutionListener.ENGINE_END:
+			engine.removeExecutionListener(this);
+			break;
+
+		default:
+			// nothing to do
+			break;
+		}
+	}
+
+	protected void update() {
+		if (fIsActive)
+			updateUI();
+	}
+
+	protected abstract void updateUI();
+
+	protected abstract Composite createComposite(IWorkbenchPartSite site, Composite parent);
+}
diff --git a/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackContentProvider.java b/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackContentProvider.java
new file mode 100644
index 0000000..4074d4d
--- /dev/null
+++ b/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackContentProvider.java
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Christian Pontesegger and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.ui.views.shell.dropins.modules;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.ease.IScriptEngine;
+import org.eclipse.ease.modules.IEnvironment;
+import org.eclipse.ease.modules.ModuleDefinition;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+
+public class ModulesStackContentProvider implements IStructuredContentProvider {
+
+	@Override
+	public Object[] getElements(Object inputElement) {
+		if (inputElement instanceof IScriptEngine) {
+			final IEnvironment environment = IEnvironment.getEnvironment((IScriptEngine) inputElement);
+			if (environment != null) {
+				final List<Object> loadedModules = new ArrayList<>();
+
+				for (final Object instance : new ArrayList<>(environment.getModules())) {
+					final ModuleDefinition definition = ModuleDefinition.forInstance(instance);
+					loadedModules.add((definition == null) ? instance : definition);
+				}
+
+				return loadedModules.toArray();
+			}
+		}
+
+		return new Object[0];
+	}
+}
diff --git a/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackDropin.java b/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackDropin.java
new file mode 100644
index 0000000..3f5bac6
--- /dev/null
+++ b/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackDropin.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Christian Pontesegger and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.ui.views.shell.dropins.modules;
+
+import org.eclipse.ease.IReplEngine;
+import org.eclipse.ease.modules.ModuleDefinition;
+import org.eclipse.ease.ui.Messages;
+import org.eclipse.ease.ui.modules.ui.ModulesDragListener;
+import org.eclipse.ease.ui.views.shell.dropins.AbstractDropin;
+import org.eclipse.jface.layout.TableColumnLayout;
+import org.eclipse.jface.util.LocalSelectionTransfer;
+import org.eclipse.jface.viewers.ColumnWeightData;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.ui.IWorkbenchPartSite;
+
+public class ModulesStackDropin extends AbstractDropin {
+
+	private TableViewer fModulesTable = null;
+
+	@Override
+	public void setScriptEngine(IReplEngine engine) {
+		super.setScriptEngine(engine);
+
+		if (fModulesTable != null) {
+			fModulesTable.setInput(engine);
+
+			update();
+		}
+	}
+
+	@Override
+	public Composite createComposite(IWorkbenchPartSite site, Composite parent) {
+		final Composite composite = new Composite(parent, SWT.NONE);
+		final TableColumnLayout tableColumnLayout = new TableColumnLayout();
+		composite.setLayout(tableColumnLayout);
+
+		fModulesTable = new TableViewer(composite, SWT.BORDER);
+		final Table table = fModulesTable.getTable();
+		table.setHeaderVisible(true);
+		table.setLinesVisible(true);
+
+		fModulesTable.setContentProvider(new ModulesStackContentProvider());
+
+		final TableViewerColumn tableViewerColumn = new TableViewerColumn(fModulesTable, SWT.NONE);
+		final TableColumn column = tableViewerColumn.getColumn();
+		tableColumnLayout.setColumnData(column, new ColumnWeightData(1));
+		column.setText(Messages.ModuleStackDropin_module);
+		tableViewerColumn.setLabelProvider(new ModulesStackLabelProvider());
+
+		fModulesTable.addDragSupport(DND.DROP_MOVE | DND.DROP_COPY, new Transfer[] { LocalSelectionTransfer.getTransfer(), TextTransfer.getInstance() },
+				new ModulesDragListener(fModulesTable) {
+					@Override
+					public void dragStart(DragSourceEvent event) {
+						super.dragStart(event);
+
+						final Object firstElement = getSelection().getFirstElement();
+						event.doit = (firstElement instanceof ModuleDefinition);
+					}
+				});
+
+		return composite;
+	}
+
+	@Override
+	public String getTitle() {
+		return Messages.ModuleStackDropin_moduleStack;
+	}
+
+	@Override
+	protected void updateUI() {
+		fModulesTable.refresh();
+	}
+}
diff --git a/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackLabelProvider.java b/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackLabelProvider.java
new file mode 100644
index 0000000..7f43282
--- /dev/null
+++ b/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackLabelProvider.java
@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Christian Pontesegger and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.ui.views.shell.dropins.modules;
+
+import org.eclipse.ease.modules.ModuleDefinition;
+import org.eclipse.ease.ui.Activator;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+public class ModulesStackLabelProvider extends ColumnLabelProvider {
+
+	@Override
+	public String getText(final Object element) {
+		if (element instanceof ModuleDefinition)
+			return ((ModuleDefinition) element).getName();
+
+		if (element != null)
+			return element.getClass().getCanonicalName();
+
+		return "";
+	}
+
+	@Override
+	public Image getImage(final Object element) {
+		if (element instanceof ModuleDefinition) {
+			final ImageDescriptor icon = ((ModuleDefinition) element).getImageDescriptor();
+			if (icon != null)
+				return icon.createImage();
+
+			return Activator.getImage(Activator.PLUGIN_ID, "/icons/eobj16/module.png", true);
+		}
+
+		if (element != null)
+			return Activator.getImage(Activator.PLUGIN_ID, "/icons/eobj16/debug_java_class.png", true);
+
+		return null;
+	}
+}
diff --git a/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/variables/VariablesDropin.java b/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/variables/VariablesDropin.java
index 064c69d..97f1905 100644
--- a/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/variables/VariablesDropin.java
+++ b/plugins/org.eclipse.ease.ui/src/org/eclipse/ease/ui/views/shell/dropins/variables/VariablesDropin.java
@@ -12,18 +12,12 @@
  *******************************************************************************/
 package org.eclipse.ease.ui.views.shell.dropins.variables;
 
-import java.time.Duration;
-
-import org.eclipse.ease.IExecutionListener;
 import org.eclipse.ease.IReplEngine;
-import org.eclipse.ease.IScriptEngine;
-import org.eclipse.ease.Script;
 import org.eclipse.ease.ui.Messages;
 import org.eclipse.ease.ui.view.VariablesDragListener;
-import org.eclipse.ease.ui.views.shell.dropins.IShellDropin;
+import org.eclipse.ease.ui.views.shell.dropins.AbstractDropin;
 import org.eclipse.jface.layout.TreeColumnLayout;
 import org.eclipse.jface.util.LocalSelectionTransfer;
-import org.eclipse.jface.util.Throttler;
 import org.eclipse.jface.viewers.ColumnWeightData;
 import org.eclipse.jface.viewers.TreeViewer;
 import org.eclipse.jface.viewers.TreeViewerColumn;
@@ -33,38 +27,28 @@
 import org.eclipse.swt.dnd.TextTransfer;
 import org.eclipse.swt.dnd.Transfer;
 import org.eclipse.swt.widgets.Composite;
-import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.Tree;
 import org.eclipse.swt.widgets.TreeColumn;
 import org.eclipse.ui.IWorkbenchPartSite;
 
-public class VariablesDropin implements IShellDropin, IExecutionListener {
+public class VariablesDropin extends AbstractDropin {
 
 	private TreeViewer fVariablesTree = null;
-	private IReplEngine fEngine;
-	private boolean fIsActive = true;
-
-	private final Throttler fUiUpdater = new Throttler(Display.getDefault(), Duration.ofMillis(500), this::update);
 
 	@Override
 	public void setScriptEngine(IReplEngine engine) {
-		if (fEngine != null)
-			fEngine.removeExecutionListener(this);
-
-		fEngine = engine;
-
-		if (fEngine != null)
-			fEngine.addExecutionListener(this);
+		super.setScriptEngine(engine);
 
 		// set tree input
 		if (fVariablesTree != null) {
 			fVariablesTree.setInput(engine);
-			fUiUpdater.throttledExec();
+
+			update();
 		}
 	}
 
 	@Override
-	public Composite createPartControl(final IWorkbenchPartSite site, final Composite parent) {
+	public Composite createComposite(final IWorkbenchPartSite site, final Composite parent) {
 
 		final Composite composite = new Composite(parent, SWT.NONE);
 		final TreeColumnLayout treeColumnLayout = new TreeColumnLayout();
@@ -94,12 +78,6 @@
 		fVariablesTree.addDragSupport(DND.DROP_MOVE | DND.DROP_COPY, new Transfer[] { LocalSelectionTransfer.getTransfer(), TextTransfer.getInstance() },
 				new VariablesDragListener(fVariablesTree));
 
-		composite.addListener(SWT.Hide, event -> fIsActive = false);
-		composite.addListener(SWT.Show, event -> {
-			fIsActive = true;
-			update();
-		});
-
 		return composite;
 	}
 
@@ -109,24 +87,7 @@
 	}
 
 	@Override
-	public void notify(IScriptEngine engine, Script script, int status) {
-		switch (status) {
-		case IExecutionListener.SCRIPT_END:
-			fUiUpdater.throttledExec();
-			break;
-
-		case IExecutionListener.ENGINE_END:
-			engine.removeExecutionListener(this);
-			break;
-
-		default:
-			// nothing to do
-			break;
-		}
-	}
-
-	public void update() {
-		if (fIsActive)
-			fVariablesTree.refresh();
+	public void updateUI() {
+		fVariablesTree.refresh();
 	};
 }
diff --git a/plugins/org.eclipse.ease/src/org/eclipse/ease/modules/ModuleDefinition.java b/plugins/org.eclipse.ease/src/org/eclipse/ease/modules/ModuleDefinition.java
index f9f7dd0..26eb337 100644
--- a/plugins/org.eclipse.ease/src/org/eclipse/ease/modules/ModuleDefinition.java
+++ b/plugins/org.eclipse.ease/src/org/eclipse/ease/modules/ModuleDefinition.java
@@ -5,15 +5,19 @@
  * which accompanies this distribution, and is available at
  * https://www.eclipse.org/legal/epl-2.0/
  *
+ * SPDX-License-Identifier: EPL-2.0
+ *
  * Contributors:
  *     Christian Pontesegger - initial API and implementation
  *******************************************************************************/
+
 package org.eclipse.ease.modules;
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IConfigurationElement;
@@ -34,6 +38,12 @@
 
 public class ModuleDefinition {
 
+	public static ModuleDefinition forInstance(Object element) {
+		final List<ModuleDefinition> modules = new ArrayList<>(ScriptService.getInstance().getAvailableModules());
+
+		return modules.stream().filter(d -> Objects.equals(d.getModuleClass(), element.getClass())).findAny().orElse(null);
+	}
+
 	public static class ModuleDependency {
 
 		/** Module dependency parameter name. */
diff --git a/tests/org.eclipse.ease.test/src/org/eclipse/ease/modules/ModuleDefinitionTest.java b/tests/org.eclipse.ease.test/src/org/eclipse/ease/modules/ModuleDefinitionTest.java
new file mode 100644
index 0000000..ab1b217
--- /dev/null
+++ b/tests/org.eclipse.ease.test/src/org/eclipse/ease/modules/ModuleDefinitionTest.java
@@ -0,0 +1,39 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Christian Pontesegger and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.modules;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class ModuleDefinitionTest {
+
+	@Test
+	@DisplayName("forInstance(module) returns definition")
+	public void forInstance_returns_definition() {
+		final ModuleDefinition definition = ModuleDefinition.forInstance(new EnvironmentModule());
+
+		assertNotNull(definition);
+		assertEquals("Environment", definition.getName());
+	}
+
+	@Test
+	@DisplayName("forInstance(unknown) returns null")
+	public void forInstance_returns_null() {
+		assertNull(ModuleDefinition.forInstance("foo"));
+	}
+}
diff --git a/tests/org.eclipse.ease.ui.test/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackContentProviderTest.java b/tests/org.eclipse.ease.ui.test/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackContentProviderTest.java
new file mode 100644
index 0000000..0ccb177
--- /dev/null
+++ b/tests/org.eclipse.ease.ui.test/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackContentProviderTest.java
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Christian Pontesegger and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.ui.views.shell.dropins.modules;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.ease.IScriptEngine;
+import org.eclipse.ease.modules.EnvironmentModule;
+import org.eclipse.ease.modules.IEnvironment;
+import org.eclipse.ease.modules.ModuleDefinition;
+import org.eclipse.ease.ui.views.shell.dropins.variables.VariablesContentProvider;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class ModulesStackContentProviderTest {
+
+	@Test
+	@DisplayName("getElements() returns found modules")
+	public void getElements_returns_modules() {
+
+		final List<Object> mockedModules = Arrays.asList(new EnvironmentModule());
+
+		final IEnvironment environment = mock(IEnvironment.class);
+		when(environment.getModules()).thenReturn(mockedModules);
+
+		final Map<String, Object> variables = new HashMap<>();
+		variables.put("env", environment);
+
+		final IScriptEngine engine = mock(IScriptEngine.class);
+		when(engine.getVariables()).thenReturn(variables);
+
+		final IStructuredContentProvider contentProvider = new ModulesStackContentProvider();
+
+		assertTrue(contentProvider.getElements(engine)[0] instanceof ModuleDefinition);
+		assertEquals("Environment", ((ModuleDefinition) contentProvider.getElements(engine)[0]).getName());
+	}
+
+	@Test
+	@DisplayName("getElements() returns empty list when no environment exists")
+	public void getElements_returns_empty_list_without_environment() {
+
+		final IScriptEngine engine = mock(IScriptEngine.class);
+		when(engine.getVariables()).thenReturn(new HashMap<>());
+
+		final ITreeContentProvider contentProvider = new VariablesContentProvider();
+		assertEquals(0, contentProvider.getElements(engine).length);
+	}
+}
diff --git a/tests/org.eclipse.ease.ui.test/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackLabelProviderTest.java b/tests/org.eclipse.ease.ui.test/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackLabelProviderTest.java
new file mode 100644
index 0000000..96ef8cd
--- /dev/null
+++ b/tests/org.eclipse.ease.ui.test/src/org/eclipse/ease/ui/views/shell/dropins/modules/ModulesStackLabelProviderTest.java
@@ -0,0 +1,47 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Christian Pontesegger and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christian Pontesegger - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.ease.ui.views.shell.dropins.modules;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.eclipse.ease.modules.ModuleDefinition;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class ModulesStackLabelProviderTest {
+
+	@Test
+	@DisplayName("getText(definition) returns definition name")
+	public void getText_returns_definition_name() {
+		final ModuleDefinition definition = mock(ModuleDefinition.class);
+		when(definition.getName()).thenReturn("myModule");
+
+		assertEquals("myModule", new ModulesStackLabelProvider().getText(definition));
+	}
+
+	@Test
+	@DisplayName("getText(unknownClass) returns full class name name")
+	public void getText_returns_full_class_name_name() {
+
+		assertEquals("java.lang.String", new ModulesStackLabelProvider().getText("dummy"));
+	}
+
+	@Test
+	@DisplayName("getText(null) returns ''")
+	public void getText_null_returns_empty_string() {
+		assertEquals("", new ModulesStackLabelProvider().getText(null));
+	}
+}