Bug 546352 - [9] New comprehensive UI for Modularity Details

Change-Id: I43d7af9018db152b250e44b7f4428921ad93f563
Signed-off-by: Stephan Herrmann <stephan.herrmann@berlin.de>
diff --git a/org.eclipse.jdt.ui/icons/full/ovr16/auto_mod_ovr.png b/org.eclipse.jdt.ui/icons/full/ovr16/auto_mod_ovr.png
new file mode 100644
index 0000000..2c74726
--- /dev/null
+++ b/org.eclipse.jdt.ui/icons/full/ovr16/auto_mod_ovr.png
Binary files differ
diff --git a/org.eclipse.jdt.ui/icons/full/ovr16/exports_pkg_ovr.png b/org.eclipse.jdt.ui/icons/full/ovr16/exports_pkg_ovr.png
new file mode 100644
index 0000000..f850dc7
--- /dev/null
+++ b/org.eclipse.jdt.ui/icons/full/ovr16/exports_pkg_ovr.png
Binary files differ
diff --git a/org.eclipse.jdt.ui/icons/full/ovr16/opens_pkg_ovr.png b/org.eclipse.jdt.ui/icons/full/ovr16/opens_pkg_ovr.png
new file mode 100644
index 0000000..9b0bbf1
--- /dev/null
+++ b/org.eclipse.jdt.ui/icons/full/ovr16/opens_pkg_ovr.png
Binary files differ
diff --git a/org.eclipse.jdt.ui/icons/full/ovr16/reads_mod_ovr.png b/org.eclipse.jdt.ui/icons/full/ovr16/reads_mod_ovr.png
new file mode 100644
index 0000000..a28b4d7
--- /dev/null
+++ b/org.eclipse.jdt.ui/icons/full/ovr16/reads_mod_ovr.png
Binary files differ
diff --git a/org.eclipse.jdt.ui/icons/full/ovr16/system_mod_ovr.png b/org.eclipse.jdt.ui/icons/full/ovr16/system_mod_ovr.png
new file mode 100644
index 0000000..3ea2f5c
--- /dev/null
+++ b/org.eclipse.jdt.ui/icons/full/ovr16/system_mod_ovr.png
Binary files differ
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/JavaPluginImages.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/JavaPluginImages.java
index 7863cbd..da1233f 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/JavaPluginImages.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/JavaPluginImages.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2018 IBM Corporation and others.
+ * Copyright (c) 2000, 2019 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -444,6 +444,11 @@
 	public static final ImageDescriptor DESC_OVR_CLASS= createUnManagedCached(T_OVR, "class_tsk.png"); //$NON-NLS-1$
 	public static final ImageDescriptor DESC_OVR_ABSTRACT_CLASS= createUnManagedCached(T_OVR, "class_abs_tsk.png"); //$NON-NLS-1$
 	public static final ImageDescriptor DESC_OVR_LIBRARY= createUnManagedCached(T_OVR, "library_ovr.png"); //$NON-NLS-1$
+	public static final ImageDescriptor DESC_OVR_SYSTEM_MOD= createUnManagedCached(T_OVR, "system_mod_ovr.png"); //$NON-NLS-1$
+	public static final ImageDescriptor DESC_OVR_AUTO_MOD= createUnManagedCached(T_OVR, "auto_mod_ovr.png"); //$NON-NLS-1$
+	public static final ImageDescriptor DESC_OVR_EXPORTS= createUnManagedCached(T_OVR, "exports_pkg_ovr.png"); //$NON-NLS-1$
+	public static final ImageDescriptor DESC_OVR_OPENS= createUnManagedCached(T_OVR, "opens_pkg_ovr.png"); //$NON-NLS-1$
+	public static final ImageDescriptor DESC_OVR_READS= createUnManagedCached(T_OVR, "reads_mod_ovr.png"); //$NON-NLS-1$
 	
     // Call Hierarchy
     public static final ImageDescriptor DESC_OVR_RECURSIVE= createUnManaged(T_OVR, "recursive_co.png");              //$NON-NLS-1$
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/NewWizardMessages.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/NewWizardMessages.java
index 0ca534d..f99c870 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/NewWizardMessages.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/NewWizardMessages.java
@@ -297,6 +297,7 @@
 	public static String BuildPathsBlock_tab_projects;
 	public static String BuildPathsBlock_tab_libraries;
 	public static String BuildPathsBlock_tab_order;
+	public static String BuildPathsBlock_tab_modules;
 	public static String BuildPathsBlock_classpath_label;
 	public static String BuildPathsBlock_classpath_up_button;
 	public static String BuildPathsBlock_classpath_down_button;
@@ -513,7 +514,33 @@
 	public static String AddReadsBlock_sourceModule_label;
 	public static String AddReadsBlock_targetModule_label;
 	//
-	
+	public static String ModuleDependenciesPage_modules_label;
+	public static String ModuleDependenciesPage_addSystemModule_button;
+	public static String ModuleDependenciesPage_details_label;
+	// buttons:
+	public static String ModuleDependenciesPage_modules_remove_button;
+	public static String ModuleDependenciesPage_modules_read_button;
+	public static String ModuleDependenciesPage_modules_expose_package_button;
+	public static String ModuleDependenciesPage_modules_patch_button;
+	public static String ModuleDependenciesPage_modules_edit_button;
+	// dialogs:
+	public static String ModuleSelectionDialog_add_button;
+	public static String ModuleSelectionDialog_addSystemModules_message;
+	public static String ModuleSelectionDialog_addSystemModules_title;
+
+	public static String ModuleDependenciesPage_removeModule_dialog_title;
+	public static String ModuleDependenciesPage_removingModule_message;
+	public static String ModuleDependenciesPage_removingModuleTransitive_message;
+	public static String ModuleDependenciesPage_remove_button;
+	public static String ModuleDependenciesPage_cancel_button;
+	public static String ModuleDependenciesPage_removeCurrentModule_error;
+	public static String ModuleDependenciesPage_removeModule_error_with_hint;
+	public static String ModuleDependenciesPage_removeSystemModule_error_hint;
+	// detail tree:
+	public static String ModuleDependenciesAdapter_configured_node;
+	public static String ModuleDependenciesAdapter_declared_node;
+	//
+
 	public static String EditVariableEntryDialog_title;
 	public static String EditVariableEntryDialog_filename_varlabel;
 	public static String EditVariableEntryDialog_filename_variable_button;
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/NewWizardMessages.properties b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/NewWizardMessages.properties
index 5b7d3e8..34aac17 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/NewWizardMessages.properties
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/NewWizardMessages.properties
@@ -277,6 +277,7 @@
 BuildPathsBlock_tab_projects=&Projects
 BuildPathsBlock_tab_libraries=&Libraries
 BuildPathsBlock_tab_order=&Order and Export
+BuildPathsBlock_tab_modules=&Module Dependencies
 
 BuildPathsBlock_classpath_label=Build class path order and e&xported entries:\n(Exported entries are contributed to dependent projects)
 
@@ -550,6 +551,32 @@
 AddReadsBlock_sourceModule_label=Source module
 AddReadsBlock_targetModule_label=Target module
 
+ModuleDependenciesPage_modules_label=All Modules
+ModuleDependenciesPage_addSystemModule_button=Add System Module ...
+ModuleDependenciesPage_details_label=Details
+# buttons
+ModuleDependenciesPage_modules_remove_button=&Remove
+ModuleDependenciesPage_modules_read_button=Rea&d Module
+ModuleDependenciesPage_modules_expose_package_button=E&xpose Package
+ModuleDependenciesPage_modules_patch_button=&Patch from ...
+ModuleDependenciesPage_modules_edit_button=&Edit
+# dialogs:
+ModuleSelectionDialog_add_button=&Add
+ModuleSelectionDialog_addSystemModules_message=Please select modules to be added to the module path.\nTransitively required modules will be automatically selected.
+ModuleSelectionDialog_addSystemModules_title=Select system modules to add
+
+ModuleDependenciesPage_removeModule_dialog_title=Remove Module
+ModuleDependenciesPage_removingModule_message=Removing module {0}
+ModuleDependenciesPage_removingModuleTransitive_message=Removing module {0} and modules depending on it:\n
+ModuleDependenciesPage_remove_button=Remove
+ModuleDependenciesPage_cancel_button=Cancel
+ModuleDependenciesPage_removeCurrentModule_error=Cannot remove the current module
+ModuleDependenciesPage_removeModule_error_with_hint=Cannot remove module {0}{1}
+ModuleDependenciesPage_removeSystemModule_error_hint=\nOnly system modules can be removed here.\n\
+To remove other modules please remove them from the ModulePath (tab Projects or Libraries).
+# detail tree:
+ModuleDependenciesAdapter_configured_node=Configured
+ModuleDependenciesAdapter_declared_node=Declared
 # ------- EditVariableEntryDialog -------
 
 EditVariableEntryDialog_title=Edit Variable Entry
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/BuildPathsBlock.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/BuildPathsBlock.java
index 84f6ebf..79fd90d 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/BuildPathsBlock.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/BuildPathsBlock.java
@@ -143,6 +143,7 @@
 	private BuildPathBasePage fSourceContainerPage;
 	private ProjectsWorkbookPage fProjectsPage;
 	private LibrariesWorkbookPage fLibrariesPage;
+	private ModuleDependenciesPage fModulesPage;
 
 	private BuildPathBasePage fCurrPage;
 
@@ -271,10 +272,18 @@
 		item.setData(ordpage);
 		item.setControl(ordpage.getControl(folder));
 
+		fModulesPage= new ModuleDependenciesPage(fClassPathList, fPageContainer);
+		item= new TabItem(folder, SWT.NONE);
+		item.setText(NewWizardMessages.BuildPathsBlock_tab_modules);
+		item.setImage(JavaPluginImages.get(JavaPluginImages.IMG_OBJS_MODULE));
+		item.setData(fModulesPage);
+		item.setControl(fModulesPage.getControl(folder));
+
 		if (fCurrJProject != null) {
 			fSourceContainerPage.init(fCurrJProject);
 			fLibrariesPage.init(fCurrJProject);
 			fProjectsPage.init(fCurrJProject);
+			fModulesPage.init(fCurrJProject);
 			fIs9OrHigher= JavaModelUtil.is9OrHigher(fCurrJProject);
 		}
 
@@ -357,6 +366,7 @@
 			fSourceContainerPage.init(fCurrJProject);
 			fProjectsPage.init(fCurrJProject);
 			fLibrariesPage.init(fCurrJProject);
+			fModulesPage.init(fCurrJProject);
 			fIs9OrHigher= JavaModelUtil.is9OrHigher(fCurrJProject);
 		}
 
@@ -392,6 +402,7 @@
 			// update the library and project page if fis9OrHigher changed
 			fLibrariesPage.init(fCurrJProject);
 			fProjectsPage.init(fCurrJProject);
+			fModulesPage.init(fCurrJProject);
 			fIs9OrHigher= is9OrHigherAfter;
 		}
 		doStatusLineUpdate();
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/CPListElement.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/CPListElement.java
index ee7321c..865a6c3 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/CPListElement.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/CPListElement.java
@@ -47,6 +47,8 @@
 import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
 import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.LimitModules;
 import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddExport;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddExpose;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddOpens;
 import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddReads;
 import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModulePatch;
 
@@ -251,6 +253,10 @@
 					if (!encodedExports.isEmpty()) {
 						res.add(JavaCore.newClasspathAttribute(IClasspathAttribute.ADD_EXPORTS, encodedExports));
 					}
+					String encodedOpens= ModuleEncapsulationDetail.encodeFiltered(detailValue, ModuleAddOpens.class);
+					if (!encodedOpens.isEmpty()) {
+						res.add(JavaCore.newClasspathAttribute(IClasspathAttribute.ADD_OPENS, encodedOpens));
+					}
 					String encodedReads= ModuleEncapsulationDetail.encodeFiltered(detailValue, ModuleAddReads.class);
 					if (!encodedReads.isEmpty()) {
 						res.add(JavaCore.newClasspathAttribute(IClasspathAttribute.ADD_READS, encodedReads));
@@ -784,7 +790,9 @@
 				if (IClasspathAttribute.PATCH_MODULE.equals(otherAttrib.getName())) {
 					details.add(ModulePatch.fromString(attribElem, otherAttrib.getValue()));
 				} else if (IClasspathAttribute.ADD_EXPORTS.equals(otherAttrib.getName())) {
-					details.addAll(ModuleAddExport.fromMultiString(attribElem, otherAttrib.getValue()));
+					details.addAll(ModuleAddExpose.fromMultiString(attribElem, otherAttrib.getValue(), true));
+				} else if (IClasspathAttribute.ADD_OPENS.equals(otherAttrib.getName())) {
+					details.addAll(ModuleAddExpose.fromMultiString(attribElem, otherAttrib.getValue(), false));
 				} else if (IClasspathAttribute.ADD_READS.equals(otherAttrib.getName())) {
 					details.addAll(ModuleAddReads.fromMultiString(attribElem, otherAttrib.getValue()));
 				} else if (IClasspathAttribute.LIMIT_MODULES.equals(otherAttrib.getName())) {
@@ -799,7 +807,7 @@
 
 	private static boolean isModuleAttribute(String attributeName) {
 		return Stream.of(IClasspathAttribute.MODULE, IClasspathAttribute.LIMIT_MODULES,
-					IClasspathAttribute.PATCH_MODULE, IClasspathAttribute.ADD_EXPORTS, IClasspathAttribute.ADD_READS)
+					IClasspathAttribute.PATCH_MODULE, IClasspathAttribute.ADD_EXPORTS, IClasspathAttribute.ADD_OPENS, IClasspathAttribute.ADD_READS)
 				.anyMatch(attributeName::equals);
 	}
 
@@ -880,6 +888,8 @@
 									buf.append(IClasspathAttribute.PATCH_MODULE+':'+detail.toString()).append(';');
 								if (detail instanceof ModuleAddExport)
 									buf.append(IClasspathAttribute.ADD_EXPORTS+':'+detail.toString()).append(';');
+								if (detail instanceof ModuleAddOpens)
+									buf.append(IClasspathAttribute.ADD_OPENS+':'+detail.toString()).append(';');
 								if (detail instanceof ModuleAddReads)
 									buf.append(IClasspathAttribute.ADD_READS+':'+detail.toString()).append(';');
 								if (detail instanceof LimitModules)
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/LibrariesWorkbookPage.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/LibrariesWorkbookPage.java
index 33f5278..97788d5 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/LibrariesWorkbookPage.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/LibrariesWorkbookPage.java
@@ -371,7 +371,7 @@
 
 	}
 
-	private boolean isJREContainer(IPath path) {
+	static boolean isJREContainer(IPath path) {
 		if (path == null)
 			return false;
 		String[] segments= path.segments();
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleAddExportsBlock.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleAddExportsBlock.java
index 112f7c8..fcc1a04 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleAddExportsBlock.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleAddExportsBlock.java
@@ -56,29 +56,37 @@
 import org.eclipse.jdt.internal.ui.wizards.IStatusChangeListener;
 import org.eclipse.jdt.internal.ui.wizards.NewWizardMessages;
 import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddExport;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddExpose;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddOpens;
 import org.eclipse.jdt.internal.ui.wizards.dialogfields.DialogField;
 import org.eclipse.jdt.internal.ui.wizards.dialogfields.IDialogFieldListener;
 import org.eclipse.jdt.internal.ui.wizards.dialogfields.LayoutUtil;
+import org.eclipse.jdt.internal.ui.wizards.dialogfields.SelectionButtonDialogFieldGroup;
 import org.eclipse.jdt.internal.ui.wizards.dialogfields.StringDialogField;
 
 
 /**
- * UI to define one additional exports (add-exports).
+ * UI to define one additional exports (add-exports) or opens (add-opens).
  */
 public class ModuleAddExportsBlock {
 
+	private static final String[] EXPORTS_OPENS_LABELS= new String[] {"exports", "opens"}; //$NON-NLS-1$ //$NON-NLS-2$
+	private static final int IDX_EXPORTS= 0;
+	private static final int IDX_OPENS= 1;
+
 	private final IStatusChangeListener fContext;
 
 	private StringDialogField fSourceModule;
 	private StringDialogField fPackage;
 	private StringDialogField fTargetModules;
+	private SelectionButtonDialogFieldGroup fExposeKindButtons;
 
 	private IStatus fSourceModuleStatus;
 	private IStatus fPackageStatus;
 
 	private Control fSWTWidget;
 
-	private final ModuleAddExport fInitialValue;
+	private final ModuleAddExpose fInitialValue;
 	
 	private IJavaElement[] fSourceJavaElements;
 
@@ -87,7 +95,7 @@
 	 * @param sourceJavaElements java element representing the source modules from where packages should be exported
 	 * @param initialValue The value to edit
 	 */
-	public ModuleAddExportsBlock(IStatusChangeListener context, IJavaElement[] sourceJavaElements, ModuleAddExport initialValue) {
+	public ModuleAddExportsBlock(IStatusChangeListener context, IJavaElement[] sourceJavaElements, ModuleAddExpose initialValue) {
 		fContext= context;
 		fInitialValue= initialValue;
 		fSourceJavaElements= sourceJavaElements;
@@ -109,6 +117,10 @@
 		fTargetModules= new StringDialogField();
 		fTargetModules.setDialogFieldListener(adapter);
 		fTargetModules.setLabelText(NewWizardMessages.AddExportsBlock_targetModules_label);
+		
+		fExposeKindButtons= new SelectionButtonDialogFieldGroup(SWT.RADIO, EXPORTS_OPENS_LABELS, 2);
+		fExposeKindButtons.setSelection(IDX_EXPORTS, initialValue instanceof ModuleAddExport);
+		fExposeKindButtons.setSelection(IDX_OPENS, initialValue instanceof ModuleAddOpens);
 
 		setDefaults();
 	}
@@ -195,13 +207,16 @@
 		return sourceModule+'/'+pack+'='+targetModules;
 	}
 
-	public ModuleAddExport getExport(CPListElementAttribute parentAttribute) {
+	public ModuleAddExpose getExport(CPListElementAttribute parentAttribute) {
 		String sourceModule= getSourceModuleText();
 		String pack= getPackageText();
 		String targetModules= getTargetModulesText();
 		if (sourceModule.isEmpty() || pack.isEmpty() || targetModules.isEmpty())
 			return null;
-		return new ModuleAddExport(sourceModule, pack, targetModules, parentAttribute);
+		if (fExposeKindButtons.isSelected(IDX_EXPORTS))
+			return new ModuleAddExport(sourceModule, pack, targetModules, parentAttribute);
+		else
+			return new ModuleAddOpens(sourceModule, pack, targetModules, parentAttribute);
 	}
 
 	/**
@@ -262,6 +277,8 @@
 		BidiUtils.applyBidiProcessing(targetModulesField, StructuredTextTypeHandlerFactory.JAVA);
 
 		DialogField.createEmptySpace(composite, 2);
+		
+		fExposeKindButtons.doFillIntoGrid(composite, 2);
 
 		Dialog.applyDialogFont(composite);
 
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleAddExportsDialog.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleAddExportsDialog.java
index 5f346bc..82db503 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleAddExportsDialog.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleAddExportsDialog.java
@@ -31,7 +31,7 @@
 import org.eclipse.jdt.internal.ui.JavaPlugin;
 import org.eclipse.jdt.internal.ui.wizards.IStatusChangeListener;
 import org.eclipse.jdt.internal.ui.wizards.NewWizardMessages;
-import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddExport;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddExpose;
 
 /**
  * A dialog to configure add-exports of a library.
@@ -49,7 +49,7 @@
 	 * @param sourceJavaElements java elements representing the source modules from where packages should be exported
 	 * @param value The value to edit.
 	 */
-	public ModuleAddExportsDialog(Shell parent, IJavaElement[] sourceJavaElements, ModuleAddExport value) {
+	public ModuleAddExportsDialog(Shell parent, IJavaElement[] sourceJavaElements, ModuleAddExpose value) {
 		super(parent);
 
 		IStatusChangeListener listener= new IStatusChangeListener() {
@@ -121,7 +121,7 @@
 	 *
 	 * @return the configured export value, or {@code null} if no export was configured.
 	 */
-	public ModuleAddExport getExport(CPListElementAttribute parentAttribute) {
+	public ModuleAddExpose getExport(CPListElementAttribute parentAttribute) {
 		return fAddExportsBlock.getExport(parentAttribute);
 	}
 }
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleDependenciesAdapter.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleDependenciesAdapter.java
new file mode 100644
index 0000000..67a7d04
--- /dev/null
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleDependenciesAdapter.java
@@ -0,0 +1,730 @@
+/*******************************************************************************
+ * Copyright (c) 2019 GK Software SE, and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Stephan Herrmann - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.internal.ui.wizards.buildpaths;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+
+import com.ibm.icu.text.MessageFormat;
+
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Shell;
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.Path;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.ResourcesPlugin;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.window.Window;
+
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IModuleDescription;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+
+import org.eclipse.jdt.internal.ui.JavaPlugin;
+import org.eclipse.jdt.internal.ui.JavaPluginImages;
+import org.eclipse.jdt.internal.ui.viewsupport.ImageDescriptorRegistry;
+import org.eclipse.jdt.internal.ui.viewsupport.JavaElementImageProvider;
+import org.eclipse.jdt.internal.ui.wizards.NewWizardMessages;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleDependenciesList.ModuleKind;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleDependenciesPage.DecoratedImageDescriptor;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddExport;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddExpose;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddOpens;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddReads;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModulePatch;
+import org.eclipse.jdt.internal.ui.wizards.dialogfields.DialogField;
+import org.eclipse.jdt.internal.ui.wizards.dialogfields.IDialogFieldListener;
+import org.eclipse.jdt.internal.ui.wizards.dialogfields.ITreeListAdapter;
+import org.eclipse.jdt.internal.ui.wizards.dialogfields.TreeListDialogField;
+
+/**
+ * Implementation of the right-hand pane of the {@link ModuleDependenciesPage}.
+ */
+class ModuleDependenciesAdapter implements IDialogFieldListener, ITreeListAdapter<Object> {
+
+	private static final int IDX_REMOVE= 0;
+
+	private static final int IDX_READ_MODULE= 2;
+	private static final int IDX_EXPOSE_PACKAGE= 3;
+
+	private static final int IDX_PATCH= 5;
+	
+	private static final int IDX_EDIT= 7;
+	
+	abstract static class Details {
+
+		/** the module selected in the LHS pane, for which details are being shown / edited in this RHS pane. */
+		protected final IModuleDescription fFocusModule;
+		/** the classpath element by which the current project refers to the focus module. */
+		protected final CPListElement fElem;
+
+		public Details(IModuleDescription focusModule, CPListElement elem) {
+			fFocusModule= focusModule;
+			fElem= elem;
+		}
+
+		/**
+		 * Answer the module of the current IJavaProject for which the build path is being configured. 
+		 * @return the module of the current project, or {@code null}.
+		 */
+		protected IModuleDescription getContextModule() {
+			try {
+				IModuleDescription moduleDescription= fElem.getJavaProject().getModuleDescription();
+				if (moduleDescription != null) {
+					return moduleDescription;
+				}
+			} catch (JavaModelException jme) {
+				JavaPlugin.log(jme.getStatus());
+			}
+			return null;
+		}
+		protected String getContextModuleName() {
+			try {
+				IModuleDescription moduleDescription= fElem.getJavaProject().getModuleDescription();
+				if (moduleDescription != null) {
+					return moduleDescription.getElementName();
+				}
+			} catch (JavaModelException jme) {
+				JavaPlugin.log(jme.getStatus());
+			}
+			return ""; //$NON-NLS-1$
+		}
+
+		protected IJavaProject getContextProject() {
+			return fElem.getJavaProject();
+		}
+	}
+
+	/** Synthetic tree node as a parent for details that are declared by the focus module (in its module-info) */
+	static class DeclaredDetails extends Details {
+
+		public DeclaredDetails(IModuleDescription mod, CPListElement elem) {
+			super(mod, elem);
+		}
+
+		/**
+		 * Answer all packages accessible to the context module (exports/opens) 
+		 * @return accessible packages represented by {@link AccessiblePackage}.
+		 */
+		public Object[] getPackages() {
+			try {
+				if (fFocusModule != null && !fFocusModule.isAutoModule()) {
+					IModuleDescription contextModule= getContextModule();
+					String[] exported= fFocusModule.getExportedPackageNames(contextModule);
+					List<AccessiblePackage> result= new ArrayList<>();
+					for (String export : exported) {
+						result.add(new AccessiblePackage(export, AccessiblePackage.Kind.Exports, this));
+					}
+					String[] opened= fFocusModule.getOpenedPackageNames(contextModule);
+					for (String open : opened) {
+						result.add(new AccessiblePackage(open, AccessiblePackage.Kind.Opens, this));
+					}
+					return result.toArray();
+				}
+			} catch (JavaModelException e) {
+				JavaPlugin.log(e);
+			}
+			return new Object[0];
+		}
+	}
+
+	/** Synthetic tree node. */
+	static class ConfiguredDetails extends Details {
+		private ModuleKind fKind;
+	
+		public ConfiguredDetails(IModuleDescription focusModule, CPListElement elem, ModuleKind moduleKind) {
+			super(focusModule, elem);
+			fKind= moduleKind;
+		}
+		
+		public Object[] getChildren() {
+			if (fKind == ModuleKind.System) {
+				// aggregate attribute is in the parent (corresponding to the JRE)
+				Object parent= fElem.getParentContainer();
+				if (parent instanceof CPListElement) {
+					Object attribute= ((CPListElement) parent).getAttribute(CPListElement.MODULE);
+					if (attribute instanceof ModuleEncapsulationDetail[]) {
+						return convertEncapsulationDetails((ModuleEncapsulationDetail[]) attribute, fFocusModule.getElementName());
+					}					
+				}
+			}
+			Object attribute= fElem.getAttribute(CPListElement.MODULE);
+			if (attribute instanceof ModuleEncapsulationDetail[]) {
+				return convertEncapsulationDetails((ModuleEncapsulationDetail[]) attribute, null);
+			}
+			return fElem.getChildren(true);
+		}
+
+		private DetailNode<?>[] convertEncapsulationDetails(ModuleEncapsulationDetail[] attribute, String filterModule) {
+			List<DetailNode<?>> filteredDetails= new ArrayList<>();
+			for (ModuleEncapsulationDetail detail : attribute) {
+				if (detail instanceof ModuleAddExpose) {
+					ModuleAddExpose moduleAddExpose= (ModuleAddExpose) detail;
+					if (filterModule == null || filterModule.equals(moduleAddExpose.fSourceModule)) {
+						AccessiblePackage.Kind kind= moduleAddExpose instanceof ModuleAddExport ? AccessiblePackage.Kind.Exports : AccessiblePackage.Kind.Opens;
+						filteredDetails.add(new AccessiblePackage(moduleAddExpose.fPackage, kind, this));
+					}
+				} else if (detail instanceof ModuleAddReads) {
+					ModuleAddReads moduleAddReads= (ModuleAddReads) detail;
+					if (filterModule == null || filterModule.equals(moduleAddReads.fSourceModule)) {
+						filteredDetails.add(new ReadModule(moduleAddReads.fTargetModule, this));
+					}								
+				} else if (detail instanceof ModulePatch) {
+					ModulePatch modulePatch= (ModulePatch) detail;
+					if (filterModule == null || filterModule.equals(modulePatch.fModule)) {
+						try {
+							if (modulePatch.fPaths != null) {
+								IPath path= new Path(modulePatch.fPaths);
+								IFolder folder= ResourcesPlugin.getWorkspace().getRoot().getFolder(path);
+								IJavaElement elem= JavaCore.create(folder.getProject()).getPackageFragmentRoot(folder);
+								if (elem instanceof IPackageFragmentRoot) {
+									filteredDetails.add(new PatchModule((IPackageFragmentRoot) elem, this));
+								}
+							} else {
+								for (IClasspathEntry entry : fElem.getJavaProject().getRawClasspath()) {
+									if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
+										for (IPackageFragmentRoot root : fElem.getJavaProject().findPackageFragmentRoots(entry)) {
+											if (root.getKind() == IPackageFragmentRoot.K_SOURCE)
+												filteredDetails.add(new PatchModule(root, this));
+										}
+									}
+								}
+							}
+						} catch (JavaModelException e) {
+							JavaPlugin.log(e);
+						}
+					}
+				}
+			}
+			return filteredDetails.toArray(new DetailNode<?>[filteredDetails.size()]);
+		}
+		public void removeAll() {
+			if (fKind == ModuleKind.System) {
+				// aggregate attribute is in the parent (corresponding to the JRE)
+				Object parent= fElem.getParentContainer();
+				if (parent instanceof CPListElement) {
+					CPListElement jreElement= (CPListElement) parent;
+					Object attribute= jreElement.getAttribute(CPListElement.MODULE);
+					if (attribute instanceof ModuleEncapsulationDetail[]) {
+						// need to filter so we remove only affected details:
+						ModuleEncapsulationDetail[] filtered= Arrays.stream((ModuleEncapsulationDetail[]) attribute)
+									.filter(d -> !d.affects(fFocusModule.getElementName()))
+									.toArray(ModuleEncapsulationDetail[]::new);
+						jreElement.setAttribute(CPListElement.MODULE, filtered);
+						return;
+					}
+				}
+			}
+			Object attribute= fElem.getAttribute(CPListElement.MODULE);
+			if (attribute instanceof ModuleEncapsulationDetail[]) {
+				fElem.setAttribute(CPListElement.MODULE, new ModuleEncapsulationDetail[0]);
+			}
+		}
+		public boolean addOrEditAccessiblePackage(AccessiblePackage selectedPackage, Shell shell) {
+			Object container= fElem.getParentContainer();
+			CPListElement element= (container instanceof CPListElement) ? (CPListElement) container : fElem;
+			CPListElementAttribute moduleAttribute= element.findAttributeElement(CPListElement.MODULE);
+
+			IPackageFragmentRoot packRoot= (IPackageFragmentRoot) fFocusModule.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
+			String packageName= selectedPackage != null ? selectedPackage.getName() : ""; //$NON-NLS-1$
+
+			ModuleAddExpose initial= (selectedPackage != null)
+					? selectedPackage.convertToCP(moduleAttribute)
+					: new ModuleAddExport(fFocusModule.getElementName(), packageName, getContextModuleName(), moduleAttribute);
+
+			ModuleAddExportsDialog dialog= new ModuleAddExportsDialog(shell, new IJavaElement[] { packRoot }, initial);
+			if (dialog.open() == Window.OK) {
+				ModuleAddExpose expose= dialog.getExport(moduleAttribute);
+				if (expose != null) {
+					Object attribute= moduleAttribute.getValue();
+					ModuleEncapsulationDetail[] arrayValue= null;
+					if (attribute instanceof ModuleEncapsulationDetail[]) {
+						arrayValue= (ModuleEncapsulationDetail[]) attribute;
+						if (selectedPackage != null) {
+							// editing: replace existing entry
+							for (int i= 0; i < arrayValue.length; i++) {
+								ModuleEncapsulationDetail detail= arrayValue[i];
+								if (detail.equals(initial)) {
+									arrayValue[i]= expose;
+									break;
+								}
+							}
+						} else {
+							arrayValue= Arrays.copyOf(arrayValue, arrayValue.length+1);
+							arrayValue[arrayValue.length-1]= expose;
+						}
+					} else {
+						arrayValue= new ModuleEncapsulationDetail[] { expose };
+					}
+					element.setAttribute(CPListElement.MODULE, arrayValue);
+					return true;
+				}
+			}
+			return false;
+		}
+
+		public void remove(List<Object> selectedElements) {
+			CPListElementAttribute moduleAttribute;
+			if (fKind == ModuleKind.System) {
+				moduleAttribute= ((CPListElement) fElem.getParentContainer()).findAttributeElement(CPListElement.MODULE);
+			} else {
+				moduleAttribute= fElem.findAttributeElement(CPListElement.MODULE);
+			}
+			if (moduleAttribute == null) {
+				// TODO report
+				return;
+			}
+			if (!(moduleAttribute.getValue() instanceof ModuleEncapsulationDetail[])) {
+				// TODO report
+				return;
+			}
+			List<ModuleEncapsulationDetail> details= new ArrayList<>(Arrays.asList((ModuleEncapsulationDetail[]) moduleAttribute.getValue()));
+			for (Object node : selectedElements) {
+				if (node instanceof DetailNode<?>) {
+					ModuleEncapsulationDetail med= ((DetailNode<?>) node).convertToCP(moduleAttribute);
+					if (med != null) {
+						details.remove(med);
+					}
+				} else if (node instanceof ConfiguredDetails) {
+					((ConfiguredDetails) node).removeAll();
+					return; // covers all details, changes in 'details' are irrelevant
+				}
+			}
+			moduleAttribute.setValue(details.toArray(new ModuleEncapsulationDetail[details.size()]));
+		}
+
+		public boolean addReads(Shell shell) {
+			Object container= fElem.getParentContainer();
+			CPListElement element= (container instanceof CPListElement) ? (CPListElement) container : fElem;
+			CPListElementAttribute moduleAttribute= element.findAttributeElement(CPListElement.MODULE);
+			if (moduleAttribute == null) {
+				return false; // TODO report
+			}
+
+			List<String> irrelevantModules;
+			try {
+				irrelevantModules= Arrays.asList(fFocusModule.getRequiredModuleNames());
+			} catch (JavaModelException e) {
+				JavaPlugin.log(e.getStatus());
+				irrelevantModules= Collections.emptyList();
+			}
+			irrelevantModules= new ArrayList<>(irrelevantModules);
+			irrelevantModules.add(fFocusModule.getElementName());
+
+			ModuleSelectionDialog dialog= new ModuleSelectionDialog(shell, fElem.getJavaProject(), null, irrelevantModules, HashSet::new);
+			if (dialog.open() != 0) {				
+				return false;
+			}
+
+			List<IModuleDescription> result= dialog.getResult();
+			editModularityDetails(element, moduleAttribute, result,
+					null,
+					mod -> new ModuleAddReads(fFocusModule.getElementName(), mod.getElementName(), moduleAttribute));
+			return true;
+		}
+
+		public boolean addPatch(Shell shell, Map<String, String> patchMap) {
+			Object container= fElem.getParentContainer();
+			CPListElement element= (container instanceof CPListElement) ? (CPListElement) container : fElem;
+			CPListElementAttribute moduleAttribute= element.findAttributeElement(CPListElement.MODULE);
+			if (moduleAttribute == null) {
+				return false; // TODO report
+			}
+			IJavaProject contextProject= getContextProject();
+			ModulePatchSourceSelectionDialog dialog= new ModulePatchSourceSelectionDialog(shell, fFocusModule, contextProject);
+			if (dialog.open() != 0) {
+				return false;
+			}
+			List<IPackageFragmentRoot> result= dialog.getResult();
+			for (IPackageFragmentRoot root : result) {
+				String rootPath= root.getPath().toString();
+				if (patchMap.containsKey(rootPath)) {
+					if (!MessageDialog.openQuestion(shell, "Patch module conflict", 
+							MessageFormat.format("The source folder {0} was declared to patch module {1}, change to replacing module {2} instead?",
+									root.getPath(), patchMap.get(rootPath), fFocusModule.getElementName()))) {
+						return false;
+					}
+				}
+			}
+			editModularityDetails(element, moduleAttribute, result,
+					(r,med) -> med instanceof ModulePatch && r.getPath().toString().equals(((ModulePatch)med).fPaths),
+					root -> new ModulePatch(fFocusModule.getElementName(), root.getPath().toString(), moduleAttribute));
+			return true;
+		}			
+
+		private <T> void editModularityDetails(CPListElement element, CPListElementAttribute moduleAttribute, List<T> result,
+				BiPredicate<T,ModuleEncapsulationDetail> shouldReplace, Function<T,ModuleEncapsulationDetail> factory) {
+			ModuleEncapsulationDetail[] arrayValue= null;
+			int idx= 0;
+			Object attribute= moduleAttribute.getValue();
+			if (attribute instanceof ModuleEncapsulationDetail[]) {
+				List<T> remaining= new ArrayList<>();
+				arrayValue= (ModuleEncapsulationDetail[]) attribute;
+				if (shouldReplace != null) {
+					allResults:
+					for (T newValue : result) { 
+						for (int i= 0; i < arrayValue.length; i++) {
+							if (shouldReplace.test(newValue, arrayValue[i])) {
+								arrayValue[i]= factory.apply(result.get(0));
+								continue allResults;
+							}
+						}
+						remaining.add(newValue);
+					}
+					if (remaining.isEmpty())
+						return;
+					result= remaining;
+				}
+				idx= arrayValue.length;
+				arrayValue= Arrays.copyOf(arrayValue, arrayValue.length+result.size());
+			} else {
+				arrayValue= new ModuleEncapsulationDetail[result.size()];
+			}
+			for (T detail : result) {
+				arrayValue[idx++]= factory.apply(detail);
+			}
+			element.setAttribute(CPListElement.MODULE, arrayValue);
+		}
+	}
+
+	abstract static class DetailNode<D extends ModuleEncapsulationDetail> {
+		protected String fName;
+		protected Details fParent;
+
+		protected DetailNode(Details parent) {
+			fParent= parent;
+		}
+		public String getName() {
+			return fName;
+		}
+		public boolean isIsConfigured() {
+			return fParent instanceof ConfiguredDetails;
+		}
+		public abstract D convertToCP(CPListElementAttribute attribElem);
+	}
+
+	/** Declare that the package denoted by {@link #fName} is accessible (exported/opened) to the current context module. */
+	static class AccessiblePackage extends DetailNode<ModuleAddExpose> {
+		enum Kind { Exports, Opens;
+			ImageDescriptor getDecoration() {
+				switch (this) {
+					case Exports: return JavaPluginImages.DESC_OVR_EXPORTS;
+					case Opens: return JavaPluginImages.DESC_OVR_OPENS;
+					default: return null;
+				}
+			}
+		}
+		private Kind fKind;
+		public AccessiblePackage(String name, Kind kind, Details parent) {
+			super(parent);
+			fName= name;
+			fKind= kind;
+		}
+		public Kind getKind() {
+			return fKind;
+		}
+		@Override
+		public ModuleAddExpose convertToCP(CPListElementAttribute attribElem) {
+			if (fParent instanceof ConfiguredDetails) {
+				String targetModule= fParent.getContextModuleName();
+				if (targetModule != null) {
+					switch (fKind) {
+						case Exports:
+							return new ModuleAddExport(fParent.fFocusModule.getElementName(), fName, targetModule, attribElem);
+						case Opens:
+							return new ModuleAddOpens(fParent.fFocusModule.getElementName(), fName, targetModule, attribElem);
+						default:
+							break;
+					}
+				}
+			}
+			return null; // TODO: report
+		}
+	}
+
+	/** Declare that the module given by {@link #fName} is read by the selected focus module. */
+	static class ReadModule extends DetailNode<ModuleAddReads> {
+
+		public ReadModule(String targetModule, Details parent) {
+			super(parent);
+			fName= targetModule;
+		}
+
+		@Override
+		public ModuleAddReads convertToCP(CPListElementAttribute attribElem) {
+			if (fParent instanceof ConfiguredDetails) {
+				return new ModuleAddReads(fParent.fFocusModule.getElementName(), fName, attribElem);
+			}
+			return null; // TODO: report
+		}
+	}
+	/** Declare that the selected focus module is patched by the content of the given package fragment root. */
+	static class PatchModule extends DetailNode<ModulePatch> {
+		private IPackageFragmentRoot fRoot;
+
+		public PatchModule(IPackageFragmentRoot root, Details parent) {
+			super(parent);
+			fRoot= root;
+			fName= root.getPath().makeRelative().toString();
+		}
+
+		@Override
+		public ModulePatch convertToCP(CPListElementAttribute attribElem) {
+			return new ModulePatch(fParent.fFocusModule.getElementName(), attribElem); // assumes root is "the source folder" of the current context project
+		}
+	}
+
+	static class ModularityDetailsLabelProvider extends CPListLabelProvider {
+		private ImageDescriptorRegistry fRegistry= JavaPlugin.getImageDescriptorRegistry();
+		private JavaElementImageProvider fImageLabelProvider= new JavaElementImageProvider();
+		@Override
+		public String getText(Object element) {
+			if (element instanceof DeclaredDetails) {
+				return NewWizardMessages.ModuleDependenciesAdapter_declared_node;
+			}
+			if (element instanceof ConfiguredDetails) {
+				return NewWizardMessages.ModuleDependenciesAdapter_configured_node;
+			}
+			if (element instanceof DetailNode) {
+				return ((DetailNode<?>) element).getName();
+			}
+			return super.getText(element);
+		}
+		@Override
+		public Image getImage(Object element) {
+			if (element instanceof CPListElement) {
+				return fRegistry.get(JavaPluginImages.DESC_OBJS_MODULE);
+			}
+			if (element instanceof AccessiblePackage) {
+				AccessiblePackage.Kind kind= ((AccessiblePackage) element).getKind();
+				ImageDescriptor imgDesc= new DecoratedImageDescriptor(JavaPluginImages.DESC_OBJS_PACKAGE, kind.getDecoration(), true);
+				return JavaPlugin.getImageDescriptorRegistry().get(imgDesc);
+			}
+			if (element instanceof ReadModule) {
+				ImageDescriptor imgDesc= new DecoratedImageDescriptor(JavaPluginImages.DESC_OBJS_MODULE,
+												JavaPluginImages.DESC_OVR_READS, true);
+				return JavaPlugin.getImageDescriptorRegistry().get(imgDesc);
+			}
+			if (element instanceof PatchModule) {
+				return fImageLabelProvider.getImageLabel(((PatchModule) element).fRoot, 0);
+			}
+			return super.getImage(element);
+		}
+	}
+	
+	static class ElementSorter extends CPListElementSorter {
+		@Override
+		public int compare(Viewer viewer, Object e1, Object e2) {
+			// sorting for root nodes: CPListElement > DeclaredDetails > ConfiguredDetails
+			if (e1 instanceof DeclaredDetails) {
+				return e2 instanceof ConfiguredDetails ? -1 : 1;
+			}
+			if (e1 instanceof ConfiguredDetails) {
+				return e2 instanceof DeclaredDetails ? 1 : -1;
+			}
+			if (e2 instanceof DeclaredDetails || e2 instanceof ConfiguredDetails) {
+				return -1;
+			}
+			return super.compare(viewer, e1, e2);
+		}
+	}
+
+	public static void enableDefaultButtons(TreeListDialogField<?> list, boolean enable, boolean removeEnabled) {
+		list.enableButton(IDX_REMOVE, removeEnabled);
+		list.enableButton(IDX_EXPOSE_PACKAGE, enable);
+		list.enableButton(IDX_READ_MODULE, enable);
+		list.enableButton(IDX_PATCH, enable);
+	}
+
+	private final ModuleDependenciesPage fModuleDependenciesPage; // parent structure
+	private TreeListDialogField<Object> fDetailsList; // RHS widget managed by this class
+
+	public ModuleDependenciesAdapter(ModuleDependenciesPage moduleDependenciesPage) {
+		fModuleDependenciesPage= moduleDependenciesPage;
+	}
+
+	public void setList(TreeListDialogField<Object> detailsList) {
+		fDetailsList= detailsList;
+		fDetailsList.enableButton(IDX_REMOVE, false);
+		fDetailsList.enableButton(IDX_EXPOSE_PACKAGE, false);
+		fDetailsList.enableButton(IDX_READ_MODULE, false);
+		fDetailsList.enableButton(IDX_PATCH, false);
+		fDetailsList.enableButton(IDX_EDIT, false);
+	}
+
+	// -------- IListAdapter --------
+	@Override
+	public void customButtonPressed(TreeListDialogField<Object> field, int index) {
+		AccessiblePackage selectedPackage= null;
+		List<Object> selectedElements= field.getSelectedElements();
+		switch (index) {
+			case IDX_REMOVE:
+				if (selectedElements.size() == 0) {
+					// no detail selected, remove the module(s) (with question):
+					fModuleDependenciesPage.removeModules();
+				} else {
+					getConfiguredDetails().remove(selectedElements);
+				}
+				field.refresh();
+				break;
+			case IDX_EDIT:
+				if (selectedElements.size() == 1 && selectedElements.get(0) instanceof AccessiblePackage) {
+					selectedPackage= (AccessiblePackage) selectedElements.get(0);
+				}
+				// FIXME: can no longer use fallthrough, when editing other details
+				//$FALL-THROUGH$
+			case IDX_EXPOSE_PACKAGE:
+				if (getConfiguredDetails().addOrEditAccessiblePackage(selectedPackage, fModuleDependenciesPage.getShell())) {
+					field.refresh();
+				}
+				break;
+			case IDX_READ_MODULE:
+				if (getConfiguredDetails().addReads(fModuleDependenciesPage.getShell())) {
+					field.refresh();
+				}
+				break;
+			case IDX_PATCH:
+				if (getConfiguredDetails().addPatch(fModuleDependenciesPage.getShell(), fModuleDependenciesPage.fPatchMap)) {
+					field.refresh();
+				}
+				break;
+			default:
+				throw new IllegalArgumentException("Non-existent button index "+index); //$NON-NLS-1$
+		}
+	}
+
+	private ConfiguredDetails getConfiguredDetails() {
+		for (Object object : fDetailsList.getElements()) {
+			if (object instanceof ConfiguredDetails)
+				return (ConfiguredDetails) object;
+		}
+		throw new IllegalStateException("detail list has no ConfiguredDetails element"); //$NON-NLS-1$
+	}
+
+	@Override
+	public void selectionChanged(TreeListDialogField<Object> field) {
+		List<Object> selected= fDetailsList.getSelectedElements();
+		boolean enable= false;
+		if (selected.size() == 1) {
+			Object selectedNode= selected.get(0);
+			enable= isConfigurableNode(selectedNode);
+			fDetailsList.enableButton(IDX_EDIT, enable && selectedNode instanceof DetailNode<?>);
+			return;
+		} else {
+			enable= allAreConfigurable(selected);
+			fDetailsList.enableButton(IDX_EDIT, false);
+		}
+		fDetailsList.enableButton(IDX_EXPOSE_PACKAGE, enable);
+		fDetailsList.enableButton(IDX_READ_MODULE, enable);
+		fDetailsList.enableButton(IDX_PATCH, enable);
+		fDetailsList.enableButton(IDX_REMOVE, enable);
+	}
+
+	private boolean allAreConfigurable(List<Object> selected) {
+		for (Object node : selected) {
+			if (!isConfigurableNode(node))
+				return false;
+		}
+		return true;
+	}
+
+	private boolean isConfigurableNode(Object node) {
+		if (node instanceof ConfiguredDetails) {
+			return true;
+		}
+		if (node instanceof DeclaredDetails) {
+			return false;
+		}
+		if (node instanceof DetailNode) {
+			return ((DetailNode<?>) node).isIsConfigured();
+		}
+		return true;
+	}
+
+	@Override
+	public void doubleClicked(TreeListDialogField<Object> field) {
+		List<Object> selectedElements= fDetailsList.getSelectedElements();
+		if (selectedElements.size() == 1) {
+			Object selected= selectedElements.get(0);
+			if (selected instanceof ReadModule) {
+				String moduleName= ((ReadModule) selected).getName();
+				fModuleDependenciesPage.setSelectionToModule(moduleName);
+			} else {
+				TreeViewer treeViewer= fDetailsList.getTreeViewer();
+				boolean isExpanded= treeViewer.getExpandedState(selectedElements.get(0));
+				if (isExpanded) {
+					treeViewer.collapseToLevel(selectedElements.get(0), 1);				
+				} else {
+					treeViewer.expandToLevel(selectedElements.get(0), 1);
+				}
+			}
+		}
+	}
+
+	@Override
+	public void keyPressed(TreeListDialogField<Object> field, KeyEvent event) {
+//			libaryPageKeyPressed(field, event);
+	}
+
+	@Override
+	public Object[] getChildren(TreeListDialogField<Object> field, Object element) {
+		if (element instanceof CPListElement) { // assumed to be root
+			// no direct children
+		} else if (element instanceof DeclaredDetails) {
+			return ((DeclaredDetails) element).getPackages();
+		} else if (element instanceof ConfiguredDetails) {
+			return ((ConfiguredDetails) element).getChildren();
+		}
+		return new Object[0];
+	}
+
+	@Override
+	public Object getParent(TreeListDialogField<Object> field, Object element) {
+		if (element instanceof CPListElementAttribute) {
+			return ((CPListElementAttribute) element).getParent();
+		}
+		// TODO
+		return null;
+	}
+
+	@Override
+	public boolean hasChildren(TreeListDialogField<Object> field, Object element) {
+		Object[] children= getChildren(field, element);
+		return children != null && children.length > 0;
+	}
+	// ---------- IDialogFieldListener --------
+
+	@Override
+	public void dialogFieldChanged(DialogField field) {
+//			libaryPageDialogFieldChanged(field);
+	}
+
+}
\ No newline at end of file
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleDependenciesList.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleDependenciesList.java
new file mode 100644
index 0000000..95cc61a
--- /dev/null
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleDependenciesList.java
@@ -0,0 +1,204 @@
+/*******************************************************************************
+ * Copyright (c) 2019 GK Software SE, and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Stephan Herrmann - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.internal.ui.wizards.buildpaths;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+
+import org.eclipse.jface.layout.PixelConverter;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+
+import org.eclipse.jdt.core.IModuleDescription;
+
+import org.eclipse.jdt.ui.JavaElementImageDescriptor;
+
+import org.eclipse.jdt.internal.ui.JavaPlugin;
+import org.eclipse.jdt.internal.ui.JavaPluginImages;
+import org.eclipse.jdt.internal.ui.viewsupport.JavaElementImageProvider;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleDialog.ListContentProvider;
+
+/**
+ * List widget for the left-hand pane showing all modules in the module graph
+ */
+class ModuleDependenciesList {
+
+	static class FocusAwareStringComparator implements Comparator<String> {
+		private String fFocusString;
+
+		public FocusAwareStringComparator(String focusString) {
+			fFocusString= focusString;
+		}
+		@Override
+		public int compare(String o1, String o2) {
+			if (o1.equals(fFocusString)) {
+				return -1;
+			}
+			if (o2.equals(fFocusString)) {
+				return 1;
+			}
+			return o1.compareTo(o2);
+		}
+	}
+
+	static ImageDescriptor DESC_OBJ_MODULE = new JavaElementImageDescriptor(JavaPluginImages.DESC_OBJS_MODULE, 0,
+			JavaElementImageProvider.BIG_SIZE);
+	
+	static class ModulesLabelProvider extends LabelProvider implements ITableLabelProvider {
+		Function<String,ModuleKind> fGetModuleKind;
+
+		public ModulesLabelProvider(Function<String, ModuleKind> getModuleKind) {
+			fGetModuleKind= getModuleKind;
+		}
+
+		@Override
+		public Image getColumnImage(Object element, int columnIndex) {
+			ModuleKind kind= fGetModuleKind.apply((String) element);
+			ImageDescriptor imgDesc= new ModuleDependenciesPage.DecoratedImageDescriptor(
+					DESC_OBJ_MODULE, kind.getDecoration(), kind != ModuleKind.Focus
+			);
+			return JavaPlugin.getImageDescriptorRegistry().get(imgDesc);
+		}
+		
+		@Override
+		public String getColumnText(Object element, int columnIndex) {
+			return element.toString();
+		}
+	}
+
+	enum ModuleKind { 
+		Normal, Focus, System, UpgradedSystem, Automatic;
+		
+		public ImageDescriptor getDecoration() {
+			switch (this) {
+				case Focus:
+					return JavaPluginImages.DESC_OVR_FOCUS;
+				case Automatic:
+					return JavaPluginImages.DESC_OVR_AUTO_MOD;
+				case System:
+					return JavaPluginImages.DESC_OVR_SYSTEM_MOD;
+					//$CASES-OMITTED$
+				default:
+					return null;
+			}
+		}
+	}
+	public final List<String> fNames= new ArrayList<>();
+	private FocusAwareStringComparator fNamesComparator;
+	public final Map<String,CPListElement> fModule2Element= new HashMap<>();
+	private List<String> fInitialNames= new ArrayList<>();
+	private TableViewer fViewer;
+	private Map<CPListElement,String> fElem2ModName= new HashMap<>();
+	private Map<CPListElement,IModuleDescription> fModules= new HashMap<>();
+	private Map<CPListElement,ModuleKind> fKinds= new HashMap<>();
+
+	public void createViewer(Composite left, PixelConverter converter) {
+		TableViewer tableViewer= new TableViewer(left, SWT.MULTI | SWT.BORDER);
+		tableViewer.setContentProvider(new ListContentProvider());
+		tableViewer.setLabelProvider(new ModulesLabelProvider(this::getModuleKind));
+		tableViewer.setInput(fNames);
+
+		GridData gd= new GridData(SWT.FILL, SWT.FILL, true, true);
+		gd.widthHint= converter.convertWidthInCharsToPixels(30);
+		gd.heightHint= converter.convertHeightInCharsToPixels(6);
+		tableViewer.getControl().setLayoutData(gd);
+		fViewer= tableViewer;
+	}
+
+	public void setSelectionChangedListener(BiConsumer<List<CPListElement>,IModuleDescription> listener) {
+		fViewer.addSelectionChangedListener(e -> listener.accept(getSelectedElements(), getSelectedModule()));
+	}
+
+	public void addModule(IModuleDescription module, CPListElement cpe, ModuleKind kind) {
+		String moduleName = module.getElementName();
+		fNames.add(moduleName);
+		fModule2Element.put(moduleName, cpe);
+		fElem2ModName.put(cpe, moduleName);
+		fKinds.put(cpe, kind);
+		switch (kind) {
+			case System:
+				break; // system modules are already stored inside the CPListElement
+			case Focus:
+				fNamesComparator= new FocusAwareStringComparator(moduleName);
+				//$FALL-THROUGH$
+				//$CASES-OMITTED$
+			default:
+				fModules.put(cpe, module);
+				break;
+		}
+	}
+
+	public void captureInitial() {
+		fInitialNames.clear();
+		fInitialNames.addAll(fNames);
+	}
+	
+	public boolean isModified() {
+		return !fInitialNames.equals(fNames);
+	}
+
+	public void refresh() {
+		fNames.sort(fNamesComparator);
+		fViewer.refresh();
+	}
+
+	public ModuleKind getModuleKind(String name) {
+		CPListElement element= fModule2Element.get(name);
+		if (element != null) {
+			return fKinds.get(element);
+		}
+		return ModuleKind.Normal;
+	}
+	
+	public ModuleKind getModuleKind(CPListElement element) {
+		return getModuleKind(fElem2ModName.get(element));
+	}
+
+	public List<CPListElement> getSelectedElements() {
+		List<CPListElement> selectedElements= new ArrayList<>();
+		for (Object selected : fViewer.getStructuredSelection().toList()) {
+			selectedElements.add(fModule2Element.get(selected));
+		}
+		return selectedElements;
+	}
+
+	private IModuleDescription getSelectedModule() {
+		List<CPListElement> selectedElems= getSelectedElements();
+		if (selectedElems.size() == 1) {
+			CPListElement selectedElem= selectedElems.get(0);
+			if (selectedElem.getModule() != null) {
+				return selectedElem.getModule(); // system module
+			}
+			return fModules.get(selectedElem);
+		}
+		return null;
+	}
+
+	public void setSelectionToModule(String moduleName) {
+		fViewer.setSelection(new StructuredSelection(moduleName), true);
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleDependenciesPage.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleDependenciesPage.java
new file mode 100644
index 0000000..d045188
--- /dev/null
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleDependenciesPage.java
@@ -0,0 +1,610 @@
+/*******************************************************************************
+ * Copyright (c) 2019 GK Software SE, and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Stephan Herrmann - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.internal.ui.wizards.buildpaths;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.ibm.icu.text.MessageFormat;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+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.Shell;
+
+import org.eclipse.core.resources.IProject;
+
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.layout.PixelConverter;
+import org.eclipse.jface.resource.CompositeImageDescriptor;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.StructuredSelection;
+
+import org.eclipse.ui.preferences.IWorkbenchPreferenceContainer;
+
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IModuleDescription;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+
+import org.eclipse.jdt.internal.ui.JavaPlugin;
+import org.eclipse.jdt.internal.ui.viewsupport.JavaElementImageProvider;
+import org.eclipse.jdt.internal.ui.wizards.NewWizardMessages;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleDependenciesList.ModuleKind;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.LimitModules;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModulePatch;
+import org.eclipse.jdt.internal.ui.wizards.dialogfields.CheckedListDialogField;
+import org.eclipse.jdt.internal.ui.wizards.dialogfields.DialogField;
+import org.eclipse.jdt.internal.ui.wizards.dialogfields.LayoutUtil;
+import org.eclipse.jdt.internal.ui.wizards.dialogfields.ListDialogField;
+import org.eclipse.jdt.internal.ui.wizards.dialogfields.TreeListDialogField;
+
+/*
+ * TODO: ("+" must have, "-" later)
+ * LHS:
+ * - module kind Upgrade of a System Library (incl. icon decoration)
+ * - indicator for module having tweaks (to be shown in the LHS)
+ * - better help on how to remove non-JRE modules (module-info, modulepath)
+ * RHS:
+ * + consider one more toplevel synth node: "Patched with/from" (whatever word we select, here and for the button)
+ *   + its children would be IPackageFragmentRoot, or (Till's suggestions): IJavaProject
+ *   + validate that no root tries to patch different modules at the same time
+ *   + handle patch project w/o module-info
+ *     + treat the patched module as the current context module (pinned)
+ * + qualified exports / opens
+ * + show Declared > reads (for requires)
+ * + fix decoration in ModulesSelectionDialog (when used for add reads)
+ * General:
+ * - distinguish test/main dependencies
+ * + special elements: ALL-UNNAMED, ALL-SYSTEM ...
+ *   + Select All button in Add system module dialog
+ * - Help page and reference to it
+ */
+public class ModuleDependenciesPage extends BuildPathBasePage {
+
+	/** Composed image descriptor consisting of a base image and optionally a decoration overlay. */
+	static class DecoratedImageDescriptor extends CompositeImageDescriptor {
+		private ImageDescriptor fBaseImage;
+		private ImageDescriptor fOverlay;
+		private boolean fDrawAtOffset;
+		public DecoratedImageDescriptor(ImageDescriptor baseImage, ImageDescriptor overlay, boolean drawAtOffset) {
+			fBaseImage= baseImage;
+			fOverlay= overlay;
+			fDrawAtOffset= drawAtOffset;
+		}
+		@Override
+		protected void drawCompositeImage(int width, int height) {
+			drawImage(createCachedImageDataProvider(fBaseImage), 0, 0);
+			if (fOverlay != null) {
+				CachedImageDataProvider provider= createCachedImageDataProvider(fOverlay);
+				if (fDrawAtOffset) {
+					drawImage(provider, getSize().x - provider.getWidth(), 0);
+				} else {
+					drawImage(provider, 0, 0);
+				}
+			}
+		}
+		@Override
+		protected Point getSize() {
+			return JavaElementImageProvider.BIG_SIZE;
+		}
+		@Override
+		public int hashCode() {
+			final int prime= 31;
+			int result= 1;
+			result= prime * result + ((fBaseImage == null) ? 0 : fBaseImage.hashCode());
+			result= prime * result + ((fOverlay == null) ? 0 : fOverlay.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;
+			DecoratedImageDescriptor other= (DecoratedImageDescriptor) obj;
+			if (fBaseImage == null) {
+				if (other.fBaseImage != null)
+					return false;
+			} else if (!fBaseImage.equals(other.fBaseImage))
+				return false;
+			if (fOverlay == null) {
+				if (other.fOverlay != null)
+					return false;
+			} else if (!fOverlay.equals(other.fOverlay))
+				return false;
+			return true;
+		}
+	}
+
+	private final ListDialogField<CPListElement> fClassPathList; // shared with other pages
+	private IJavaProject fCurrJProject;
+
+	// LHS list:
+	private ModuleDependenciesList fModuleList;
+
+	// RHS tree:
+	private final TreeListDialogField<Object> fDetailsList;
+
+	// bi-directional dependency graph:
+	private Map<String,List<String>> fModule2RequiredModules;
+	private Map<String,List<String>> fModuleRequiredByModules;
+
+	public final Map<String,String> fPatchMap= new HashMap<>();
+
+	private Control fSWTControl;
+	private final IWorkbenchPreferenceContainer fPageContainer; // for switching page (not yet used)
+
+	public ModuleDependenciesPage(CheckedListDialogField<CPListElement> classPathList, IWorkbenchPreferenceContainer pageContainer) {
+		fClassPathList= classPathList;
+		fPageContainer= pageContainer;
+		fSWTControl= null;
+		
+		String[] buttonLabels= new String[] {
+				NewWizardMessages.ModuleDependenciesPage_modules_remove_button,
+				/* */ null,
+				NewWizardMessages.ModuleDependenciesPage_modules_read_button,
+				NewWizardMessages.ModuleDependenciesPage_modules_expose_package_button,
+				/* */ null,
+				NewWizardMessages.ModuleDependenciesPage_modules_patch_button,
+				/* */ null,
+				NewWizardMessages.ModuleDependenciesPage_modules_edit_button
+			};
+
+		fModuleList= new ModuleDependenciesList();
+
+		ModuleDependenciesAdapter adapter= new ModuleDependenciesAdapter(this);
+		fDetailsList= new TreeListDialogField<>(adapter, buttonLabels, new ModuleDependenciesAdapter.ModularityDetailsLabelProvider());
+		fDetailsList.setDialogFieldListener(adapter);
+		fDetailsList.setLabelText(NewWizardMessages.ModuleDependenciesPage_details_label);
+
+		adapter.setList(fDetailsList);
+
+		fDetailsList.setViewerComparator(new ModuleDependenciesAdapter.ElementSorter());
+	}
+
+	@Override
+	public Control getControl(Composite parent) {
+		PixelConverter converter= new PixelConverter(parent);
+
+		Composite composite= new Composite(parent, SWT.NONE);
+		composite.setFont(parent.getFont());
+		GridLayout layout= new GridLayout(2, false);
+		layout.marginBottom= 0;
+		composite.setLayout(layout);
+		GridData gd= new GridData(SWT.FILL, SWT.FILL, true, true);
+		gd.minimumWidth= 0;
+		composite.setLayoutData(gd);
+
+		// === left: ===
+		Composite left= new Composite(composite, SWT.NONE);
+		layout= new GridLayout(1, false);
+		layout.marginBottom= 0;
+		left.setLayout(layout);
+		gd= new GridData(SWT.FILL, SWT.FILL, true, true);
+		gd.minimumWidth= 0;
+		left.setLayoutData(gd);
+
+		Label title= new Label(left, SWT.NONE);
+		title.setText(NewWizardMessages.ModuleDependenciesPage_modules_label);
+
+		fModuleList.createViewer(left, converter);
+		fModuleList.setSelectionChangedListener((elems, mod) -> selectModule(elems, mod));
+		
+		Button addButton= new Button(left, SWT.NONE);
+		addButton.setText(NewWizardMessages.ModuleDependenciesPage_addSystemModule_button);
+		addButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> addSystemModules()));
+
+		// === right: ===
+		Composite right= new Composite(composite, SWT.NONE);
+		layout= new GridLayout(2, false);
+		layout.marginBottom= 0;
+		right.setLayout(layout);
+		gd= new GridData(SWT.FILL, SWT.FILL, true, true);
+		gd.minimumWidth= 0;
+		right.setLayoutData(gd);
+
+		LayoutUtil.doDefaultLayout(right, new DialogField[] { fDetailsList }, true, SWT.DEFAULT, SWT.DEFAULT);
+		LayoutUtil.setHorizontalGrabbing(fDetailsList.getTreeControl(null));
+
+		int buttonBarWidth= converter.convertWidthInCharsToPixels(24);
+		fDetailsList.setButtonsMinWidth(buttonBarWidth);
+
+		fDetailsList.setViewerComparator(new CPListElementSorter());
+
+		fSWTControl= composite;
+
+		return composite;
+	}
+
+	public Shell getShell() {
+		if (fSWTControl != null) {
+			return fSWTControl.getShell();
+		}
+		return JavaPlugin.getActiveWorkbenchShell();
+	}
+
+	@Override
+	public void init(IJavaProject jproject) {
+		fCurrJProject= jproject;
+		if (Display.getCurrent() != null) {
+			scanModules();
+		} else {
+			Display.getDefault().asyncExec(new Runnable() {
+				@Override
+				public void run() {
+					scanModules();
+				}
+			});
+		}
+	}
+
+	protected void scanModules() {
+		fModule2RequiredModules= new HashMap<>();
+		fModuleRequiredByModules= new HashMap<>();
+		Set<String> recordedModules = new HashSet<>();
+
+		List<CPListElement> cpelements= fClassPathList.getElements();
+
+		for (CPListElement cpe : cpelements) {
+			Object value= cpe.getAttribute(CPListElement.MODULE);
+			if (value instanceof ModuleEncapsulationDetail[]) {
+				for (ModuleEncapsulationDetail detail : (ModuleEncapsulationDetail[]) value) {
+					if (detail instanceof ModulePatch) {
+						ModulePatch patch= (ModulePatch) detail;
+						fPatchMap.put(patch.fPaths, patch.fModule);
+					}
+				}
+			}
+
+			switch (cpe.getEntryKind()) {
+				case IClasspathEntry.CPE_SOURCE:
+					IPackageFragmentRoot[] fragmentRoots= fCurrJProject.findPackageFragmentRoots(cpe.getClasspathEntry());
+					if (fragmentRoots != null && fragmentRoots.length == 1) {
+						for (IPackageFragmentRoot fragmentRoot : fragmentRoots) {
+							IModuleDescription module= fragmentRoot.getModuleDescription();
+							if (module != null) {
+								recordModule(module, recordedModules, cpe, ModuleKind.Focus);
+								break;
+							}
+						}
+					}
+					break;
+				case IClasspathEntry.CPE_PROJECT:
+					IProject project= fCurrJProject.getProject().getWorkspace().getRoot().getProject(cpe.getClasspathEntry().getPath().toString());
+					try {
+						IJavaProject jProject= JavaCore.create(project);
+						IModuleDescription module= jProject.getModuleDescription();
+						ModuleKind kind= ModuleKind.Normal;
+						if (module == null) {
+							module= JavaCore.getAutomaticModuleDescription(jProject);
+							kind= ModuleKind.Automatic;
+						}
+						if (module != null) {
+							recordModule(module, recordedModules, cpe, kind);
+						}
+					} catch (JavaModelException e) {
+						// ignore
+					}
+					break;
+				case IClasspathEntry.CPE_CONTAINER:
+					ModuleKind kind= LibrariesWorkbookPage.isJREContainer(cpe.getPath()) ? ModuleKind.System : ModuleKind.Normal;
+					for (Object object : cpe.getChildren(true)) {
+						if (object instanceof CPListElement) {
+							CPListElement childElement= (CPListElement) object;
+							IModuleDescription childModule= childElement.getModule();
+							if (childModule != null) {
+								fModuleList.addModule(childModule, childElement, kind);
+							}
+						}
+					}
+					if (kind == ModuleKind.System) {
+						// additionally capture dependency information about all system module disregarding --limit-modules
+						for (IPackageFragmentRoot packageRoot : fCurrJProject.findUnfilteredPackageFragmentRoots(cpe.getClasspathEntry())) {
+							IModuleDescription module= packageRoot.getModuleDescription();
+							if (module != null) {
+								recordModule(module, recordedModules, null/*don't add to fModuleList*/, kind);
+							}
+						}
+					}
+					break;
+				default: // LIBRARY & VARIABLE:
+					for (IPackageFragmentRoot packageRoot : fCurrJProject.findPackageFragmentRoots(cpe.getClasspathEntry())) {
+						IModuleDescription module= packageRoot.getModuleDescription();
+						kind= ModuleKind.Normal;
+						if (module == null) {
+							try {
+								module= JavaCore.getAutomaticModuleDescription(packageRoot);
+								kind= ModuleKind.Automatic;
+							} catch (JavaModelException | IllegalArgumentException e) {
+								// ignore
+							}
+						}
+						if (module != null) {
+							recordModule(module, recordedModules, cpe, kind);
+							break;
+						}
+					}
+			}
+		}
+		fModuleList.captureInitial();
+		fModuleList.refresh();
+	}
+
+	private void recordModule(IModuleDescription module, Set<String> moduleNames, CPListElement cpe, ModuleKind kind) {
+		if (cpe != null) {
+			fModuleList.addModule(module, cpe, kind);
+		}
+		String moduleName= module.getElementName();
+		if (moduleNames.add(moduleName)) {
+			try {
+				for (String required : module.getRequiredModuleNames()) {
+					List<String> otherModules= fModule2RequiredModules.get(moduleName);
+					if (otherModules == null) {
+						otherModules= new ArrayList<>();
+						fModule2RequiredModules.put(moduleName, otherModules);
+					}
+					otherModules.add(required);
+
+					otherModules= fModuleRequiredByModules.get(required);
+					if (otherModules == null) {
+						otherModules= new ArrayList<>();
+						fModuleRequiredByModules.put(required, otherModules);
+					}
+					otherModules.add(moduleName);
+				}
+			} catch (JavaModelException e) {
+				JavaPlugin.log(e);
+			}
+		}
+	}
+
+
+	@Override
+	public List<?> getSelection() {
+		return fDetailsList.getSelectedElements();
+	}
+
+	@Override
+	public void setSelection(List<?> selElements, boolean expand) {
+		fDetailsList.selectElements(new StructuredSelection(selElements));
+		if (expand) {
+			for (int i= 0; i < selElements.size(); i++) {
+				fDetailsList.expandElement(selElements.get(i), 1);
+			}
+		}
+	}
+
+	public void setSelectionToModule(String moduleName) {
+		int idx= fModuleList.fNames.indexOf(moduleName);
+		if (idx != -1) {
+			fModuleList.setSelectionToModule(moduleName);
+		}
+	}
+
+	private void selectModule(List<CPListElement> elements, IModuleDescription module) {
+		fDetailsList.removeAllElements();
+		if (elements.size() == 1) {
+			CPListElement element= elements.get(0);
+			fDetailsList.addElement(element);
+			fDetailsList.addElement(new ModuleDependenciesAdapter.DeclaredDetails(module, element));
+			ModuleKind moduleKind= fModuleList.getModuleKind(element);
+			if (moduleKind != ModuleKind.Focus) {
+				ModuleDependenciesAdapter.ConfiguredDetails configured= new ModuleDependenciesAdapter.ConfiguredDetails(module, element, moduleKind);
+				fDetailsList.addElement(configured);
+				fDetailsList.expandElement(configured, 1);
+			}
+			ModuleDependenciesAdapter.enableDefaultButtons(fDetailsList, true, !elements.isEmpty());
+		} else {
+			ModuleDependenciesAdapter.enableDefaultButtons(fDetailsList, false, !elements.isEmpty());
+		}
+	}
+
+	@Override
+	public boolean isEntryKind(int kind) {
+		return true;
+	}
+
+	@Override
+	public void setFocus() {
+    	fDetailsList.setFocus();
+	}
+	
+	void addSystemModules() {
+		CPListElement cpListElement= findSystemLibraryElement();
+		ModuleSelectionDialog dialog= new ModuleSelectionDialog(getShell(), fCurrJProject, cpListElement.getClasspathEntry(), fModuleList.fNames, this::computeForwardClosure);
+		if (dialog.open() == IDialogConstants.OK_ID) {
+			for (IModuleDescription addedModule : dialog.getResult()) {
+				fModuleList.addModule(addedModule, getOrCreateModuleCPE(cpListElement, addedModule), ModuleKind.System);
+			}
+			updateLimitModules(cpListElement.findAttributeElement(CPListElement.MODULE));
+			fModuleList.refresh();
+		}
+	}
+
+	CPListElement getOrCreateModuleCPE(CPListElement parentCPE, IModuleDescription module) {
+		CPListElement element= fModuleList.fModule2Element.get(module.getElementName());
+		if (element != null) {
+			return element;
+		}
+		try {
+			IClasspathEntry entry= fCurrJProject.getClasspathEntryFor(module.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT).getPath());
+			return CPListElement.create(parentCPE, entry, module, true, fCurrJProject);
+		} catch (JavaModelException e) {
+			JavaPlugin.log(e.getStatus());
+			return null;
+		}
+	}
+	private CPListElement findSystemLibraryElement() {
+		for (CPListElement cpListElement : fClassPathList.getElements()) {
+			if (LibrariesWorkbookPage.isJREContainer(cpListElement.getPath()))
+				return cpListElement;
+		}
+		return null;
+	}
+
+	void removeModules() {
+		List<CPListElement> selectedElements= fModuleList.getSelectedElements();
+		List<String> selectedModuleNames= new ArrayList<>();
+		Set<String> allModulesToRemove = new HashSet<>();
+		for (CPListElement selectedElement : selectedElements) {
+			if (fModuleList.getModuleKind(selectedElement) == ModuleKind.Focus) {
+				MessageDialog.openError(getShell(), NewWizardMessages.ModuleDependenciesPage_removeModule_dialog_title,
+						NewWizardMessages.ModuleDependenciesPage_removeCurrentModule_error);
+				return;
+			}
+			IModuleDescription mod = selectedElement.getModule();
+			if (mod == null) {
+				MessageDialog.openError(getShell(), NewWizardMessages.ModuleDependenciesPage_removeModule_dialog_title,
+						MessageFormat.format(NewWizardMessages.ModuleDependenciesPage_removeModule_error_with_hint,
+								selectedElement.getPath().lastSegment(), NewWizardMessages.ModuleDependenciesPage_removeSystemModule_error_hint));
+				// TODO: offer to switch to the corresponding tab?
+				return;
+			}
+			String moduleName= mod.getElementName();
+			if (moduleName.equals("java.base")) { //$NON-NLS-1$
+				MessageDialog.openError(getShell(), NewWizardMessages.ModuleDependenciesPage_removeModule_dialog_title,
+						MessageFormat.format(NewWizardMessages.ModuleDependenciesPage_removeModule_error_with_hint, moduleName, "")); //$NON-NLS-1$
+				return;
+			}
+			selectedModuleNames.add(moduleName);
+			collectModulesToRemove(moduleName, allModulesToRemove);
+		}
+		String seedModules= String.join(", ", selectedModuleNames); //$NON-NLS-1$
+		if (allModulesToRemove.size() == selectedModuleNames.size()) {
+			if (confirmRemoveModule(MessageFormat.format(NewWizardMessages.ModuleDependenciesPage_removingModule_message, seedModules))) {
+				fModuleList.fNames.removeAll(selectedModuleNames);
+				fModuleList.refresh();
+			}
+		} else {
+			StringBuilder message = new StringBuilder(
+					MessageFormat.format(NewWizardMessages.ModuleDependenciesPage_removingModuleTransitive_message, seedModules));
+			// append sorted list minus the selected module:
+			message.append(allModulesToRemove.stream()
+					.filter(m -> !seedModules.contains(m))
+					.sorted()
+					.collect(Collectors.joining("\n\t", "\t", ""))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+			if (!confirmRemoveModule(message.toString()))
+				return;
+			fModuleList.fNames.removeAll(allModulesToRemove);
+			fModuleList.refresh();
+		}
+		for (CPListElement elem: selectedElements) {
+			Object container= elem.getParentContainer();
+			if (container instanceof CPListElement) {
+				CPListElement containerElement= (CPListElement) container;
+				if (LibrariesWorkbookPage.isJREContainer(containerElement.getPath())) {
+					CPListElementAttribute attribute= containerElement.findAttributeElement(CPListElement.MODULE);
+					updateLimitModules(attribute);
+					break;
+				}
+			}
+		}
+	}
+
+	private Set<String> computeForwardClosure(List<String> seeds) {
+		Set<String> closure= new HashSet<>();
+		collectForwardClosure(seeds, closure);
+		return closure;
+	}
+	private void collectForwardClosure(List<String> seeds, Set<String> closure) {
+		for (String seed : seeds) {
+			if (closure.add(seed) && !fModuleList.fNames.contains(seed)) {
+				List<String> deps= fModule2RequiredModules.get(seed);
+				if (deps != null) {
+					collectForwardClosure(deps, closure);
+				}
+			}
+		}
+	}
+
+	private void collectModulesToRemove(String mod, Set<String> modulesToRemove) {
+		if (fModuleList.fNames.contains(mod) && modulesToRemove.add(mod)) {
+			List<String> requireds= fModuleRequiredByModules.get(mod);
+			if (requireds != null) {
+				for (String required : requireds) {
+					collectModulesToRemove(required, modulesToRemove);
+				}
+			}
+		}
+	}
+	
+	private boolean confirmRemoveModule(String message) {
+		int answer= MessageDialog.open(MessageDialog.QUESTION, getShell(), NewWizardMessages.ModuleDependenciesPage_removeModule_dialog_title, message, SWT.NONE, NewWizardMessages.ModuleDependenciesPage_remove_button, NewWizardMessages.ModuleDependenciesPage_cancel_button);
+		return answer == 0;
+	}
+
+	private void updateLimitModules(CPListElementAttribute moduleAttribute) {
+		LimitModules limitModules= new ModuleEncapsulationDetail.LimitModules(reduceNames(fModuleList.fNames), moduleAttribute);
+		Object value= moduleAttribute.getValue();
+		if (value instanceof ModuleEncapsulationDetail[]) {
+			ModuleEncapsulationDetail[] details= (ModuleEncapsulationDetail[]) value;
+			for (int i= 0; i < details.length; i++) {
+				if (details[i] instanceof LimitModules) {
+					// replace existing --limit-modules
+					details[i]= limitModules;
+					moduleAttribute.setValue(details);
+					return;
+				}
+			}
+			if (details.length > 0) {
+				// append to existing list of other details:
+				ModuleEncapsulationDetail[] newDetails= Arrays.copyOf(details, details.length+1);
+				newDetails[newDetails.length-1]= limitModules;
+				moduleAttribute.setValue(newDetails);
+				return;
+			}
+		}
+		// set as singleton detail:
+		moduleAttribute.setValue(new ModuleEncapsulationDetail[] { limitModules });
+	}
+	
+	List<String> reduceNames(List<String> names) {
+		List<String> reduced= new ArrayList<>();
+		outer:
+		for (String name : names) {
+			if (fModuleList.getModuleKind(name) == ModuleKind.System) {
+				List<String> dominators= fModuleRequiredByModules.get(name);
+				if (dominators != null) {
+					for (String dominator : dominators) {
+						if (fModuleList.fNames.contains(dominator)) {
+							continue outer;
+						}
+					}
+				}
+				reduced.add(name);
+			}
+		}
+		return reduced;
+	}
+}
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleDialog.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleDialog.java
index 130d440..9a4500f 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleDialog.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleDialog.java
@@ -85,6 +85,7 @@
 import org.eclipse.jdt.internal.ui.wizards.NewWizardMessages;
 import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.LimitModules;
 import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddExport;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddExpose;
 import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModuleAddReads;
 import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleEncapsulationDetail.ModulePatch;
 import org.eclipse.jdt.internal.ui.wizards.dialogfields.DialogField;
@@ -184,7 +185,7 @@
 	private final SelectionButtonDialogField fIsPatchCheckbox;
 	private final StringDialogField fPatchedModule;
 	
-	private final ListDialogField<ModuleAddExport> fAddExportsList;
+	private final ListDialogField<ModuleAddExpose> fAddExportsList;
 
 	private final ListDialogField<ModuleAddReads> fAddReadsList;
 	
@@ -225,7 +226,7 @@
 		fPatchedModule.setLabelText(NewWizardMessages.ModuleDialog_patched_module_label);
 		fPatchedModule.setDialogFieldListener(field -> validateDetails(field));
 
-		fAddExportsList= createDetailListContents(entryToEdit, NewWizardMessages.ModuleDialog_exports_label, new AddExportsAdapter(), ModuleAddExport.class);
+		fAddExportsList= createDetailListContents(entryToEdit, NewWizardMessages.ModuleDialog_exports_label, new AddExportsAdapter(), ModuleAddExpose.class);
 		fAddReadsList= createDetailListContents(entryToEdit, NewWizardMessages.ModuleDialog_reads_label, new AddReadsAdapter(), ModuleAddReads.class);
 
 		initializeValues();
@@ -635,7 +636,7 @@
 			}
 		}
 		if (status.isOK()) {
-			for (ModuleAddExport export : fAddExportsList.getElements()) {
+			for (ModuleAddExpose export : fAddExportsList.getElements()) {
 				if (!packages.add(export.fPackage)) {
 					status.setError(Messages.format(NewWizardMessages.ModuleDialog_duplicatePackage_error, export.fPackage));
 					break;
@@ -917,24 +918,24 @@
 		abstract void editEntry(ListDialogField<T> field, T detail);
 	}
 
-	private class AddExportsAdapter extends ListAdapter<ModuleAddExport> {
+	private class AddExportsAdapter extends ListAdapter<ModuleAddExpose> {
 
 		@Override
-		void addEntry(ListDialogField<ModuleAddExport> field) {
+		void addEntry(ListDialogField<ModuleAddExpose> field) {
 			ModuleAddExport initialValue= new ModuleAddExport(getSourceModuleName(), NO_NAME, getCurrentModuleName(), null);
 			ModuleAddExportsDialog dialog= new ModuleAddExportsDialog(getShell(), fJavaElements, initialValue);
 			if (dialog.open() == Window.OK) {
-				ModuleAddExport export= dialog.getExport(fCurrCPElement.findAttributeElement(CPListElement.MODULE));
+				ModuleAddExpose export= dialog.getExport(fCurrCPElement.findAttributeElement(CPListElement.MODULE));
 				if (export != null)
 					field.addElement(export);
 			}
 		}
 
 		@Override
-		void editEntry(ListDialogField<ModuleAddExport> field, ModuleAddExport export) {
+		void editEntry(ListDialogField<ModuleAddExpose> field, ModuleAddExpose export) {
 			ModuleAddExportsDialog dialog= new ModuleAddExportsDialog(getShell(), fJavaElements, export);
 			if (dialog.open() == Window.OK) {
-				ModuleAddExport newExport= dialog.getExport(fCurrCPElement.findAttributeElement(CPListElement.MODULE));
+				ModuleAddExpose newExport= dialog.getExport(fCurrCPElement.findAttributeElement(CPListElement.MODULE));
 				if (newExport != null) {
 					field.replaceElement(export, newExport);
 				} else {
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleEncapsulationDetail.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleEncapsulationDetail.java
index de89fee..73b6131 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleEncapsulationDetail.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleEncapsulationDetail.java
@@ -103,39 +103,96 @@
 		}
 
 		public final String fModule;
+		public final String fPaths;
 		
-		public ModulePatch(String module, CPListElementAttribute attribElem) {
-			fModule= module;
+		public ModulePatch(String value, CPListElementAttribute attribElem) {
+			int eqIdx= value.indexOf('=');
+			if (eqIdx == -1) {
+				fModule= value;
+				fPaths= null; // FIXME: find path to encl. project (src folder??)
+			} else {
+				fModule= value.substring(0, eqIdx);
+				fPaths= value.substring(eqIdx + 1);
+			}
+			fAttribElem= attribElem;
+		}
+
+		public ModulePatch(String moduleName, String path, CPListElementAttribute attribElem) {
+			fModule= moduleName;
+			fPaths= path;
 			fAttribElem= attribElem;
 		}
 
 		@Override
+		public boolean affects(String module) {
+			return module.equals(fModule);
+		}
+
+		@Override
+		public int hashCode() {
+			final int prime= 31;
+			int result= 1;
+			result= prime * result + ((fModule == null) ? 0 : fModule.hashCode());
+			result= prime * result + ((fPaths == null) ? 0 : fPaths.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;
+			ModulePatch other= (ModulePatch) obj;
+			if (fModule == null) {
+				if (other.fModule != null)
+					return false;
+			} else if (!fModule.equals(other.fModule))
+				return false;
+			if (fPaths == null) {
+				if (other.fPaths != null)
+					return false;
+			} else if (!fPaths.equals(other.fPaths))
+				return false;
+			return true;
+		}
+
+		@Override
 		public String toString() {
+			if (fPaths != null) {
+				return fModule + '=' + fPaths;
+			}
 			return fModule;
 		}
 	}
 
-	/**
-	 * Node in the tree of CPListElement et al, representing an add-exports module directive.
-	 */
-	static class ModuleAddExport extends ModuleEncapsulationDetail {
+	/** Shared implementation for ModuleAddExports & ModuleAddOpens (same structure). */
+	abstract static class ModuleAddExpose extends ModuleEncapsulationDetail {
 
-		public static ModuleAddExport fromString(CPListElementAttribute attribElem, String value) {
+		public static ModuleAddExpose fromString(CPListElementAttribute attribElem, String value, boolean isExports) {
 			int slash= value.indexOf('/');
 			int equals= value.indexOf('=');
 			if (slash != -1 && equals != -1 && equals > slash) {
-				return new ModuleAddExport(value.substring(0, slash),
+				if (isExports)
+					return new ModuleAddExport(value.substring(0, slash),
 											value.substring(slash+1, equals),
 											value.substring(equals+1),
 											attribElem);
+				else
+					return new ModuleAddOpens(value.substring(0, slash),
+							value.substring(slash+1, equals),
+							value.substring(equals+1),
+							attribElem);
 			}
 			return null;
 		}
 
-		public static Collection<ModuleAddExport> fromMultiString(CPListElementAttribute attribElem, String values) {
-			List<ModuleAddExport> exports= new ArrayList<>();
+		public static Collection<ModuleAddExpose> fromMultiString(CPListElementAttribute attribElem, String values, boolean isExports) {
+			List<ModuleAddExpose> exports= new ArrayList<>();
 			for (String value : values.split(":")) { //$NON-NLS-1$
-				ModuleAddExport export= fromString(attribElem, value);
+				ModuleAddExpose export= fromString(attribElem, value, isExports);
 				if (export != null)
 					exports.add(export);
 			}
@@ -146,7 +203,7 @@
 		public final String fPackage;
 		public final String fTargetModules;
 
-		public ModuleAddExport(String sourceModule, String aPackage, String targetModules, CPListElementAttribute attribElem) {
+		public ModuleAddExpose(String sourceModule, String aPackage, String targetModules, CPListElementAttribute attribElem) {
 			fSourceModule= sourceModule;
 			fPackage= aPackage;
 			fTargetModules= targetModules;
@@ -154,12 +211,72 @@
 		}
 
 		@Override
+		public boolean affects(String module) {
+			return module.equals(fSourceModule);
+		}
+
+		@Override
+		public int hashCode() {
+			final int prime= 31;
+			int result= 1;
+			result= prime * result + ((fPackage == null) ? 0 : fPackage.hashCode());
+			result= prime * result + ((fSourceModule == null) ? 0 : fSourceModule.hashCode());
+			result= prime * result + ((fTargetModules == null) ? 0 : fTargetModules.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;
+			ModuleAddExpose other= (ModuleAddExpose) obj;
+			if (fPackage == null) {
+				if (other.fPackage != null)
+					return false;
+			} else if (!fPackage.equals(other.fPackage))
+				return false;
+			if (fSourceModule == null) {
+				if (other.fSourceModule != null)
+					return false;
+			} else if (!fSourceModule.equals(other.fSourceModule))
+				return false;
+			if (fTargetModules == null) {
+				if (other.fTargetModules != null)
+					return false;
+			} else if (!fTargetModules.equals(other.fTargetModules))
+				return false;
+			return true;
+		}
+
+		@Override
 		public String toString() {
 			return fSourceModule+'/'+fPackage+'='+fTargetModules;
 		}
 	}
 
 	/**
+	 * Node in the tree of CPListElement et al, representing an add-exports module directive.
+	 */
+	static class ModuleAddExport extends ModuleAddExpose {
+		public ModuleAddExport(String sourceModule, String aPackage, String targetModules, CPListElementAttribute attribElem) {
+			super(sourceModule, aPackage, targetModules, attribElem);
+		}
+	}
+
+	/**
+	 * Node in the tree of CPListElement et al, representing an add-opens module directive.
+	 */
+	static class ModuleAddOpens extends ModuleAddExpose {
+		public ModuleAddOpens(String sourceModule, String aPackage, String targetModules, CPListElementAttribute attribElem) {
+			super(sourceModule, aPackage, targetModules, attribElem);
+		}
+	}
+
+	/**
 	 * Node in the tree of CPListElement et al, representing an add-reads module directive.
 	 */
 	static class ModuleAddReads extends ModuleEncapsulationDetail {
@@ -194,6 +311,42 @@
 		}
 
 		@Override
+		public boolean affects(String module) {
+			return module.equals(fSourceModule);
+		}
+
+		@Override
+		public int hashCode() {
+			final int prime= 31;
+			int result= 1;
+			result= prime * result + ((fSourceModule == null) ? 0 : fSourceModule.hashCode());
+			result= prime * result + ((fTargetModule == null) ? 0 : fTargetModule.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;
+			ModuleAddReads other= (ModuleAddReads) obj;
+			if (fSourceModule == null) {
+				if (other.fSourceModule != null)
+					return false;
+			} else if (!fSourceModule.equals(other.fSourceModule))
+				return false;
+			if (fTargetModule == null) {
+				if (other.fTargetModule != null)
+					return false;
+			} else if (!fTargetModule.equals(other.fTargetModule))
+				return false;
+			return true;
+		}
+
+		@Override
 		public String toString() {
 			return fSourceModule+'='+fTargetModule;
 		}
@@ -219,8 +372,14 @@
 			fAttribElem= attribElem;
 		}
 		@Override
+		public boolean affects(String module) {
+			return false; // no change on the module, just on the module graph / set of root modules
+		}
+		@Override
 		public String toString() {
 			return String.join(",", fExplicitlyIncludedModules); //$NON-NLS-1$
 		}
 	}
+
+	public abstract boolean affects(String module);
 }
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModulePatchSourceSelectionDialog.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModulePatchSourceSelectionDialog.java
new file mode 100644
index 0000000..13a23ad
--- /dev/null
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModulePatchSourceSelectionDialog.java
@@ -0,0 +1,206 @@
+/*******************************************************************************
+ * Copyright (c) 2019 GK Software SE, and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Stephan Herrmann - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.internal.ui.wizards.buildpaths;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import org.eclipse.core.runtime.CoreException;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.ResourcesPlugin;
+
+import org.eclipse.jface.dialogs.TrayDialog;
+import org.eclipse.jface.layout.PixelConverter;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerComparator;
+
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IModuleDescription;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+
+import org.eclipse.jdt.ui.JavaElementLabelProvider;
+
+import org.eclipse.jdt.internal.ui.JavaPlugin;
+import org.eclipse.jdt.internal.ui.wizards.NewWizardMessages;
+import org.eclipse.jdt.internal.ui.wizards.dialogfields.TreeListDialogField;
+
+public class ModulePatchSourceSelectionDialog extends TrayDialog {
+
+	/**
+	 * Selective tree showing only java projects and their source folders.
+	 */
+	class SourcesContentProvider implements ITreeContentProvider {
+		@Override
+		public Object[] getElements(Object inputElement) {
+			if (inputElement instanceof List<?>)
+				return ((List<?>) inputElement).toArray();
+			return null;
+		}
+		@Override
+		public Object[] getChildren(Object parentElement) {
+			if (parentElement instanceof IJavaProject) {
+				List<IPackageFragmentRoot> sourceRoots= new ArrayList<>();
+				IJavaProject parentProject= (IJavaProject) parentElement;
+				collectSourceFolders(sourceRoots, parentProject);
+				return sourceRoots.toArray();
+			}
+			return null;
+		}
+		@Override
+		public Object getParent(Object element) {
+			if (element instanceof IPackageFragmentRoot) {
+				return ((IPackageFragmentRoot) element).getJavaProject();
+			}
+			return null;
+		}
+		@Override
+		public boolean hasChildren(Object element) {
+			if (element instanceof IJavaProject) {
+				return getChildren(element) != null;
+			}
+			return false;
+		}
+	}
+
+	class ContextProjectFirstComparator extends ViewerComparator {
+		@Override
+		public int compare(Viewer viewer, Object e1, Object e2) {
+			if (e1.equals(fContextProject)) {
+				return -1;
+			}
+			if (e1.equals(fContextProject)) {
+				return 1;
+			}
+			return super.compare(viewer, e1, e2);
+		}
+	}
+
+	private IModuleDescription fFocusModule;
+	private List<IJavaProject> fProjects;
+	private IJavaProject fContextProject;
+
+	private TreeListDialogField<IJavaProject> fTreeField;
+	private TreeViewer fViewer;
+	private List<IPackageFragmentRoot> fResult;
+
+	protected ModulePatchSourceSelectionDialog(Shell shell, IModuleDescription focusModule, IJavaProject contextProject) {
+		super(shell);
+		fFocusModule= focusModule;
+		fContextProject= contextProject;
+		findProjects();
+	}
+
+	private void  findProjects() {
+		fProjects= new ArrayList<>();
+		IProject toSkip= null;
+		IPackageFragmentRoot focusRoot= (IPackageFragmentRoot) fFocusModule.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
+		try {
+			if (focusRoot != null) {
+				if (focusRoot.getKind() == IPackageFragmentRoot.K_SOURCE) {
+					toSkip= focusRoot.getJavaProject().getProject();
+				}
+			} else if (fFocusModule.isAutoModule()) {
+				if (fFocusModule.getJavaProject() != null) {
+					toSkip= fFocusModule.getJavaProject().getProject();
+				}
+			}
+		} catch (JavaModelException e1) {
+			JavaPlugin.log(e1.getStatus());
+		}
+		for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) {
+			if (toSkip != null && toSkip.equals(project)) {
+				continue;
+			}
+			try {
+				if (project.hasNature(JavaCore.NATURE_ID)) {
+					IJavaProject jProj= JavaCore.create(project);
+					fProjects.add(jProj);
+				}
+			} catch (CoreException e) {
+				JavaPlugin.log(e.getStatus());
+			}
+		}
+	}
+
+	static void collectSourceFolders(List<IPackageFragmentRoot> sourceRoots, IJavaProject project) {
+		try {
+			for (IClasspathEntry entry : project.getRawClasspath()) {
+				if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
+					for (IPackageFragmentRoot packageFragmentRoot : project.findPackageFragmentRoots(entry)) {
+						sourceRoots.add(packageFragmentRoot);
+					}
+				}
+			}
+		} catch (JavaModelException e) {
+			JavaPlugin.log(e.getStatus());
+		}
+	}
+
+	@Override
+	protected Control createDialogArea(Composite parent) {
+		Composite composite= (Composite) super.createDialogArea(parent);
+		Label message= new Label(composite, SWT.NONE);
+		message.setText(NewWizardMessages.ModuleSelectionDialog_addSystemModules_message);
+		
+		TreeViewer treeViewer= new TreeViewer(composite, SWT.MULTI | SWT.BORDER);
+		treeViewer.setContentProvider(new SourcesContentProvider());
+		treeViewer.setLabelProvider(new JavaElementLabelProvider());
+		treeViewer.setComparator(new ContextProjectFirstComparator());
+		treeViewer.addSelectionChangedListener(this::selectionChanged);
+		treeViewer.setInput(fProjects);
+
+		PixelConverter converter= new PixelConverter(parent);
+		GridData gd= new GridData(SWT.FILL, SWT.FILL, true, true);
+		gd.widthHint= converter.convertWidthInCharsToPixels(50);
+		gd.heightHint= converter.convertHeightInCharsToPixels(20);
+		treeViewer.getControl().setLayoutData(gd);
+
+		fViewer= treeViewer;
+		return composite;
+	}
+	
+	void selectionChanged(SelectionChangedEvent event) {
+		IStructuredSelection selection= event.getStructuredSelection();
+		fResult= new ArrayList<>();
+		for (Iterator<Object> iterator= selection.iterator(); iterator.hasNext(); ) {
+			Object object= iterator.next();
+			if (object instanceof IPackageFragmentRoot) {
+				fResult.add((IPackageFragmentRoot) object);
+			} else if (object instanceof IJavaProject) {
+				collectSourceFolders(fResult, (IJavaProject) object);
+			}
+		}
+	}
+
+	public List<IPackageFragmentRoot> getResult() {
+		return fResult;
+	}
+}
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleSelectionDialog.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleSelectionDialog.java
new file mode 100644
index 0000000..ac00b69
--- /dev/null
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/wizards/buildpaths/ModuleSelectionDialog.java
@@ -0,0 +1,238 @@
+/*******************************************************************************
+ * Copyright (c) 2019 GK Software SE, and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Stephan Herrmann - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.internal.ui.wizards.buildpaths;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.TrayDialog;
+import org.eclipse.jface.layout.PixelConverter;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.window.Window;
+
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IModuleDescription;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.search.IJavaSearchConstants;
+import org.eclipse.jdt.core.search.SearchEngine;
+import org.eclipse.jdt.core.search.SearchMatch;
+import org.eclipse.jdt.core.search.SearchParticipant;
+import org.eclipse.jdt.core.search.SearchPattern;
+import org.eclipse.jdt.core.search.SearchRequestor;
+
+import org.eclipse.jdt.internal.ui.wizards.NewWizardMessages;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleDependenciesList.ModuleKind;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleDependenciesList.ModulesLabelProvider;
+import org.eclipse.jdt.internal.ui.wizards.buildpaths.ModuleDialog.ListContentProvider;
+
+public class ModuleSelectionDialog extends TrayDialog {
+
+	// widgets:
+	private TableViewer fViewer;
+	private Button fOkButton;
+	private Runnable fFlipMessage; // may show a wait message first, use this to flip to the normal message
+	
+	boolean fInSetSelection= false; // to avoid re-entrance -> StackOverflow
+
+	// input data:
+	private IJavaProject fJavaProject;
+	private IClasspathEntry fJREEntry;
+
+	// internal storage and one client-provided function:
+	private Set<String> fAllIncluded; 		// transitive closure over modules already shown
+	private List<String> fAvailableModules;	// additional modules outside fAllIncluded 
+	private Function<List<String>, Set<String>> fClosureComputation;
+	private Map<String,IModuleDescription> fModulesByName= new HashMap<>();
+
+	// result:
+	private List<String> fSelectedModules;
+
+	/**
+	 * Let the user select among available modules that are not yet included (explicitly or implicitly).
+	 * @param shell for showing the dialog
+	 * @param javaProject the java project whose build path is being configured
+	 * @param jreEntry a classpath entry representing the JRE system library
+	 * @param shownModules set of modules already shown in the LHS list ({@link ModuleDependenciesList})
+	 * @param closureComputation a function from module names to their full transitive closure over 'requires'. 
+	 */
+	protected ModuleSelectionDialog(Shell shell, IJavaProject javaProject, IClasspathEntry jreEntry, List<String> shownModules, Function<List<String>, Set<String>> closureComputation) {
+		super(shell);
+		fJavaProject= javaProject;
+		fJREEntry= jreEntry;
+		fAllIncluded= closureComputation.apply(shownModules);
+		fClosureComputation= closureComputation;
+		if (jreEntry != null) {  // searching only modules from this JRE entry (quick)
+			List<String> result= new ArrayList<>();
+			for (IPackageFragmentRoot root : fJavaProject.findUnfilteredPackageFragmentRoots(fJREEntry)) {
+				checkAddModule(result, root.getModuleDescription());
+			}
+			result.sort(String::compareTo);
+			fAvailableModules= result;
+		} else {  // searching all modules in the workspace (slow)
+			new Job("Searching modules in workspace") {
+				@Override
+				public IStatus run(IProgressMonitor monitor) {
+					try {
+						fAvailableModules= searchAvailableModules(monitor);
+						if (getReturnCode() == Window.CANCEL) {
+							return Status.CANCEL_STATUS;
+						}
+						shell.getDisplay().asyncExec(() -> {
+							if (fFlipMessage != null) {
+								fFlipMessage.run();
+							}
+							fViewer.setInput(fAvailableModules);
+							fViewer.refresh();
+						});
+					} catch (CoreException e) {
+						return e.getStatus();
+					}
+					return Status.OK_STATUS;
+				}
+			}.schedule();
+		}
+	}
+
+	private List<String> searchAvailableModules(IProgressMonitor monitor) throws CoreException {
+		List<String> result= new ArrayList<>();
+		SearchPattern pattern= SearchPattern.createPattern("*", IJavaSearchConstants.MODULE, IJavaSearchConstants.DECLARATIONS, SearchPattern.R_PATTERN_MATCH|SearchPattern.R_CASE_SENSITIVE); //$NON-NLS-1$
+		SearchRequestor requestor= new SearchRequestor() {
+			@Override
+			public void acceptSearchMatch(SearchMatch match) throws CoreException {
+				Object element= match.getElement();
+				if (element instanceof IModuleDescription) {
+					checkAddModule(result, (IModuleDescription) element);
+				}
+			}
+		};
+		SearchParticipant[] participants= new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() };
+		new SearchEngine().search(pattern, participants, SearchEngine.createWorkspaceScope(), requestor, monitor);
+		if (getReturnCode() == Window.CANCEL) { // TODO: should cancelPressed() actively abort the search?
+			return Collections.emptyList();
+		}
+		result.sort(String::compareTo);
+		return result;
+	}
+
+	void checkAddModule(List<String> result, IModuleDescription moduleDescription) {
+		if (moduleDescription == null)
+			return;
+		if (!fAllIncluded.contains(moduleDescription.getElementName())) {
+			result.add(moduleDescription.getElementName());
+		}
+		fModulesByName.put(moduleDescription.getElementName(), moduleDescription); // hold on to module description to be used for getResult()
+	}
+
+	@Override
+	protected void configureShell(Shell newShell) {
+		super.configureShell(newShell);
+		newShell.setText(NewWizardMessages.ModuleSelectionDialog_addSystemModules_title);
+// TODO:
+//		PlatformUI.getWorkbench().getHelpSystem().setHelp(newShell, IJavaHelpContextIds.MODULE_DIALOG);
+	}
+	
+	@Override
+	protected int getShellStyle() {
+		return super.getShellStyle() | SWT.RESIZE;
+	}
+
+	@Override
+	protected Control createDialogArea(Composite parent) {
+		Composite composite= (Composite) super.createDialogArea(parent);
+		Label message= new Label(composite, SWT.NONE);
+		
+		TableViewer tableViewer= new TableViewer(composite, SWT.MULTI | SWT.BORDER);
+		tableViewer.setContentProvider(new ListContentProvider());
+		tableViewer.setLabelProvider(new ModulesLabelProvider(s -> ModuleKind.System));
+		tableViewer.addSelectionChangedListener(this::selectionChanged);
+
+		PixelConverter converter= new PixelConverter(parent);
+		GridData gd= new GridData(SWT.FILL, SWT.FILL, true, true);
+		gd.widthHint= converter.convertWidthInCharsToPixels(50);
+		gd.heightHint= converter.convertHeightInCharsToPixels(20);
+		tableViewer.getControl().setLayoutData(gd);
+
+		if (fAvailableModules == null) {
+			message.setText("Searching modules in workspace ...");
+			fFlipMessage= () ->  {
+				message.setText(NewWizardMessages.ModuleSelectionDialog_addSystemModules_message);
+			};
+		} else {
+			tableViewer.setInput(fAvailableModules);
+			message.setText(NewWizardMessages.ModuleSelectionDialog_addSystemModules_message);
+		}
+		fViewer= tableViewer;
+		return composite;
+	}
+
+	@Override
+	protected void createButtonsForButtonBar(Composite parent) {
+		fOkButton = createButton(parent, IDialogConstants.OK_ID,
+				NewWizardMessages.ModuleSelectionDialog_add_button, true);
+		createButton(parent, IDialogConstants.CANCEL_ID,
+				IDialogConstants.CANCEL_LABEL, false);
+	}
+
+	private void selectionChanged(SelectionChangedEvent e) {
+		IStructuredSelection selection= e.getStructuredSelection();
+		if (selection == null || selection.isEmpty()) {
+			fOkButton.setEnabled(false);
+			return;
+		}
+		List<String> selectedNames= selection.toList();
+		Set<String> closure= fClosureComputation.apply(selectedNames);
+		if (closure.size() > selectedNames.size()) {
+			// select all members of the closure:
+			if (!fInSetSelection) {
+				fInSetSelection= true;
+				fViewer.setSelection(new StructuredSelection(new ArrayList<>(closure)));
+				fInSetSelection= false;
+			}
+		}
+		fOkButton.setEnabled(true);
+		fSelectedModules= new ArrayList<>(closure); // remember result
+	}
+
+	public List<IModuleDescription> getResult() {
+		return fSelectedModules.stream()
+				.filter(m -> !fAllIncluded.contains(m)) // skip modules that are already included
+				.map(fModulesByName::get)
+				.collect(Collectors.toList());
+	}
+}