Bug 576106 - Add initial support for type hierarchy
diff --git a/org.eclipse.handly.examples.adapter.ui/icons/eview16/type_hierarchy.png b/org.eclipse.handly.examples.adapter.ui/icons/eview16/type_hierarchy.png
new file mode 100644
index 0000000..28231da
--- /dev/null
+++ b/org.eclipse.handly.examples.adapter.ui/icons/eview16/type_hierarchy.png
Binary files differ
diff --git a/org.eclipse.handly.examples.adapter.ui/icons/eview16/type_hierarchy@2x.png b/org.eclipse.handly.examples.adapter.ui/icons/eview16/type_hierarchy@2x.png
new file mode 100644
index 0000000..8b2a9d5
--- /dev/null
+++ b/org.eclipse.handly.examples.adapter.ui/icons/eview16/type_hierarchy@2x.png
Binary files differ
diff --git a/org.eclipse.handly.examples.adapter.ui/plugin.xml b/org.eclipse.handly.examples.adapter.ui/plugin.xml
index af3e7ed..2db6b2d 100644
--- a/org.eclipse.handly.examples.adapter.ui/plugin.xml
+++ b/org.eclipse.handly.examples.adapter.ui/plugin.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?eclipse version="3.4"?>
 <!--
-    Copyright (c) 2015, 2018 1C-Soft LLC.
+    Copyright (c) 2015, 2021 1C-Soft LLC.
 
     This program and the accompanying materials are made available under
     the terms of the Eclipse Public License 2.0 which is available at
@@ -34,11 +34,20 @@
             id="org.eclipse.handly.examples.adapter.ui.JavaCallHierarchyView"
             category="org.eclipse.handly.examples"
             class="org.eclipse.handly.internal.examples.adapter.ui.callhierarchy.JavaCallHierarchyView"
-            name="Method Calls"
+            name="Call Hierarchy"
             icon="icons/eview16/call_hierarchy.png"
             allowMultiple="true"
             restorable="true">
       </view>
+      <view
+            id="org.eclipse.handly.examples.adapter.ui.JavaTypeHierarchyView"
+            category="org.eclipse.handly.examples"
+            class="org.eclipse.handly.internal.examples.adapter.ui.typehierarchy.JavaTypeHierarchyView"
+            name="Type Hierarchy"
+            icon="icons/eview16/type_hierarchy.png"
+            allowMultiple="false"
+            restorable="true">
+      </view>
    </extension>
    <extension
          point="org.eclipse.search.searchResultViewPages">
diff --git a/org.eclipse.handly.examples.adapter.ui/src/org/eclipse/handly/internal/examples/adapter/ui/JavaOutlinePage.java b/org.eclipse.handly.examples.adapter.ui/src/org/eclipse/handly/internal/examples/adapter/ui/JavaOutlinePage.java
index 64bc3bc..4b6ce82 100644
--- a/org.eclipse.handly.examples.adapter.ui/src/org/eclipse/handly/internal/examples/adapter/ui/JavaOutlinePage.java
+++ b/org.eclipse.handly.examples.adapter.ui/src/org/eclipse/handly/internal/examples/adapter/ui/JavaOutlinePage.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2015, 2018 1C-Soft LLC.
+ * Copyright (c) 2015, 2021 1C-Soft LLC.
  *
  * This program and the accompanying materials are made available under
  * the terms of the Eclipse Public License 2.0 which is available at
@@ -15,6 +15,7 @@
 import org.eclipse.handly.examples.adapter.JavaModelAdapter;
 import org.eclipse.handly.internal.examples.adapter.ui.callhierarchy.OpenCallHierarchyAction;
 import org.eclipse.handly.internal.examples.adapter.ui.search.FindReferencesAction;
+import org.eclipse.handly.internal.examples.adapter.ui.typehierarchy.OpenTypeHierarchyAction;
 import org.eclipse.handly.model.IElementChangeListener;
 import org.eclipse.handly.model.adapter.DefaultContentAdapter;
 import org.eclipse.handly.model.adapter.IContentAdapter;
@@ -49,6 +50,8 @@
     private static final String GROUP_OPEN = "group.open"; //$NON-NLS-1$
     private static final String GROUP_SEARCH = "group.search"; //$NON-NLS-1$
 
+    private final OpenTypeHierarchyAction openTypeHierarchyAction =
+        new OpenTypeHierarchyAction();
     private final OpenCallHierarchyAction openCallHierarchyAction =
         new OpenCallHierarchyAction();
     private final FindReferencesAction findReferencesAction =
@@ -118,6 +121,10 @@
                 IStructuredSelection selection =
                     (IStructuredSelection)getSelection();
 
+                openTypeHierarchyAction.selectionChanged(selection);
+                if (openTypeHierarchyAction.isEnabled())
+                    manager.appendToGroup(GROUP_OPEN, openTypeHierarchyAction);
+
                 openCallHierarchyAction.selectionChanged(selection);
                 if (openCallHierarchyAction.isEnabled())
                     manager.appendToGroup(GROUP_OPEN, openCallHierarchyAction);
diff --git a/org.eclipse.handly.examples.adapter.ui/src/org/eclipse/handly/internal/examples/adapter/ui/callhierarchy/OpenCallHierarchyAction.java b/org.eclipse.handly.examples.adapter.ui/src/org/eclipse/handly/internal/examples/adapter/ui/callhierarchy/OpenCallHierarchyAction.java
index c1c486a..06a3be3 100644
--- a/org.eclipse.handly.examples.adapter.ui/src/org/eclipse/handly/internal/examples/adapter/ui/callhierarchy/OpenCallHierarchyAction.java
+++ b/org.eclipse.handly.examples.adapter.ui/src/org/eclipse/handly/internal/examples/adapter/ui/callhierarchy/OpenCallHierarchyAction.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2018, 2019 1C-Soft LLC.
+ * Copyright (c) 2018, 2021 1C-Soft LLC.
  *
  * This program and the accompanying materials are made available under
  * the terms of the Eclipse Public License 2.0 which is available at
@@ -36,7 +36,7 @@
      */
     public OpenCallHierarchyAction()
     {
-        super("Show Calls");
+        super("Open Call Hierarchy");
     }
 
     @Override
diff --git a/org.eclipse.handly.examples.adapter.ui/src/org/eclipse/handly/internal/examples/adapter/ui/typehierarchy/JavaTypeHierarchyView.java b/org.eclipse.handly.examples.adapter.ui/src/org/eclipse/handly/internal/examples/adapter/ui/typehierarchy/JavaTypeHierarchyView.java
new file mode 100644
index 0000000..51b7ee6
--- /dev/null
+++ b/org.eclipse.handly.examples.adapter.ui/src/org/eclipse/handly/internal/examples/adapter/ui/typehierarchy/JavaTypeHierarchyView.java
@@ -0,0 +1,455 @@
+/*******************************************************************************
+ * Copyright (c) 2021 1C-Soft LLC.
+ *
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Vladimir Piskarev (1C) - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.handly.internal.examples.adapter.ui.typehierarchy;
+
+import static org.eclipse.jdt.ui.JavaElementLabels.APPEND_ROOT_PATH;
+import static org.eclipse.jdt.ui.JavaElementLabels.COLORIZE;
+import static org.eclipse.jdt.ui.JavaElementLabels.DEFAULT_QUALIFIED;
+import static org.eclipse.jdt.ui.JavaElementLabels.M_APP_RETURNTYPE;
+import static org.eclipse.jdt.ui.JavaElementLabels.M_APP_TYPE_PARAMETERS;
+import static org.eclipse.jdt.ui.JavaElementLabels.M_PARAMETER_NAMES;
+import static org.eclipse.jdt.ui.JavaElementLabels.M_PARAMETER_TYPES;
+import static org.eclipse.jdt.ui.JavaElementLabels.P_COMPRESSED;
+import static org.eclipse.jdt.ui.JavaElementLabels.T_POST_QUALIFIED;
+import static org.eclipse.jdt.ui.JavaElementLabels.T_TYPE_PARAMETERS;
+
+import java.text.MessageFormat;
+import java.util.EnumSet;
+import java.util.Objects;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.handly.context.IContext;
+import org.eclipse.handly.internal.examples.adapter.ui.Activator;
+import org.eclipse.handly.internal.examples.adapter.ui.JavaEditorUtility;
+import org.eclipse.handly.ui.EditorOpener;
+import org.eclipse.handly.ui.typehierarchy.TypeHierarchyKind;
+import org.eclipse.handly.ui.typehierarchy.TypeHierarchyViewPart;
+import org.eclipse.handly.ui.viewer.DeferredTreeContentProvider;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IMethod;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.ui.JavaElementLabelProvider;
+import org.eclipse.jdt.ui.JavaElementLabels;
+import org.eclipse.jface.action.IStatusLineManager;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.AbstractTreeViewer;
+import org.eclipse.jface.viewers.ArrayContentProvider;
+import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.StyledString;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.IWorkbenchPartSite;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.progress.IElementCollector;
+
+/**
+ * Java-specific type hierarchy view.
+ */
+public class JavaTypeHierarchyView
+    extends TypeHierarchyViewPart
+{
+    /**
+     * The view ID.
+     */
+    public static final String ID = Activator.PLUGIN_ID
+        + ".JavaTypeHierarchyView"; //$NON-NLS-1$
+
+    private final TypeHierarchyHolder hierarchyHolder =
+        new TypeHierarchyHolder();
+    private TableViewer methodViewer;
+
+    /**
+     * Creates a new <code>JavaTypeHierarchyView</code>.
+     */
+    public JavaTypeHierarchyView()
+    {
+        super(EnumSet.of(TypeHierarchyKind.SUPERTYPES,
+            TypeHierarchyKind.SUBTYPES));
+    }
+
+    @Override
+    public void createPartControl(Composite parent)
+    {
+        super.createPartControl(parent);
+
+        methodViewer = new TableViewer(getSashForm(), SWT.MULTI);
+        methodViewer.setContentProvider(new ArrayContentProvider());
+        methodViewer.setLabelProvider(new DelegatingStyledCellLabelProvider(
+            new MethodLabelProvider()));
+        methodViewer.addSelectionChangedListener(e -> methodSelectionChanged(
+            e.getSelection()));
+
+        new OpenEditorHelper(methodViewer);
+    }
+
+    @Override
+    public boolean supportsLayoutMode(int layoutMode)
+    {
+        return layoutMode == SWT.VERTICAL || layoutMode == SWT.HORIZONTAL
+            || layoutMode == (SWT.VERTICAL | SWT.HORIZONTAL)
+            || layoutMode == SWT.NONE;
+    }
+
+    @Override
+    public String getTitleToolTip()
+    {
+        return getPartName() + " (Handly Adapter Example)";
+    }
+
+    @Override
+    public void setInputElements(Object[] elements)
+    {
+        if (elements.length > 1)
+            throw new IllegalArgumentException();
+
+        super.setInputElements(elements);
+    }
+
+    @Override
+    protected void onInputElementsChanged(Object[] oldInputElements,
+        Object[] newInputElements)
+    {
+        IType inputType = newInputElements.length > 0
+            ? (IType)newInputElements[0] : null;
+        hierarchyHolder.setInputType(inputType);
+
+        super.onInputElementsChanged(oldInputElements, newInputElements);
+    }
+
+    @Override
+    protected boolean isPossibleInputElement(Object element)
+    {
+        return element instanceof IType;
+    }
+
+    @Override
+    protected HistoryEntry createHistoryEntry(Object[] inputElements)
+    {
+        return new TypeHistoryEntry(inputElements);
+    }
+
+    @Override
+    protected EditorOpener createEditorOpener()
+    {
+        return new EditorOpener(getSite().getPage(),
+            JavaEditorUtility.INSTANCE);
+    }
+
+    @Override
+    protected String computeContentDescription()
+    {
+        Object[] elements = getInputElements();
+        if (elements.length == 0)
+            return ""; //$NON-NLS-1$
+        return MessageFormat.format("''{0}''", JavaElementLabels.getTextLabel(
+            elements[0], T_POST_QUALIFIED | T_TYPE_PARAMETERS | P_COMPRESSED));
+    }
+
+    @Override
+    protected void configureHierarchyViewer(TreeViewer viewer,
+        TypeHierarchyKind kind)
+    {
+        viewer.setUseHashlookup(true);
+        viewer.setAutoExpandLevel(2);
+        viewer.setContentProvider(new TypeHierarchyContentProvider(viewer, kind,
+            hierarchyHolder, getSite()));
+        viewer.setLabelProvider(new JavaElementLabelProvider());
+    }
+
+    @Override
+    protected void setHierarchyViewerInput(TreeViewer viewer,
+        TypeHierarchyKind kind)
+    {
+        super.setHierarchyViewerInput(viewer, kind);
+
+        IType inputType = hierarchyHolder.getInputType();
+        if (inputType != null)
+            viewer.setSelection(new StructuredSelection(inputType), true);
+    }
+
+    @Override
+    protected void refresh(IContext context)
+    {
+        hierarchyHolder.setInputType(hierarchyHolder.getInputType()); // reset type hierarchy
+
+        super.refresh(context);
+    }
+
+    @Override
+    protected void updateStatusLine(IStatusLineManager manager,
+        IStructuredSelection selection)
+    {
+        super.updateStatusLine(manager, selection);
+
+        if (selection.size() == 1)
+        {
+            Object element = selection.getFirstElement();
+            if (element instanceof IJavaElement)
+            {
+                manager.setMessage(JavaElementLabels.getTextLabel(element,
+                    DEFAULT_QUALIFIED | APPEND_ROOT_PATH | M_PARAMETER_TYPES
+                        | M_PARAMETER_NAMES | M_APP_RETURNTYPE
+                        | T_TYPE_PARAMETERS));
+
+                if (!((IJavaElement)element).exists())
+                {
+                    manager.setErrorMessage(MessageFormat.format(
+                        "''{0}'' no longer exists. Try to refresh the view",
+                        JavaElementLabels.getTextLabel(element, 0)));
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void onHierarchySelectionChanged(ISelection selection,
+        TypeHierarchyKind kind)
+    {
+        super.onHierarchySelectionChanged(selection, kind);
+
+        Object element = getSelectedElement(selection);
+
+        IMethod[] methods = null;
+        if (element instanceof IType)
+        {
+            try
+            {
+                methods = ((IType)element).getMethods();
+            }
+            catch (JavaModelException e)
+            {
+                if (!e.isDoesNotExist())
+                    Activator.logError(e);
+            }
+        }
+
+        methodViewer.setInput(methods);
+
+        if (methods != null && methods.length > 0)
+            methodViewer.setSelection(new StructuredSelection(methods[0]),
+                true);
+    }
+
+    private void methodSelectionChanged(ISelection selection)
+    {
+        Object element = getSelectedElement(selection);
+        if (element != null)
+        {
+            if (methodViewer.getControl().isFocusControl())
+            {
+                updateStatusLine(
+                    getViewSite().getActionBars().getStatusLineManager(),
+                    methodViewer.getStructuredSelection());
+
+                try
+                {
+                    revealInEditor(element, false, false);
+                }
+                catch (PartInitException e)
+                {
+                    // cannot happen: may not open a new editor
+                }
+            }
+        }
+    }
+
+    private static Object getSelectedElement(ISelection selection)
+    {
+        if (selection instanceof IStructuredSelection)
+        {
+            IStructuredSelection ss = (IStructuredSelection)selection;
+            if (ss.size() == 1)
+                return ss.getFirstElement();
+        }
+        return null;
+    }
+
+    private static class TypeHierarchyHolder
+    {
+        private IType inputType;
+        private ITypeHierarchy typeHierarchy;
+
+        void setInputType(IType inputType)
+        {
+            this.inputType = inputType;
+            this.typeHierarchy = null;
+        }
+
+        IType getInputType()
+        {
+            return inputType;
+        }
+
+        ITypeHierarchy getTypeHierarchy()
+        {
+            return typeHierarchy;
+        }
+
+        ITypeHierarchy getOrCreateTypeHierarchy(IProgressMonitor monitor)
+            throws JavaModelException
+        {
+            if (inputType != null && typeHierarchy == null)
+                typeHierarchy = inputType.newTypeHierarchy(monitor);
+            return typeHierarchy;
+        }
+    }
+
+    private static class TypeHierarchyContentProvider
+        extends DeferredTreeContentProvider
+    {
+        private final TypeHierarchyKind hierarchyKind;
+        private final TypeHierarchyHolder hierarchyHolder;
+
+        TypeHierarchyContentProvider(AbstractTreeViewer viewer,
+            TypeHierarchyKind hierarchyKind,
+            TypeHierarchyHolder hierarchyHolder, IWorkbenchPartSite site)
+        {
+            super(viewer, site);
+            this.hierarchyKind = Objects.requireNonNull(hierarchyKind);
+            this.hierarchyHolder = Objects.requireNonNull(hierarchyHolder);
+        }
+
+        @Override
+        public Object[] getElements(Object inputElement)
+        {
+            IType inputType = hierarchyHolder.getInputType();
+            if (inputType != null)
+                return new Object[] { inputType };
+            return new Object[0];
+        }
+
+        @Override
+        public Object[] getChildren(Object parentElement)
+        {
+            if (!(parentElement instanceof IType))
+                return new Object[0];
+            ITypeHierarchy typeHierarchy = hierarchyHolder.getTypeHierarchy();
+            if (typeHierarchy != null)
+                return getChildren(typeHierarchy, parentElement);
+            return getDeferredTreeContentManager().getChildren(parentElement);
+        }
+
+        @Override
+        public Object getParent(Object element)
+        {
+            return null;
+        }
+
+        @Override
+        public boolean hasChildren(Object element)
+        {
+            if (!(element instanceof IType))
+                return false;
+            ITypeHierarchy typeHierarchy = hierarchyHolder.getTypeHierarchy();
+            if (typeHierarchy != null)
+                return getChildren(typeHierarchy, element).length > 0;
+            return true;
+        }
+
+        @Override
+        protected void fetchDeferredChildren(Object parentElement,
+            IElementCollector collector, IProgressMonitor monitor)
+        {
+            ITypeHierarchy typeHierarchy = null;
+            try
+            {
+                typeHierarchy = hierarchyHolder.getOrCreateTypeHierarchy(
+                    monitor);
+            }
+            catch (JavaModelException e)
+            {
+                Activator.logError(e);
+            }
+            if (typeHierarchy != null)
+            {
+                collector.add(getChildren(typeHierarchy, parentElement), null);
+            }
+            collector.done();
+        }
+
+        private Object[] getChildren(ITypeHierarchy typeHierarchy,
+            Object parentElement)
+        {
+            IType type = (IType)parentElement;
+            return (hierarchyKind == TypeHierarchyKind.SUBTYPES)
+                ? typeHierarchy.getSubtypes(type) : typeHierarchy.getSupertypes(
+                    type);
+        }
+    }
+
+    private static class MethodLabelProvider
+        extends JavaElementLabelProvider
+    {
+        private static final long M_FLAGS = M_PARAMETER_TYPES
+            | M_APP_TYPE_PARAMETERS | M_APP_RETURNTYPE;
+
+        @Override
+        public String getText(Object element)
+        {
+            if (element instanceof IMethod)
+                return JavaElementLabels.getTextLabel(element, M_FLAGS);
+
+            return super.getText(element);
+        }
+
+        @Override
+        public StyledString getStyledText(Object element)
+        {
+            if (element instanceof IMethod)
+                return JavaElementLabels.getStyledTextLabel(element, M_FLAGS
+                    | COLORIZE);
+
+            return super.getStyledText(element);
+        }
+    }
+
+    private static class TypeHistoryEntry
+        extends HistoryEntry
+    {
+        TypeHistoryEntry(Object[] inputElements)
+        {
+            super(inputElements);
+        }
+
+        @SuppressWarnings("restriction")
+        @Override
+        public ImageDescriptor getImageDescriptor()
+        {
+            IJavaElement firstElement = (IJavaElement)getInputElements()[0];
+            org.eclipse.jdt.internal.ui.viewsupport.JavaElementImageProvider imageProvider =
+                new org.eclipse.jdt.internal.ui.viewsupport.JavaElementImageProvider();
+            try
+            {
+                return imageProvider.getBaseImageDescriptor(firstElement, 0);
+            }
+            finally
+            {
+                imageProvider.dispose();
+            }
+        }
+
+        @Override
+        protected String getElementLabel(Object element)
+        {
+            return JavaElementLabels.getTextLabel(element, T_POST_QUALIFIED
+                | T_TYPE_PARAMETERS | P_COMPRESSED);
+        }
+    }
+}
diff --git a/org.eclipse.handly.examples.adapter.ui/src/org/eclipse/handly/internal/examples/adapter/ui/typehierarchy/OpenTypeHierarchyAction.java b/org.eclipse.handly.examples.adapter.ui/src/org/eclipse/handly/internal/examples/adapter/ui/typehierarchy/OpenTypeHierarchyAction.java
new file mode 100644
index 0000000..ed99254
--- /dev/null
+++ b/org.eclipse.handly.examples.adapter.ui/src/org/eclipse/handly/internal/examples/adapter/ui/typehierarchy/OpenTypeHierarchyAction.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Copyright (c) 2021 1C-Soft LLC.
+ *
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Vladimir Piskarev (1C) - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.handly.internal.examples.adapter.ui.typehierarchy;
+
+import org.eclipse.handly.internal.examples.adapter.ui.Activator;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.IViewPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.actions.BaseSelectionListenerAction;
+
+/**
+ * An action that opens the {@link JavaTypeHierarchyView} for the current
+ * selection.
+ */
+public final class OpenTypeHierarchyAction
+    extends BaseSelectionListenerAction
+{
+    /**
+     * Creates a new <code>OpenTypeHierarchyAction</code>.
+     */
+    public OpenTypeHierarchyAction()
+    {
+        super("Open Type Hierarchy");
+    }
+
+    @Override
+    public void run()
+    {
+        IWorkbenchWindow window =
+            PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+        if (window == null)
+            return;
+        IWorkbenchPage page = window.getActivePage();
+        if (page == null)
+            return;
+        try
+        {
+            IViewPart view = page.showView(JavaTypeHierarchyView.ID);
+            if (view instanceof JavaTypeHierarchyView)
+                ((JavaTypeHierarchyView)view).setInputElements(
+                    getStructuredSelection().toArray());
+        }
+        catch (PartInitException e)
+        {
+            Activator.logError(e);
+        }
+    }
+
+    @Override
+    protected boolean updateSelection(IStructuredSelection selection)
+    {
+        if (selection.isEmpty() || selection.size() > 1)
+            return false;
+
+        return selection.getFirstElement() instanceof IType;
+    }
+}
diff --git a/org.eclipse.handly.ui/META-INF/MANIFEST.MF b/org.eclipse.handly.ui/META-INF/MANIFEST.MF
index c61e4de..4864ed4 100644
--- a/org.eclipse.handly.ui/META-INF/MANIFEST.MF
+++ b/org.eclipse.handly.ui/META-INF/MANIFEST.MF
@@ -31,5 +31,6 @@
  org.eclipse.handly.ui.search,
  org.eclipse.handly.ui.text.reconciler,
  org.eclipse.handly.ui.texteditor,
+ org.eclipse.handly.ui.typehierarchy,
  org.eclipse.handly.ui.viewer,
  org.eclipse.handly.ui.workingset
diff --git a/org.eclipse.handly.ui/icons/elcl16/ch_automatic.png b/org.eclipse.handly.ui/icons/elcl16/layout_automatic.png
similarity index 100%
rename from org.eclipse.handly.ui/icons/elcl16/ch_automatic.png
rename to org.eclipse.handly.ui/icons/elcl16/layout_automatic.png
Binary files differ
diff --git a/org.eclipse.handly.ui/icons/elcl16/ch_automatic@2x.png b/org.eclipse.handly.ui/icons/elcl16/layout_automatic@2x.png
similarity index 100%
rename from org.eclipse.handly.ui/icons/elcl16/ch_automatic@2x.png
rename to org.eclipse.handly.ui/icons/elcl16/layout_automatic@2x.png
Binary files differ
diff --git a/org.eclipse.handly.ui/icons/elcl16/ch_horizontal.png b/org.eclipse.handly.ui/icons/elcl16/layout_horizontal.png
similarity index 100%
rename from org.eclipse.handly.ui/icons/elcl16/ch_horizontal.png
rename to org.eclipse.handly.ui/icons/elcl16/layout_horizontal.png
Binary files differ
diff --git a/org.eclipse.handly.ui/icons/elcl16/ch_horizontal@2x.png b/org.eclipse.handly.ui/icons/elcl16/layout_horizontal@2x.png
similarity index 100%
rename from org.eclipse.handly.ui/icons/elcl16/ch_horizontal@2x.png
rename to org.eclipse.handly.ui/icons/elcl16/layout_horizontal@2x.png
Binary files differ
diff --git a/org.eclipse.handly.ui/icons/elcl16/layout_single.png b/org.eclipse.handly.ui/icons/elcl16/layout_single.png
new file mode 100644
index 0000000..ff2d582
--- /dev/null
+++ b/org.eclipse.handly.ui/icons/elcl16/layout_single.png
Binary files differ
diff --git a/org.eclipse.handly.ui/icons/elcl16/layout_single@2x.png b/org.eclipse.handly.ui/icons/elcl16/layout_single@2x.png
new file mode 100644
index 0000000..20cc7ec
--- /dev/null
+++ b/org.eclipse.handly.ui/icons/elcl16/layout_single@2x.png
Binary files differ
diff --git a/org.eclipse.handly.ui/icons/elcl16/ch_vertical.png b/org.eclipse.handly.ui/icons/elcl16/layout_vertical.png
similarity index 100%
rename from org.eclipse.handly.ui/icons/elcl16/ch_vertical.png
rename to org.eclipse.handly.ui/icons/elcl16/layout_vertical.png
Binary files differ
diff --git a/org.eclipse.handly.ui/icons/elcl16/ch_vertical@2x.png b/org.eclipse.handly.ui/icons/elcl16/layout_vertical@2x.png
similarity index 100%
rename from org.eclipse.handly.ui/icons/elcl16/ch_vertical@2x.png
rename to org.eclipse.handly.ui/icons/elcl16/layout_vertical@2x.png
Binary files differ
diff --git a/org.eclipse.handly.ui/icons/elcl16/th_subtypes.png b/org.eclipse.handly.ui/icons/elcl16/th_subtypes.png
new file mode 100644
index 0000000..8cf8fd3
--- /dev/null
+++ b/org.eclipse.handly.ui/icons/elcl16/th_subtypes.png
Binary files differ
diff --git a/org.eclipse.handly.ui/icons/elcl16/th_subtypes@2x.png b/org.eclipse.handly.ui/icons/elcl16/th_subtypes@2x.png
new file mode 100644
index 0000000..10bb534
--- /dev/null
+++ b/org.eclipse.handly.ui/icons/elcl16/th_subtypes@2x.png
Binary files differ
diff --git a/org.eclipse.handly.ui/icons/elcl16/th_supertypes.png b/org.eclipse.handly.ui/icons/elcl16/th_supertypes.png
new file mode 100644
index 0000000..5a79120
--- /dev/null
+++ b/org.eclipse.handly.ui/icons/elcl16/th_supertypes.png
Binary files differ
diff --git a/org.eclipse.handly.ui/icons/elcl16/th_supertypes@2x.png b/org.eclipse.handly.ui/icons/elcl16/th_supertypes@2x.png
new file mode 100644
index 0000000..920e7f4
--- /dev/null
+++ b/org.eclipse.handly.ui/icons/elcl16/th_supertypes@2x.png
Binary files differ
diff --git a/org.eclipse.handly.ui/icons/elcl16/th_types.png b/org.eclipse.handly.ui/icons/elcl16/th_types.png
new file mode 100644
index 0000000..7ceec3f
--- /dev/null
+++ b/org.eclipse.handly.ui/icons/elcl16/th_types.png
Binary files differ
diff --git a/org.eclipse.handly.ui/icons/elcl16/th_types@2x.png b/org.eclipse.handly.ui/icons/elcl16/th_types@2x.png
new file mode 100644
index 0000000..b39603c
--- /dev/null
+++ b/org.eclipse.handly.ui/icons/elcl16/th_types@2x.png
Binary files differ
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/internal/ui/Activator.java b/org.eclipse.handly.ui/src/org/eclipse/handly/internal/ui/Activator.java
index 15cbb1c..fe41316 100644
--- a/org.eclipse.handly.ui/src/org/eclipse/handly/internal/ui/Activator.java
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/internal/ui/Activator.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2014, 2020 1C-Soft LLC and others.
+ * Copyright (c) 2014, 2021 1C-Soft LLC and others.
  *
  * This program and the accompanying materials are made available under
  * the terms of the Eclipse Public License 2.0 which is available at
@@ -40,26 +40,34 @@
     public static final String IMG_DLCL_REFRESH = PLUGIN_ID + T_DLCL
         + "refresh.png"; //$NON-NLS-1$
 
-    public static final String IMG_ELCL_CH_AUTOMATIC = PLUGIN_ID + T_ELCL
-        + "ch_automatic.png"; //$NON-NLS-1$
     public static final String IMG_ELCL_CH_CALLEES = PLUGIN_ID + T_ELCL
         + "ch_callees.png"; //$NON-NLS-1$
     public static final String IMG_ELCL_CH_CALLERS = PLUGIN_ID + T_ELCL
         + "ch_callers.png"; //$NON-NLS-1$
-    public static final String IMG_ELCL_CH_HORIZONTAL = PLUGIN_ID + T_ELCL
-        + "ch_horizontal.png"; //$NON-NLS-1$
-    public static final String IMG_ELCL_CH_VERTICAL = PLUGIN_ID + T_ELCL
-        + "ch_vertical.png"; //$NON-NLS-1$
     public static final String IMG_ELCL_EXPANDALL = PLUGIN_ID + T_ELCL
         + "expandall.png"; //$NON-NLS-1$
     public static final String IMG_ELCL_HISTORY_LIST = PLUGIN_ID + T_ELCL
         + "history_list.png"; //$NON-NLS-1$
+    public static final String IMG_ELCL_LAYOUT_AUTOMATIC = PLUGIN_ID + T_ELCL
+        + "layout_automatic.png"; //$NON-NLS-1$
+    public static final String IMG_ELCL_LAYOUT_HORIZONTAL = PLUGIN_ID + T_ELCL
+        + "layout_horizontal.png"; //$NON-NLS-1$
+    public static final String IMG_ELCL_LAYOUT_SINGLE = PLUGIN_ID + T_ELCL
+        + "layout_single.png"; //$NON-NLS-1$
+    public static final String IMG_ELCL_LAYOUT_VERTICAL = PLUGIN_ID + T_ELCL
+        + "layout_vertical.png"; //$NON-NLS-1$
     public static final String IMG_ELCL_LEXICAL_SORT = PLUGIN_ID + T_ELCL
         + "lexical_sort.png"; //$NON-NLS-1$
     public static final String IMG_ELCL_PIN_VIEW = PLUGIN_ID + T_ELCL
         + "pin_view.png"; //$NON-NLS-1$
     public static final String IMG_ELCL_REFRESH = PLUGIN_ID + T_ELCL
         + "refresh.png"; //$NON-NLS-1$
+    public static final String IMG_ELCL_TH_SUPERTYPES = PLUGIN_ID + T_ELCL
+        + "th_supertypes.png"; //$NON-NLS-1$
+    public static final String IMG_ELCL_TH_SUBTYPES = PLUGIN_ID + T_ELCL
+        + "th_subtypes.png"; //$NON-NLS-1$
+    public static final String IMG_ELCL_TH_TYPES = PLUGIN_ID + T_ELCL
+        + "th_types.png"; //$NON-NLS-1$
 
     public static final String IMG_OBJ_SEARCH_OCCURRENCE = PLUGIN_ID + T_OBJ
         + "occ_match.png"; //$NON-NLS-1$
@@ -147,26 +155,34 @@
         reg.put(IMG_DLCL_REFRESH, imageDescriptorFromSymbolicName(
             IMG_DLCL_REFRESH));
 
-        reg.put(IMG_ELCL_CH_AUTOMATIC, imageDescriptorFromSymbolicName(
-            IMG_ELCL_CH_AUTOMATIC));
         reg.put(IMG_ELCL_CH_CALLEES, imageDescriptorFromSymbolicName(
             IMG_ELCL_CH_CALLEES));
         reg.put(IMG_ELCL_CH_CALLERS, imageDescriptorFromSymbolicName(
             IMG_ELCL_CH_CALLERS));
-        reg.put(IMG_ELCL_CH_HORIZONTAL, imageDescriptorFromSymbolicName(
-            IMG_ELCL_CH_HORIZONTAL));
-        reg.put(IMG_ELCL_CH_VERTICAL, imageDescriptorFromSymbolicName(
-            IMG_ELCL_CH_VERTICAL));
         reg.put(IMG_ELCL_EXPANDALL, imageDescriptorFromSymbolicName(
             IMG_ELCL_EXPANDALL));
         reg.put(IMG_ELCL_HISTORY_LIST, imageDescriptorFromSymbolicName(
             IMG_ELCL_HISTORY_LIST));
+        reg.put(IMG_ELCL_LAYOUT_AUTOMATIC, imageDescriptorFromSymbolicName(
+            IMG_ELCL_LAYOUT_AUTOMATIC));
+        reg.put(IMG_ELCL_LAYOUT_HORIZONTAL, imageDescriptorFromSymbolicName(
+            IMG_ELCL_LAYOUT_HORIZONTAL));
+        reg.put(IMG_ELCL_LAYOUT_SINGLE, imageDescriptorFromSymbolicName(
+            IMG_ELCL_LAYOUT_SINGLE));
+        reg.put(IMG_ELCL_LAYOUT_VERTICAL, imageDescriptorFromSymbolicName(
+            IMG_ELCL_LAYOUT_VERTICAL));
         reg.put(IMG_ELCL_LEXICAL_SORT, imageDescriptorFromSymbolicName(
             IMG_ELCL_LEXICAL_SORT));
         reg.put(IMG_ELCL_PIN_VIEW, imageDescriptorFromSymbolicName(
             IMG_ELCL_PIN_VIEW));
         reg.put(IMG_ELCL_REFRESH, imageDescriptorFromSymbolicName(
             IMG_ELCL_REFRESH));
+        reg.put(IMG_ELCL_TH_SUPERTYPES, imageDescriptorFromSymbolicName(
+            IMG_ELCL_TH_SUPERTYPES));
+        reg.put(IMG_ELCL_TH_SUBTYPES, imageDescriptorFromSymbolicName(
+            IMG_ELCL_TH_SUBTYPES));
+        reg.put(IMG_ELCL_TH_TYPES, imageDescriptorFromSymbolicName(
+            IMG_ELCL_TH_TYPES));
 
         reg.put(IMG_OBJ_SEARCH_OCCURRENCE, imageDescriptorFromSymbolicName(
             IMG_OBJ_SEARCH_OCCURRENCE));
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/PartListenerAdapter.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/PartListenerAdapter.java
new file mode 100644
index 0000000..b7ff52d
--- /dev/null
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/PartListenerAdapter.java
@@ -0,0 +1,54 @@
+/*******************************************************************************
+ * Copyright (c) 2021 1C-Soft LLC.
+ *
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Vladimir Piskarev (1C) - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.handly.ui;
+
+import org.eclipse.ui.IPartListener;
+import org.eclipse.ui.IWorkbenchPart;
+
+/**
+ * Provides default implementations for the methods of {@link IPartListener}.
+ * <p>
+ * Clients may extend this class and override only the methods which they are
+ * interested in.
+ * </p>
+ *
+ * @since 1.6
+ */
+public class PartListenerAdapter
+    implements IPartListener
+{
+    @Override
+    public void partOpened(IWorkbenchPart part)
+    {
+    }
+
+    @Override
+    public void partClosed(IWorkbenchPart part)
+    {
+    }
+
+    @Override
+    public void partActivated(IWorkbenchPart part)
+    {
+    }
+
+    @Override
+    public void partDeactivated(IWorkbenchPart part)
+    {
+    }
+
+    @Override
+    public void partBroughtToTop(IWorkbenchPart part)
+    {
+    }
+}
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/callhierarchy/CallHierarchyViewPart.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/callhierarchy/CallHierarchyViewPart.java
index 81d57ec..83e23a2 100644
--- a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/callhierarchy/CallHierarchyViewPart.java
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/callhierarchy/CallHierarchyViewPart.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2018, 2019 1C-Soft LLC.
+ * Copyright (c) 2018, 2021 1C-Soft LLC.
  *
  * This program and the accompanying materials are made available under
  * the terms of the Eclipse Public License 2.0 which is available at
@@ -26,6 +26,7 @@
 import org.eclipse.handly.ui.DefaultEditorUtility;
 import org.eclipse.handly.ui.EditorOpener;
 import org.eclipse.handly.ui.EditorUtility;
+import org.eclipse.handly.ui.PartListenerAdapter;
 import org.eclipse.handly.ui.action.HistoryDropDownAction;
 import org.eclipse.handly.ui.viewer.ColumnDescription;
 import org.eclipse.handly.ui.viewer.DelegatingSelectionProvider;
@@ -141,7 +142,7 @@
     private HistoryDropDownAction<HistoryEntry> historyDropDownAction;
     private final PinAction pinAction = new PinAction();
 
-    private final IPartListener partListener = new IPartListener()
+    private final IPartListener partListener = new PartListenerAdapter()
     {
         @Override
         public void partActivated(IWorkbenchPart part)
@@ -170,16 +171,6 @@
             getViewManager().viewOpenedOrActivated(CallHierarchyViewPart.this);
             refresh();
         }
-
-        @Override
-        public void partDeactivated(IWorkbenchPart part)
-        {
-        }
-
-        @Override
-        public void partBroughtToTop(IWorkbenchPart part)
-        {
-        }
     };
 
     /**
@@ -662,8 +653,11 @@
     public void setFocus()
     {
         pageBook.setFocus();
-        updateStatusLine(getViewSite().getActionBars().getStatusLineManager(),
-            (IStructuredSelection)getSite().getSelectionProvider().getSelection());
+        ISelection selection = getSite().getSelectionProvider().getSelection();
+        if (selection instanceof IStructuredSelection)
+            updateStatusLine(
+                getViewSite().getActionBars().getStatusLineManager(),
+                (IStructuredSelection)selection);
     }
 
     /**
@@ -1184,12 +1178,15 @@
             if (editorRef != null)
             {
                 editor = editorRef.getEditor(true);
-                if (activate)
-                    page.activate(editor);
-                else
-                    page.bringToTop(editor);
-                if (callLocation == null)
-                    editorUtility.revealElement(editor, element);
+                if (editor != null)
+                {
+                    if (activate)
+                        page.activate(editor);
+                    else
+                        page.bringToTop(editor);
+                    if (callLocation == null)
+                        editorUtility.revealElement(editor, element);
+                }
             }
         }
         if (editor != null && callLocation != null)
@@ -1871,7 +1868,7 @@
                 setToolTipText(
                     Messages.CallHierarchyViewPart_Layout_horizontal_action_tooltip);
                 setImageDescriptor(Activator.getImageDescriptor(
-                    Activator.IMG_ELCL_CH_HORIZONTAL));
+                    Activator.IMG_ELCL_LAYOUT_HORIZONTAL));
                 break;
             case SWT.VERTICAL:
                 setText(
@@ -1879,7 +1876,7 @@
                 setToolTipText(
                     Messages.CallHierarchyViewPart_Layout_vertical_action_tooltip);
                 setImageDescriptor(Activator.getImageDescriptor(
-                    Activator.IMG_ELCL_CH_VERTICAL));
+                    Activator.IMG_ELCL_LAYOUT_VERTICAL));
                 break;
             default:
                 setText(
@@ -1887,7 +1884,7 @@
                 setToolTipText(
                     Messages.CallHierarchyViewPart_Layout_automatic_action_tooltip);
                 setImageDescriptor(Activator.getImageDescriptor(
-                    Activator.IMG_ELCL_CH_AUTOMATIC));
+                    Activator.IMG_ELCL_LAYOUT_AUTOMATIC));
                 setChecked(true);
             }
         }
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/Messages.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/Messages.java
new file mode 100644
index 0000000..acb1d51
--- /dev/null
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/Messages.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * Copyright (c) 2021 1C-Soft LLC.
+ *
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Vladimir Piskarev (1C) - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.handly.ui.typehierarchy;
+
+import org.eclipse.osgi.util.NLS;
+
+class Messages
+    extends NLS
+{
+    private static final String BUNDLE_NAME =
+        "org.eclipse.handly.ui.typehierarchy.messages"; //$NON-NLS-1$
+
+    public static String TypeHierarchyViewPart_0__items_selected;
+    public static String TypeHierarchyViewPart_Error_opening_editor;
+    public static String TypeHierarchyViewPart_Focus_on_selection_action_text;
+    public static String TypeHierarchyViewPart_Focus_on_selection_action_tooltip;
+    public static String TypeHierarchyViewPart_History_entry_label__0;
+    public static String TypeHierarchyViewPart_History_entry_label__0__1;
+    public static String TypeHierarchyViewPart_History_entry_label__0__1_more;
+    public static String TypeHierarchyViewPart_Layout_automatic_action_text;
+    public static String TypeHierarchyViewPart_Layout_automatic_action_tooltip;
+    public static String TypeHierarchyViewPart_Layout_horizontal_action_text;
+    public static String TypeHierarchyViewPart_Layout_horizontal_action_tooltip;
+    public static String TypeHierarchyViewPart_Layout_menu;
+    public static String TypeHierarchyViewPart_Layout_single_action_text;
+    public static String TypeHierarchyViewPart_Layout_single_action_tooltip;
+    public static String TypeHierarchyViewPart_Layout_vertical_action_text;
+    public static String TypeHierarchyViewPart_Layout_vertical_action_tooltip;
+    public static String TypeHierarchyViewPart_No_hierarchy_to_display;
+    public static String TypeHierarchyViewPart_Open_selected_element;
+    public static String TypeHierarchyViewPart_Refresh_action_text;
+    public static String TypeHierarchyViewPart_Refresh_action_tooltip;
+    public static String TypeHierarchyViewPart_Show_subtype_hierarchy_action_text;
+    public static String TypeHierarchyViewPart_Show_subtype_hierarchy_action_tooltip;
+    public static String TypeHierarchyViewPart_Show_supertype_hierarchy_action_text;
+    public static String TypeHierarchyViewPart_Show_supertype_hierarchy_action_tooltip;
+    public static String TypeHierarchyViewPart_Show_type_hierarchy_action_text;
+    public static String TypeHierarchyViewPart_Show_type_hierarchy_action_tooltip;
+
+    static
+    {
+        // initialize resource bundle
+        NLS.initializeMessages(BUNDLE_NAME, Messages.class);
+    }
+
+    private Messages()
+    {
+    }
+}
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/TypeHierarchyKind.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/TypeHierarchyKind.java
new file mode 100644
index 0000000..99a3ac0
--- /dev/null
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/TypeHierarchyKind.java
@@ -0,0 +1,37 @@
+/*******************************************************************************
+ * Copyright (c) 2021 1C-Soft LLC.
+ *
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Vladimir Piskarev (1C) - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.handly.ui.typehierarchy;
+
+/**
+ * Enumeration of the type hierarchy kinds.
+ *
+ * @since 1.6
+ */
+public enum TypeHierarchyKind
+{
+    /**
+     * A type hierarchy that includes both supertypes and subtypes of a type.
+     * Also known as the inheritance tree.
+     */
+    TYPES,
+
+    /**
+     * The supertype hierarchy.
+     */
+    SUPERTYPES,
+
+    /**
+     * The subtype hierarchy.
+     */
+    SUBTYPES
+}
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/TypeHierarchyViewPart.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/TypeHierarchyViewPart.java
new file mode 100644
index 0000000..232a230
--- /dev/null
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/TypeHierarchyViewPart.java
@@ -0,0 +1,1622 @@
+/*******************************************************************************
+ * Copyright (c) 2021 1C-Soft LLC.
+ *
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Vladimir Piskarev (1C) - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.handly.ui.typehierarchy;
+
+import static org.eclipse.handly.context.Contexts.EMPTY_CONTEXT;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.handly.context.IContext;
+import org.eclipse.handly.internal.ui.Activator;
+import org.eclipse.handly.ui.DefaultEditorUtility;
+import org.eclipse.handly.ui.EditorOpener;
+import org.eclipse.handly.ui.EditorUtility;
+import org.eclipse.handly.ui.PartListenerAdapter;
+import org.eclipse.handly.ui.action.HistoryDropDownAction;
+import org.eclipse.handly.ui.viewer.DelegatingSelectionProvider;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IMenuListener;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.IStatusLineManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.StructuredViewer;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorReference;
+import org.eclipse.ui.IMemento;
+import org.eclipse.ui.IPartListener;
+import org.eclipse.ui.IViewSite;
+import org.eclipse.ui.IWorkbenchActionConstants;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.OpenAndLinkWithEditorHelper;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.actions.BaseSelectionListenerAction;
+import org.eclipse.ui.part.PageBook;
+import org.eclipse.ui.part.ViewPart;
+
+/**
+ * An abstract base implementation of a type hierarchy view.
+ *
+ * @since 1.6 Provisional API
+ */
+public abstract class TypeHierarchyViewPart
+    extends ViewPart
+{
+    /**
+     * Pop-up menu: name of group for focus actions (value
+     * <code>"group.focus"</code>).
+     */
+    protected static final String GROUP_FOCUS = "group.focus"; //$NON-NLS-1$
+
+    private static final Object[] NO_ELEMENTS = new Object[0];
+
+    private static final String KEY_HIERARCHY_KIND =
+        "org.eclipse.handly.typehierarchy.view.hierarchyKind"; //$NON-NLS-1$
+    private static final String KEY_LAYOUT_MODE =
+        "org.eclipse.handly.typehierarchy.view.layoutMode"; //$NON-NLS-1$
+    private static final String KEY_HORIZONTAL_WEIGHTS =
+        "org.eclipse.handly.typehierarchy.view.horizontalWeights"; //$NON-NLS-1$
+    private static final String KEY_VERTICAL_WEIGHTS =
+        "org.eclipse.handly.typehierarchy.view.verticalWeights"; //$NON-NLS-1$
+
+    private static final int LAYOUT_AUTOMATIC = SWT.HORIZONTAL | SWT.VERTICAL;
+
+    private final Set<TypeHierarchyKind> supportedHierarchyKinds;
+    private Set<TypeHierarchyKind> enabledHierarchyKinds;
+    private TypeHierarchyKind hierarchyKind;
+    private int layoutMode = getDefaultLayoutMode();
+    private boolean layoutAdjusted;
+    private int[] horizontalWeights, verticalWeights;
+    private Object[] inputElements = NO_ELEMENTS;
+    private final List<HistoryEntry> history = new ArrayList<>();
+    private PageBook pageBook, hierarchyPageBook;
+    private Control noHierarchyPage;
+    private SashForm sashForm;
+    private final Map<TypeHierarchyKind, TreeViewer> hierarchyViewers =
+        new HashMap<>();
+    private EditorOpener editorOpener;
+    private SetHierarchyKindAction[] setHierarchyKindActions =
+        new SetHierarchyKindAction[0];
+    private SetLayoutModeAction[] setLayoutModeActions =
+        new SetLayoutModeAction[0];
+    private final FocusOnSelectionAction focusOnSelectionAction =
+        new FocusOnSelectionAction();
+    private HistoryDropDownAction<HistoryEntry> historyDropDownAction;
+    private final RefreshAction refreshAction = new RefreshAction();
+
+    private final IPartListener partListener = new PartListenerAdapter()
+    {
+        @Override
+        public void partOpened(IWorkbenchPart part)
+        {
+            if (part != TypeHierarchyViewPart.this)
+                return;
+
+            refresh();
+        };
+    };
+
+    /**
+     * Constructs a type hierarchy view that supports all of the
+     * type hierarchy kinds.
+     *
+     * @see TypeHierarchyKind
+     * @see #TypeHierarchyViewPart(Set)
+     */
+    public TypeHierarchyViewPart()
+    {
+        this(EnumSet.allOf(TypeHierarchyKind.class));
+    }
+
+    /**
+     * Constructs a type hierarchy view that supports the given
+     * type hierarchy kinds. Clients <b>must not</b> modify the given set
+     * afterwards.
+     *
+     * @param supportedHierarchyKinds not <code>null</code> and not empty
+     */
+    public TypeHierarchyViewPart(Set<TypeHierarchyKind> supportedHierarchyKinds)
+    {
+        if (supportedHierarchyKinds.isEmpty())
+            throw new IllegalArgumentException();
+
+        this.supportedHierarchyKinds = supportedHierarchyKinds;
+        this.enabledHierarchyKinds = supportedHierarchyKinds;
+        this.hierarchyKind = supportedHierarchyKinds.iterator().next();
+
+        makeSetHierarchyKindActions();
+        makeSetLayoutModeActions();
+        refreshAction.setEnabled(false);
+    }
+
+    /**
+     * Returns whether the given elements are possible input elements for this view.
+     * <p>
+     * Default implementation invokes {@link #isPossibleInputElement(Object)} for
+     * each of the given elements until <code>false</code> is returned for an element
+     * (in which case this method will return <code>false</code>) or all elements
+     * have been checked (in which case it will return <code>true</code>).
+     * </p>
+     *
+     * @param elements may be <code>null</code> or may contain null elements,
+     *  in which case <code>false</code> will be returned; may be empty,
+     *  in which case <code>true</code> will be returned
+     * @return <code>true</code> if the given elements are possible input elements
+     *  for this view, and <code>false</code> otherwise
+     */
+    public boolean arePossibleInputElements(Object[] elements)
+    {
+        if (elements == null)
+            return false;
+        for (Object element : elements)
+        {
+            if (!isPossibleInputElement(element))
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * Sets the current input elements for this view. Clients <b>must not</b>
+     * modify the given array afterwards.
+     *
+     * @param elements not <code>null</code>, must not contain null elements;
+     *  may be empty
+     * @throws IllegalArgumentException if {@link #arePossibleInputElements(Object[])}
+     *  returns <code>false</code> for the given elements
+     */
+    public void setInputElements(Object[] elements)
+    {
+        if (!arePossibleInputElements(elements))
+            throw new IllegalArgumentException(Arrays.toString(elements));
+
+        Object[] oldInputElements = inputElements;
+
+        inputElements = elements;
+
+        refreshAction.setEnabled(elements.length > 0);
+
+        if (elements.length > 0)
+            addHistoryEntry(createHistoryEntry(elements));
+
+        onInputElementsChanged(oldInputElements, elements);
+    }
+
+    /**
+     * Returns the current input elements for this view.
+     *
+     * @return the current input elements (never <code>null</code>,
+     *  may be empty). Clients <b>must not</b> modify the returned array.
+     */
+    public final Object[] getInputElements()
+    {
+        return inputElements;
+    }
+
+    /**
+     * Returns whether the given hierarchy kind is currently enabled for this view.
+     *
+     * @param kind may be <code>null</code>, in which case <code>false</code>
+     *  will be returned
+     * @return <code>true</code> if the given kind is currently enabled,
+     *  and <code>false</code> otherwise
+     */
+    public final boolean isEnabledHierarchyKind(TypeHierarchyKind kind)
+    {
+        return enabledHierarchyKinds.contains(kind);
+    }
+
+    /**
+     * Sets the current hierarchy kind for this view.
+     *
+     * @param kind not <code>null</code>
+     * @throws IllegalArgumentException if the given kind is not currently {@link
+     *  #isEnabledHierarchyKind(TypeHierarchyKind) enabled} for this view
+     */
+    public void setHierarchyKind(TypeHierarchyKind kind)
+    {
+        if (!isEnabledHierarchyKind(kind))
+            throw new IllegalArgumentException();
+
+        if (kind == hierarchyKind)
+            return;
+
+        TypeHierarchyKind oldKind = hierarchyKind;
+
+        hierarchyKind = kind;
+
+        for (SetHierarchyKindAction action : setHierarchyKindActions)
+            action.setChecked(action.kind == kind);
+
+        onHierarchyKindChanged(oldKind, kind);
+    }
+
+    /**
+     * Returns the current hierarchy kind for this view.
+     *
+     * @return the current hierarchy kind (never <code>null</code>)
+     * @see #setHierarchyKind(TypeHierarchyKind)
+     */
+    public final TypeHierarchyKind getHierarchyKind()
+    {
+        return hierarchyKind;
+    }
+
+    /**
+     * Sets layout mode for this view, which may be one of the constants
+     * {@link SWT#HORIZONTAL} or {@link SWT#VERTICAL}; this method may also
+     * be called with <code>SWT.HORIZONTAL|SWT.VERTICAL</code> for automatic
+     * layout or {@link SWT#NONE} for hierarchy-only layout.
+     * <p>
+     * Note that only a subset of the allowed values for layout mode may be
+     * {@link #supportsLayoutMode(int) supported} by the view.
+     * </p>
+     *
+     * @param layoutMode the given layout mode
+     * @throws IllegalArgumentException if the given layout mode is not
+     *  {@link #supportsLayoutMode(int) supported} by the view
+     */
+    public void setLayoutMode(int layoutMode)
+    {
+        if (!supportsLayoutMode(layoutMode))
+            throw new IllegalArgumentException();
+
+        if (layoutMode == this.layoutMode)
+            return;
+
+        int oldLayoutMode = this.layoutMode;
+
+        this.layoutMode = layoutMode;
+
+        for (SetLayoutModeAction action : setLayoutModeActions)
+            action.setChecked(action.layoutMode == layoutMode);
+
+        onLayoutModeChanged(oldLayoutMode, layoutMode);
+    }
+
+    /**
+     * Returns the current layout mode for this view.
+     *
+     * @return the current layout mode
+     * @see #setLayoutMode(int)
+     */
+    public final int getLayoutMode()
+    {
+        return layoutMode;
+    }
+
+    /**
+     * Returns <code>true</code> if the given value represents a supported
+     * layout mode for this view, and <code>false</code> otherwise.
+     * The set of the supported layout mode values does not change over
+     * the lifetime of the view and is never empty.
+     * <p>
+     * Default implementation only supports {@link SWT#NONE}, which represents
+     * hierarchy-only layout.
+     * </p>
+     *
+     * @param layoutMode the layout mode to check
+     * @return <code>true</code> if the given value represents a supported
+     *  layout mode for this view, and <code>false</code> otherwise
+     */
+    public boolean supportsLayoutMode(int layoutMode)
+    {
+        return layoutMode == SWT.NONE;
+    }
+
+    /**
+     * Performs a full refresh of the content of this view.
+     */
+    public final void refresh()
+    {
+        refresh(EMPTY_CONTEXT);
+    }
+
+    @Override
+    public void init(IViewSite site, IMemento memento) throws PartInitException
+    {
+        super.init(site, memento);
+        if (memento != null)
+        {
+            if (supportedHierarchyKinds.size() > 1)
+            {
+                String value = memento.getString(KEY_HIERARCHY_KIND);
+                if (value != null)
+                {
+                    TypeHierarchyKind kind = null;
+                    try
+                    {
+                        kind = TypeHierarchyKind.valueOf(value);
+                    }
+                    catch (IllegalArgumentException e)
+                    {
+                    }
+                    if (supportedHierarchyKinds.contains(kind))
+                        setHierarchyKind(kind);
+                }
+            }
+
+            Integer layoutMode = memento.getInteger(KEY_LAYOUT_MODE);
+            if (layoutMode != null)
+            {
+                int value = layoutMode.intValue();
+                if (supportsLayoutMode(value))
+                    setLayoutMode(value);
+            }
+
+            try
+            {
+                horizontalWeights = toIntArray(memento.getString(
+                    KEY_HORIZONTAL_WEIGHTS));
+            }
+            catch (NumberFormatException e)
+            {
+            }
+
+            try
+            {
+                verticalWeights = toIntArray(memento.getString(
+                    KEY_VERTICAL_WEIGHTS));
+            }
+            catch (NumberFormatException e)
+            {
+            }
+        }
+    }
+
+    @Override
+    public void saveState(IMemento memento)
+    {
+        super.saveState(memento);
+        if (supportedHierarchyKinds.size() > 1)
+            memento.putString(KEY_HIERARCHY_KIND, hierarchyKind.name());
+        memento.putInteger(KEY_LAYOUT_MODE, layoutMode);
+        saveSashFormWeights();
+        memento.putString(KEY_HORIZONTAL_WEIGHTS, toString(horizontalWeights));
+        memento.putString(KEY_VERTICAL_WEIGHTS, toString(verticalWeights));
+    }
+
+    @Override
+    public void createPartControl(Composite parent)
+    {
+        getSite().getPage().addPartListener(partListener);
+
+        parent.addControlListener(new ControlAdapter()
+        {
+            @Override
+            public void controlResized(ControlEvent e)
+            {
+                if (!layoutAdjusted || layoutMode == LAYOUT_AUTOMATIC)
+                    adjustLayout(layoutMode);
+            }
+        });
+
+        DelegatingSelectionProvider selectionProvider =
+            new DelegatingSelectionProvider();
+
+        getSite().setSelectionProvider(selectionProvider);
+
+        selectionProvider.addSelectionChangedListener(e ->
+        {
+            ISelection selection = e.getSelection();
+            if (selection instanceof IStructuredSelection)
+                updateStatusLine(
+                    getViewSite().getActionBars().getStatusLineManager(),
+                    (IStructuredSelection)selection);
+        });
+
+        pageBook = new PageBook(parent, SWT.NONE);
+
+        noHierarchyPage = createNoHierarchyPage(pageBook);
+
+        sashForm = new SashForm(pageBook, SWT.NONE);
+
+        hierarchyPageBook = new PageBook(sashForm, SWT.NONE);
+
+        if (layoutMode == SWT.NONE)
+            sashForm.setMaximizedControl(hierarchyPageBook);
+
+        for (TypeHierarchyKind kind : supportedHierarchyKinds)
+        {
+            TreeViewer hierarchyViewer = createHierarchyViewer(
+                hierarchyPageBook, kind);
+
+            configureHierarchyViewer(hierarchyViewer, kind);
+
+            initContextMenu(hierarchyViewer.getControl(), (
+                IMenuManager manager) ->
+            {
+                createHierarchyViewerMenuGroups(manager, kind);
+                fillHierarchyViewerMenu(manager, kind);
+            }, getSite().getId(), selectionProvider);
+
+            new OpenEditorHelper(hierarchyViewer);
+
+            hierarchyViewer.getControl().addFocusListener(new FocusListener()
+            {
+                @Override
+                public void focusGained(FocusEvent e)
+                {
+                    selectionProvider.setDelegate(hierarchyViewer);
+                    onHierarchySelectionChanged(hierarchyViewer.getSelection(),
+                        kind);
+                }
+
+                @Override
+                public void focusLost(FocusEvent e)
+                {
+                    selectionProvider.setDelegate(null);
+                }
+            });
+
+            hierarchyViewer.addSelectionChangedListener(
+                e -> onHierarchySelectionChanged(e.getSelection(), kind));
+
+            hierarchyViewers.put(kind, hierarchyViewer);
+        }
+
+        editorOpener = createEditorOpener();
+
+        selectionProvider.addSelectionChangedListener(focusOnSelectionAction);
+
+        addRefreshAction(refreshAction);
+
+        getViewSite().getActionBars().setGlobalActionHandler(
+            ActionFactory.REFRESH.getId(), refreshAction);
+
+        for (SetHierarchyKindAction action : setHierarchyKindActions)
+            addSetHierarchyKindAction(action, action.kind);
+
+        for (SetLayoutModeAction action : setLayoutModeActions)
+            addSetLayoutModeAction(action, action.layoutMode);
+
+        historyDropDownAction = createHistoryDropDownAction(
+            new HistoryDropDownAction.History<HistoryEntry>()
+            {
+                @Override
+                public List<HistoryEntry> getHistoryEntries()
+                {
+                    return getHistory();
+                }
+
+                @Override
+                public void setHistoryEntries(List<HistoryEntry> entries)
+                {
+                    List<HistoryEntry> history = getHistory();
+                    history.clear();
+                    history.addAll(entries);
+                    onHistoryChanged();
+                }
+
+                @Override
+                public HistoryEntry getActiveEntry()
+                {
+                    Object[] inputElements = getInputElements();
+                    if (inputElements.length == 0)
+                        return null;
+                    return createHistoryEntry(inputElements);
+                }
+
+                @Override
+                public void setActiveEntry(HistoryEntry entry)
+                {
+                    setInputElements(entry.getInputElements());
+                }
+
+                @Override
+                public String getLabel(HistoryEntry entry)
+                {
+                    return entry.getLabel();
+                }
+
+                @Override
+                public ImageDescriptor getImageDescriptor(HistoryEntry entry)
+                {
+                    return entry.getImageDescriptor();
+                }
+            });
+        historyDropDownAction.setEnabled(!getHistory().isEmpty());
+        addHistoryDropDownAction(historyDropDownAction);
+    }
+
+    @Override
+    public void dispose()
+    {
+        getSite().getPage().removePartListener(partListener);
+
+        super.dispose();
+    }
+
+    @Override
+    public void setFocus()
+    {
+        pageBook.setFocus();
+    }
+
+    /**
+     * Returns whether the given element is a possible input element for this view.
+     *
+     * @param element may be <code>null</code>, in which case <code>false</code>
+     *  will be returned
+     * @return <code>true</code> if the given element is a possible input element
+     *  for this view, and <code>false</code> otherwise
+     * @see #arePossibleInputElements(Object[])
+     */
+    protected abstract boolean isPossibleInputElement(Object element);
+
+    /**
+     * Creates and returns a history entry for the given input elements.
+     *
+     * @param inputElements never <code>null</code>; never empty
+     * @return the created history entry (not <code>null</code>)
+     */
+    protected abstract HistoryEntry createHistoryEntry(Object[] inputElements);
+
+    /**
+     * Computes the content description for this view.
+     *
+     * @return the computed content description (not <code>null</code>)
+     * @see #getContentDescription()
+     */
+    protected abstract String computeContentDescription();
+
+    /**
+     * Returns whether the view has already been opened and has not yet been
+     * closed.
+     *
+     * @return <code>true</code> if the view is currently open,
+     *  and <code>false</code> otherwise
+     */
+    protected final boolean isOpen()
+    {
+        return pageBook != null && !pageBook.isDisposed();
+    }
+
+    /**
+     * Performs a refresh of the content of this view according to options
+     * specified in the given context.
+     *
+     * @param context the operation context (never <code>null</code>)
+     */
+    protected void refresh(IContext context)
+    {
+        if (!isOpen())
+            return;
+
+        setContentDescription(computeContentDescription());
+
+        for (Map.Entry<TypeHierarchyKind, TreeViewer> entry : hierarchyViewers.entrySet())
+            setHierarchyViewerInput(entry.getValue(), entry.getKey());
+
+        updateHierarchyPage();
+    }
+
+    /**
+     * Ensures that the appropriate hierarchy page is shown by this view.
+     * <p>
+     * This method may only be called after the SWT controls for this view
+     * have been created and before they have been disposed.
+     * </p>
+     */
+    protected void updateHierarchyPage()
+    {
+        if (inputElements.length > 0)
+        {
+            TreeViewer hierarchyViewer = hierarchyViewers.get(hierarchyKind);
+            Control hierarchyPage = hierarchyViewer.getControl();
+            pageBook.showPage(sashForm);
+            hierarchyPageBook.showPage(hierarchyPage);
+            hierarchyPage.setFocus();
+        }
+        else
+        {
+            pageBook.showPage(noHierarchyPage);
+            pageBook.setFocus();
+        }
+    }
+
+    /**
+     * A callback that is invoked when the view input elements change.
+     *
+     * @param oldInputElements never <code>null</code>
+     * @param newInputElements never <code>null</code>
+     */
+    protected void onInputElementsChanged(Object[] oldInputElements,
+        Object[] newInputElements)
+    {
+        refresh();
+    }
+
+    /**
+     * A callback that is invoked when the current hierarchy kind changes.
+     *
+     * @param oldKind never <code>null</code>
+     * @param newKind never <code>null</code>
+     */
+    protected void onHierarchyKindChanged(TypeHierarchyKind oldKind,
+        TypeHierarchyKind newKind)
+    {
+        if (!isOpen())
+            return;
+
+        updateHierarchyPage();
+    }
+
+    /**
+     * Returns a set of the supported hierarchy kinds for this view.
+     *
+     * @return the supported hierarchy kinds (never <code>null</code>,
+     *  never empty)
+     */
+    protected final Set<TypeHierarchyKind> getSupportedHierarchyKinds()
+    {
+        return supportedHierarchyKinds;
+    }
+
+    /**
+     * Sets the enabled hierarchy kinds for this view. Note that at least one
+     * hierarchy kind must be enabled. Clients <b>must not</b> modify the given
+     * set afterwards.
+     *
+     * @param kinds not <code>null</code> and not empty
+     */
+    protected void setEnabledHierarchyKinds(Set<TypeHierarchyKind> kinds)
+    {
+        if (kinds.isEmpty())
+            throw new IllegalArgumentException();
+
+        if (!supportedHierarchyKinds.containsAll(kinds))
+            throw new IllegalArgumentException();
+
+        if (enabledHierarchyKinds.equals(kinds))
+            return;
+
+        Set<TypeHierarchyKind> oldKinds = enabledHierarchyKinds;
+
+        enabledHierarchyKinds = kinds;
+
+        for (SetHierarchyKindAction action : setHierarchyKindActions)
+            action.setEnabled(kinds.contains(action.kind));
+
+        if (!kinds.contains(hierarchyKind))
+            setHierarchyKind(kinds.iterator().next());
+
+        onEnabledHierarchyKindsChanged(oldKinds, kinds);
+    }
+
+    /**
+     * A callback that is invoked when the set of enabled hierarchy kinds changes.
+     *
+     * @param oldKinds never <code>null</code>
+     * @param newKinds never <code>null</code>
+     */
+    protected void onEnabledHierarchyKindsChanged(
+        Set<TypeHierarchyKind> oldKinds, Set<TypeHierarchyKind> newKinds)
+    {
+        if (!isOpen())
+            return;
+
+        for (TypeHierarchyKind kind : symmetricDifference(oldKinds, newKinds))
+            setHierarchyViewerInput(hierarchyViewers.get(kind), kind);
+    }
+
+    /**
+     * A callback that is invoked when the view layout mode changes.
+     *
+     * @param oldLayoutMode the old layout mode
+     * @param newLayoutMode the new layout mode
+     */
+    protected void onLayoutModeChanged(int oldLayoutMode, int newLayoutMode)
+    {
+        if (!isOpen())
+            return;
+
+        sashForm.setMaximizedControl(newLayoutMode == SWT.NONE
+            ? hierarchyPageBook : null);
+        adjustLayout(newLayoutMode);
+    }
+
+    /**
+     * Updates the status line based on the given selection in this view.
+     * <p>
+     * Default implementation clears the status line message if the selection
+     * is empty or if exactly one element is selected; sets a generic message
+     * of the form "(x) items selected" otherwise. It always clears the error
+     * message.
+     * </p>
+     *
+     * @param manager the status line manager (never <code>null</code>)
+     * @param selection the current selection (never <code>null</code>)
+     */
+    protected void updateStatusLine(IStatusLineManager manager,
+        IStructuredSelection selection)
+    {
+        String message = null;
+        int size = selection.size();
+        if (size > 1)
+        {
+            message = MessageFormat.format(
+                Messages.TypeHierarchyViewPart_0__items_selected, size);
+        }
+        manager.setMessage(message);
+        manager.setErrorMessage(null);
+    }
+
+    /**
+     * Creates and returns a control for the 'no hierarchy' page.
+     * This method is called once, when the part's control is created.
+     * <p>
+     * Default implementation returns a <code>Label</code> telling,
+     * in general terms, that there is no type hierarchy to display.
+     * Subclasses may override this method (e.g., to give details
+     * on what the user needs to do to display a type hierarchy).
+     * </p>
+     *
+     * @param parent the parent composite (never <code>null</code>)
+     * @return the created control (not <code>null</code>)
+     */
+    protected Control createNoHierarchyPage(Composite parent)
+    {
+        Label label = new Label(parent, SWT.LEAD + SWT.WRAP);
+        label.setText(Messages.TypeHierarchyViewPart_No_hierarchy_to_display);
+        return label;
+    }
+
+    /**
+     * Returns the {@link SashForm} created for this view.
+     *
+     * @return the <code>SashForm</code>,
+     *  or <code>null</code> if it has yet to be created
+     */
+    protected final SashForm getSashForm()
+    {
+        return sashForm;
+    }
+
+    /**
+     * Returns the hierarchy tree viewer for the given hierarchy kind.
+     *
+     * @param kind the type hierarchy kind (never <code>null</code>)
+     * @return the hierarchy tree viewer,
+     *  or <code>null</code> if it has yet to be created
+     * @see #createHierarchyViewer(Composite, TypeHierarchyKind)
+     */
+    protected final TreeViewer getHierarchyViewer(TypeHierarchyKind kind)
+    {
+        return hierarchyViewers.get(kind);
+    }
+
+    /**
+     * Creates and returns a tree viewer that will be used for displaying
+     * the type hierarchy of the given kind. This method only creates the viewer;
+     * it does not configure it. This method is called once, when the part's
+     * control is created.
+     *
+     * @param parent the parent composite (never <code>null</code>)
+     * @param kind the type hierarchy kind (never <code>null</code>)
+     * @return the created viewer (not <code>null</code>)
+     * @see #configureHierarchyViewer(TreeViewer, TypeHierarchyKind)
+     */
+    protected TreeViewer createHierarchyViewer(Composite parent,
+        TypeHierarchyKind kind)
+    {
+        return new TreeViewer(parent, SWT.SINGLE);
+    }
+
+    /**
+     * Configures the newly created hierarchy viewer for the given hierarchy kind.
+     * This method has to set as least a content provider and a label provider.
+     * This method is called once, just after the viewer is created.
+     *
+     * @param viewer the viewer to configure (never <code>null</code>)
+     * @param kind the type hierarchy kind (never <code>null</code>)
+     */
+    protected abstract void configureHierarchyViewer(TreeViewer viewer,
+        TypeHierarchyKind kind);
+
+    /**
+     * Sets the input for the hierarchy viewer for the given hierarchy kind.
+     *
+     * @param viewer the viewer to set the input for (never <code>null</code>)
+     * @param kind the type hierarchy kind (never <code>null</code>)
+     */
+    protected void setHierarchyViewerInput(TreeViewer viewer,
+        TypeHierarchyKind kind)
+    {
+        viewer.setInput(inputElements.length > 0 && isEnabledHierarchyKind(kind)
+            ? inputElements : null);
+    }
+
+    /**
+     * Creates the menu groups for the pop-up menu of the hierarchy viewer of
+     * the given hierarchy kind. This method is called each time the pop-up menu
+     * is about to show, just before {@link #fillHierarchyViewerMenu} is called.
+     * <p>
+     * Default implementation adds groups named {@link #GROUP_FOCUS} and
+     * {@link IWorkbenchActionConstants#MB_ADDITIONS}. Subclasses may extend
+     * or override this method, but should usually keep the default groups.
+     * <p>
+     *
+     * @param manager the menu manager (never <code>null</code>)
+     * @param kind the type hierarchy kind (never <code>null</code>)
+     */
+    protected void createHierarchyViewerMenuGroups(IMenuManager manager,
+        TypeHierarchyKind kind)
+    {
+        manager.add(new Separator(GROUP_FOCUS));
+        manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
+    }
+
+    /**
+     * Fills the pop-menu for the hierarchy viewer for the given hierarchy kind
+     * using the menu groups created by {@link #createHierarchyViewerMenuGroups}.
+     * This method is called each time the pop-up menu is about to show.
+     * <p>
+     * Default implementation adds generic actions such as 'focus on selection'.
+     * Subclasses may extend or override this method.
+     * </p>
+     *
+     * @param manager the menu manager (never <code>null</code>)
+     * @param kind the type hierarchy kind (never <code>null</code>)
+     */
+    protected void fillHierarchyViewerMenu(IMenuManager manager,
+        TypeHierarchyKind kind)
+    {
+        if (focusOnSelectionAction.isEnabled())
+            manager.appendToGroup(GROUP_FOCUS, focusOnSelectionAction);
+    }
+
+    /**
+     * A callback that is invoked when selection changes in the hierarchy viewer
+     * for the given hierarchy kind.
+     * <p>
+     * Default implementation tries to reveal the selected hierarchy node in an
+     * open editor with {@link #revealInEditor(Object, boolean, boolean)
+     * revealInEditor}.
+     * </p>
+     *
+     * @param selection the new selection (never <code>null</code>)
+     * @param kind the type hierarchy kind (never <code>null</code>)
+     */
+    protected void onHierarchySelectionChanged(ISelection selection,
+        TypeHierarchyKind kind)
+    {
+        Object element = getSelectedElement(selection);
+        if (element != null)
+        {
+            if (hierarchyViewers.get(kind).getControl().isFocusControl())
+            {
+                try
+                {
+                    revealInEditor(element, false, false);
+                }
+                catch (PartInitException e)
+                {
+                    // cannot happen: may not open a new editor
+                }
+            }
+        }
+    }
+
+    /**
+     * Reveals the given element in an appropriate editor on a best effort basis.
+     * <p>
+     * Default implementation uses the {@link #getEditorOpener() editor opener}.
+     * </p>
+     *
+     * @param element not <code>null</code>
+     * @param activate whether to activate the editor
+     * @param mayOpenNewEditor whether a new editor may be opened
+     *  when the element cannot be revealed in an existing editor
+     * @throws PartInitException if a new editor could not be created
+     *  or initialized
+     */
+    protected void revealInEditor(Object element, boolean activate,
+        boolean mayOpenNewEditor) throws PartInitException
+    {
+        EditorUtility editorUtility = editorOpener.getEditorUtility();
+        if (editorUtility.getEditorInput(element) == null)
+            return;
+
+        if (mayOpenNewEditor)
+            editorOpener.open(element, activate, true);
+        else
+        {
+            IWorkbenchPage page = editorOpener.getWorkbenchPage();
+            IEditorReference editorRef = editorUtility.findEditor(page,
+                element);
+            if (editorRef != null)
+            {
+                IEditorPart editor = editorRef.getEditor(true);
+                if (editor != null)
+                {
+                    if (activate)
+                        page.activate(editor);
+                    else
+                        page.bringToTop(editor);
+                    editorUtility.revealElement(editor, element);
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the editor opener used by this view.
+     *
+     * @return the editor opener,
+     *  or <code>null</code> if it has yet to be created
+     * @see #createEditorOpener()
+     */
+    protected final EditorOpener getEditorOpener()
+    {
+        return editorOpener;
+    }
+
+    /**
+     * Creates and returns an editor opener for this view.
+     * This method is called once, when the part's control is created.
+     * <p>
+     * Subclasses may override this method if they require a specific
+     * editor opener.
+     * </p>
+     *
+     * @return the created editor opener (not <code>null</code>)
+     */
+    protected EditorOpener createEditorOpener()
+    {
+        return new EditorOpener(getSite().getPage(),
+            DefaultEditorUtility.INSTANCE);
+    }
+
+    /**
+     * Returns the 'focus on selection' action used by this view.
+     *
+     * @return the 'focus on selection' action
+     */
+    protected final IAction getFocusOnSelectionAction()
+    {
+        return focusOnSelectionAction;
+    }
+
+    /**
+     * Contributes the 'refresh' action to this view.
+     * This method is called once, when the part's control is created.
+     * <p>
+     * Default implementation adds the given action to the view tool bar.
+     * Subclasses may extend or override this method.
+     * </p>
+     *
+     * @param action the 'refresh' action (never <code>null</code>)
+     */
+    protected void addRefreshAction(IAction action)
+    {
+        getViewSite().getActionBars().getToolBarManager().add(action);
+    }
+
+    /**
+     * Contributes a 'set hierarchy kind' action to this view. This method
+     * is called once for each of the 'set hierarchy kind' actions, when the
+     * part's control is created. If this view supports only one hierarchy kind,
+     * no 'set hierarchy kind' action is created and this method is not called.
+     * <p>
+     * Default implementation adds the given action to the view tool bar
+     * as well as to the view menu. Subclasses may extend or override
+     * this method.
+     * </p>
+     *
+     * @param action a 'set hierarchy kind' action
+     *  (never <code>null</code>)
+     * @param kind the hierarchy kind set by the given action
+     *  (never <code>null</code>)
+     * @see #setHierarchyKind(TypeHierarchyKind)
+     */
+    protected void addSetHierarchyKindAction(IAction action,
+        TypeHierarchyKind kind)
+    {
+        IActionBars actionBars = getViewSite().getActionBars();
+        actionBars.getToolBarManager().add(action);
+        actionBars.getMenuManager().add(action);
+    }
+
+    /**
+     * Contributes a 'set layout mode' action to this view. This method
+     * is called once for each of the 'set layout mode' actions, when the
+     * part's control is created. If this view supports only one layout mode,
+     * no 'set layout mode' action is created and this method is not called.
+     * <p>
+     * Default implementation adds the given action to the 'Layout' sub-menu
+     * of the view menu. The sub-menu is created if necessary. Subclasses may
+     * extend or override this method.
+     * </p>
+     *
+     * @param action a 'set layout mode' action
+     *  (never <code>null</code>)
+     * @param layoutMode the layout mode set by the given action
+     * @see #setLayoutMode(int)
+     */
+    protected void addSetLayoutModeAction(IAction action, int layoutMode)
+    {
+        IActionBars actionBars = getViewSite().getActionBars();
+        IMenuManager viewMenu = actionBars.getMenuManager();
+        String id = "layout"; //$NON-NLS-1$
+        IMenuManager layoutSubMenu = viewMenu.findMenuUsingPath(id);
+        if (layoutSubMenu == null)
+        {
+            viewMenu.add(new Separator());
+            layoutSubMenu = new MenuManager(
+                Messages.TypeHierarchyViewPart_Layout_menu, id);
+            viewMenu.add(layoutSubMenu);
+        }
+        layoutSubMenu.add(action);
+    }
+
+    /**
+     * Creates and returns a 'show history list' action for this view.
+     * This method is called once, when the part's control is created.
+     * <p>
+     * Subclasses need to override this method if they extend
+     * {@link HistoryDropDownAction}.
+     * </p>
+     *
+     * @param history never <code>null</code>
+     * @return the created action (not <code>null</code>)
+     */
+    protected HistoryDropDownAction<HistoryEntry> createHistoryDropDownAction(
+        HistoryDropDownAction.History<HistoryEntry> history)
+    {
+        return new HistoryDropDownAction<>(history);
+    }
+
+    /**
+     * Contributes the 'show history list' action to this view.
+     * This method is called once, when the part's control is created.
+     * <p>
+     * Default implementation adds the given action to the view tool bar.
+     * Subclasses may extend or override this method.
+     * </p>
+     *
+     * @param action the 'show history list' action (never <code>null</code>)
+     */
+    protected void addHistoryDropDownAction(IAction action)
+    {
+        getViewSite().getActionBars().getToolBarManager().add(action);
+    }
+
+    /**
+     * Returns the history used by this view; the history is represented by
+     * a "live" list of history entries.
+     *
+     * @return the view history (never <code>null</code>)
+     */
+    protected List<HistoryEntry> getHistory()
+    {
+        return history;
+    }
+
+    /**
+     * A callback that is invoked when the history has been updated.
+     * <p>
+     * Default implementation sets the enabled state of the 'show history list'
+     * action according to whether the history is empty and, if the history
+     * is empty, clears the view input.
+     * </p>
+     *
+     * @see #getHistory()
+     */
+    protected void onHistoryChanged()
+    {
+        boolean empty = getHistory().isEmpty();
+        historyDropDownAction.setEnabled(!empty);
+        if (empty)
+            setInputElements(NO_ELEMENTS);
+    }
+
+    private void addHistoryEntry(HistoryEntry entry)
+    {
+        List<HistoryEntry> history = getHistory();
+        history.remove(entry);
+        history.add(0, entry);
+        onHistoryChanged();
+    }
+
+    private void makeSetHierarchyKindActions()
+    {
+        int size = supportedHierarchyKinds.size();
+        if (size > 1)
+        {
+            setHierarchyKindActions = new SetHierarchyKindAction[size];
+            int i = 0;
+            for (TypeHierarchyKind kind : supportedHierarchyKinds)
+                setHierarchyKindActions[i++] = new SetHierarchyKindAction(kind);
+        }
+    }
+
+    private void makeSetLayoutModeActions()
+    {
+        List<SetLayoutModeAction> actions = new ArrayList<>();
+        if (supportsLayoutMode(SWT.VERTICAL))
+            actions.add(new SetLayoutModeAction(SWT.VERTICAL));
+        if (supportsLayoutMode(SWT.HORIZONTAL))
+            actions.add(new SetLayoutModeAction(SWT.HORIZONTAL));
+        if (supportsLayoutMode(LAYOUT_AUTOMATIC))
+            actions.add(new SetLayoutModeAction(LAYOUT_AUTOMATIC));
+        if (supportsLayoutMode(SWT.NONE))
+            actions.add(new SetLayoutModeAction(SWT.NONE));
+        int size = actions.size();
+        if (size > 1)
+            setLayoutModeActions = actions.toArray(
+                new SetLayoutModeAction[size]);
+    }
+
+    private int getDefaultLayoutMode()
+    {
+        if (supportsLayoutMode(LAYOUT_AUTOMATIC))
+            return LAYOUT_AUTOMATIC;
+        if (supportsLayoutMode(SWT.VERTICAL))
+            return SWT.VERTICAL;
+        if (supportsLayoutMode(SWT.HORIZONTAL))
+            return SWT.HORIZONTAL;
+        if (supportsLayoutMode(SWT.NONE))
+            return SWT.NONE;
+        throw new AssertionError();
+    }
+
+    private void adjustLayout(int layoutMode)
+    {
+        if (layoutMode == SWT.NONE)
+            return;
+
+        Point size = sashForm.getParent().getParent().getSize();
+        if (size.x == 0 || size.y == 0)
+            return;
+
+        int orientation = (layoutMode == LAYOUT_AUTOMATIC) ? (size.x > size.y
+            ? SWT.HORIZONTAL : SWT.VERTICAL) : layoutMode;
+
+        if (sashForm.getOrientation() == orientation && layoutAdjusted)
+            return;
+
+        if (sashForm.getOrientation() != orientation)
+        {
+            saveSashFormWeights();
+            sashForm.setOrientation(orientation);
+        }
+
+        restoreSashFormWeights();
+        layoutAdjusted = true;
+    }
+
+    private void saveSashFormWeights()
+    {
+        if (!layoutAdjusted)
+            return;
+
+        int[] weights = sashForm.getWeights();
+        if (weights.length < 2)
+            return;
+
+        if (sashForm.getOrientation() == SWT.HORIZONTAL)
+            horizontalWeights = weights;
+        else
+            verticalWeights = weights;
+    }
+
+    private void restoreSashFormWeights()
+    {
+        int[] weights = (sashForm.getOrientation() == SWT.HORIZONTAL)
+            ? horizontalWeights : verticalWeights;
+        if (weights != null)
+            sashForm.setWeights(weights);
+    }
+
+    private void initContextMenu(Control parent, IMenuListener listener,
+        String menuId, ISelectionProvider selectionProvider)
+    {
+        MenuManager manager = new MenuManager();
+        manager.setRemoveAllWhenShown(true);
+        manager.addMenuListener(listener);
+        Menu menu = manager.createContextMenu(parent);
+        parent.setMenu(menu);
+        getSite().registerContextMenu(menuId, manager, selectionProvider);
+    }
+
+    private static Object getSelectedElement(ISelection selection)
+    {
+        if (selection instanceof IStructuredSelection)
+        {
+            IStructuredSelection ss = (IStructuredSelection)selection;
+            if (ss.size() == 1)
+                return ss.getFirstElement();
+        }
+        return null;
+    }
+
+    private static Set<TypeHierarchyKind> symmetricDifference(
+        Set<TypeHierarchyKind> kindsA, Set<TypeHierarchyKind> kindsB)
+    {
+        Set<TypeHierarchyKind> result = EnumSet.copyOf(kindsA);
+        for (TypeHierarchyKind kind : kindsB)
+        {
+            if (!result.add(kind))
+                result.remove(kind);
+        }
+        return result;
+    }
+
+    private static String toString(int[] values)
+    {
+        if (values == null)
+            return null;
+        StringBuilder sb = new StringBuilder();
+        int length = values.length;
+        for (int i = 0; i < length; i++)
+        {
+            sb.append(values[i]);
+            if (i < length - 1)
+                sb.append(',');
+        }
+        return sb.toString();
+    }
+
+    private static int[] toIntArray(String s) throws NumberFormatException
+    {
+        if (s == null)
+            return null;
+        String[] values = s.split(","); //$NON-NLS-1$
+        int length = values.length;
+        int[] result = new int[length];
+        for (int i = 0; i < length; i++)
+        {
+            result[i] = Integer.parseInt(values[i]);
+        }
+        return result;
+    }
+
+    /**
+     * Represents an entry of the type hierarchy view history list.
+     */
+    protected abstract static class HistoryEntry
+    {
+        private final Object[] inputElements;
+
+        /**
+         * Constructs a history entry for the given input elements.
+         * Clients <b>must not</b> modify the given array afterwards.
+         *
+         * @param inputElements never <code>null</code>; never empty
+         */
+        protected HistoryEntry(Object[] inputElements)
+        {
+            if (inputElements.length == 0)
+                throw new AssertionError();
+            this.inputElements = inputElements;
+        }
+
+        /**
+         * Returns the input elements for this history entry.
+         *
+         * @return the input elements (never <code>null</code>; never empty).
+         *  Clients <b>must not</b> modify the returned array.
+         */
+        public final Object[] getInputElements()
+        {
+            return inputElements;
+        }
+
+        @Override
+        public boolean equals(Object obj)
+        {
+            if (this == obj)
+                return true;
+            if (obj == null)
+                return false;
+            if (getClass() != obj.getClass())
+                return false;
+            HistoryEntry other = (HistoryEntry)obj;
+            if (!Arrays.equals(inputElements, other.inputElements))
+                return false;
+            return true;
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Arrays.hashCode(inputElements);
+        }
+
+        /**
+         * Returns a user-readable text label for this history entry.
+         * <p>
+         * Default implementation composes a label based on labels for
+         * input elements. It invokes {@link #getElementLabel(Object)}
+         * to obtain a label for an input element.
+         * </p>
+         *
+         * @return the text label of the history entry (never <code>null</code>)
+         */
+        public String getLabel()
+        {
+            switch (inputElements.length)
+            {
+            case 1:
+                return MessageFormat.format(
+                    Messages.TypeHierarchyViewPart_History_entry_label__0,
+                    getElementLabel(inputElements[0]));
+            case 2:
+                return MessageFormat.format(
+                    Messages.TypeHierarchyViewPart_History_entry_label__0__1,
+                    getElementLabel(inputElements[0]), getElementLabel(
+                        inputElements[1]));
+            default:
+                return MessageFormat.format(
+                    Messages.TypeHierarchyViewPart_History_entry_label__0__1_more,
+                    getElementLabel(inputElements[0]), getElementLabel(
+                        inputElements[1]));
+            }
+        }
+
+        /**
+         * Returns a user-readable text label for the given element.
+         *
+         * @param element the given element
+         * @return the text label of the element (never <code>null</code>)
+         */
+        protected abstract String getElementLabel(Object element);
+
+        /**
+         * Returns an image descriptor for this history entry.
+         * <p>
+         * Default implementation always returns <code>null</code>.
+         * </p>
+         *
+         * @return the image descriptor of the history entry
+         *  (may be <code>null</code>)
+         */
+        public ImageDescriptor getImageDescriptor()
+        {
+            return null;
+        }
+    }
+
+    /**
+     * Helper for opening editors on the viewer's selection.
+     */
+    protected class OpenEditorHelper
+        extends OpenAndLinkWithEditorHelper
+    {
+        /**
+         * Creates a new helper for the given viewer.
+         *
+         * @param viewer the viewer
+         */
+        public OpenEditorHelper(StructuredViewer viewer)
+        {
+            super(viewer);
+        }
+
+        @Override
+        protected void activate(ISelection selection)
+        {
+            Object element = getSelectedElement(selection);
+            if (element != null)
+            {
+                try
+                {
+                    revealInEditor(element, true, false);
+                }
+                catch (PartInitException e)
+                {
+                    // cannot happen: may not open a new editor
+                }
+            }
+        }
+
+        @Override
+        protected void open(ISelection selection, boolean activate)
+        {
+            Object element = getSelectedElement(selection);
+            if (element != null)
+            {
+                try
+                {
+                    revealInEditor(element, activate, true);
+                }
+                catch (PartInitException e)
+                {
+                    ErrorDialog.openError(getSite().getShell(),
+                        Messages.TypeHierarchyViewPart_Open_selected_element,
+                        Messages.TypeHierarchyViewPart_Error_opening_editor,
+                        e.getStatus());
+                }
+            }
+        }
+    }
+
+    private class FocusOnSelectionAction
+        extends BaseSelectionListenerAction
+    {
+        FocusOnSelectionAction()
+        {
+            super(
+                Messages.TypeHierarchyViewPart_Focus_on_selection_action_text);
+            setToolTipText(
+                Messages.TypeHierarchyViewPart_Focus_on_selection_action_tooltip);
+        }
+
+        @Override
+        public void run()
+        {
+            setInputElements(getStructuredSelection().toArray());
+        }
+
+        @Override
+        protected boolean updateSelection(IStructuredSelection selection)
+        {
+            return !selection.isEmpty() && arePossibleInputElements(
+                selection.toArray());
+        }
+    }
+
+    private class RefreshAction
+        extends Action
+    {
+        RefreshAction()
+        {
+            setText(Messages.TypeHierarchyViewPart_Refresh_action_text);
+            setToolTipText(
+                Messages.TypeHierarchyViewPart_Refresh_action_tooltip);
+            setImageDescriptor(Activator.getImageDescriptor(
+                Activator.IMG_ELCL_REFRESH));
+            setDisabledImageDescriptor(Activator.getImageDescriptor(
+                Activator.IMG_DLCL_REFRESH));
+        }
+
+        @Override
+        public void run()
+        {
+            refresh();
+        }
+    }
+
+    private class SetHierarchyKindAction
+        extends Action
+    {
+        final TypeHierarchyKind kind;
+
+        SetHierarchyKindAction(TypeHierarchyKind kind)
+        {
+            super(null, AS_RADIO_BUTTON);
+            this.kind = kind;
+            switch (kind)
+            {
+            case TYPES:
+                setText(
+                    Messages.TypeHierarchyViewPart_Show_type_hierarchy_action_text);
+                setToolTipText(
+                    Messages.TypeHierarchyViewPart_Show_type_hierarchy_action_tooltip);
+                setImageDescriptor(Activator.getImageDescriptor(
+                    Activator.IMG_ELCL_TH_TYPES));
+                break;
+            case SUPERTYPES:
+                setText(
+                    Messages.TypeHierarchyViewPart_Show_supertype_hierarchy_action_text);
+                setToolTipText(
+                    Messages.TypeHierarchyViewPart_Show_supertype_hierarchy_action_tooltip);
+                setImageDescriptor(Activator.getImageDescriptor(
+                    Activator.IMG_ELCL_TH_SUPERTYPES));
+                break;
+            case SUBTYPES:
+                setText(
+                    Messages.TypeHierarchyViewPart_Show_subtype_hierarchy_action_text);
+                setToolTipText(
+                    Messages.TypeHierarchyViewPart_Show_subtype_hierarchy_action_tooltip);
+                setImageDescriptor(Activator.getImageDescriptor(
+                    Activator.IMG_ELCL_TH_SUBTYPES));
+                break;
+            }
+            if (kind == getHierarchyKind())
+                setChecked(true);
+        }
+
+        @Override
+        public void run()
+        {
+            setHierarchyKind(kind);
+        }
+    }
+
+    private class SetLayoutModeAction
+        extends Action
+    {
+        final int layoutMode;
+
+        SetLayoutModeAction(int layoutMode)
+        {
+            super(null, AS_RADIO_BUTTON);
+            this.layoutMode = layoutMode;
+            switch (layoutMode)
+            {
+            case SWT.VERTICAL:
+                setText(
+                    Messages.TypeHierarchyViewPart_Layout_vertical_action_text);
+                setToolTipText(
+                    Messages.TypeHierarchyViewPart_Layout_vertical_action_tooltip);
+                setImageDescriptor(Activator.getImageDescriptor(
+                    Activator.IMG_ELCL_LAYOUT_VERTICAL));
+                break;
+            case SWT.HORIZONTAL:
+                setText(
+                    Messages.TypeHierarchyViewPart_Layout_horizontal_action_text);
+                setToolTipText(
+                    Messages.TypeHierarchyViewPart_Layout_horizontal_action_tooltip);
+                setImageDescriptor(Activator.getImageDescriptor(
+                    Activator.IMG_ELCL_LAYOUT_HORIZONTAL));
+                break;
+            case LAYOUT_AUTOMATIC:
+                setText(
+                    Messages.TypeHierarchyViewPart_Layout_automatic_action_text);
+                setToolTipText(
+                    Messages.TypeHierarchyViewPart_Layout_automatic_action_tooltip);
+                setImageDescriptor(Activator.getImageDescriptor(
+                    Activator.IMG_ELCL_LAYOUT_AUTOMATIC));
+                break;
+            case SWT.NONE:
+                setText(
+                    Messages.TypeHierarchyViewPart_Layout_single_action_text);
+                setToolTipText(
+                    Messages.TypeHierarchyViewPart_Layout_single_action_tooltip);
+                setImageDescriptor(Activator.getImageDescriptor(
+                    Activator.IMG_ELCL_LAYOUT_SINGLE));
+                break;
+            default:
+                throw new IllegalArgumentException();
+            }
+            if (layoutMode == getLayoutMode())
+                setChecked(true);
+        }
+
+        @Override
+        public void run()
+        {
+            setLayoutMode(layoutMode);
+        }
+    }
+}
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/messages.properties b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/messages.properties
new file mode 100644
index 0000000..60eb10b
--- /dev/null
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/messages.properties
@@ -0,0 +1,39 @@
+###############################################################################
+# Copyright (c) 2021 1C-Soft LLC.
+#
+# This program and the accompanying materials are made available under
+# the terms of the Eclipse Public License 2.0 which is available at
+# https://www.eclipse.org/legal/epl-2.0/
+#
+# SPDX-License-Identifier: EPL-2.0
+#
+# Contributors:
+#     Vladimir Piskarev (1C) - initial API and implementation
+###############################################################################
+
+TypeHierarchyViewPart_0__items_selected={0} items selected
+TypeHierarchyViewPart_Error_opening_editor=An error occurred while opening the editor.
+TypeHierarchyViewPart_Focus_on_selection_action_text=Fo&cus on Selection
+TypeHierarchyViewPart_Focus_on_selection_action_tooltip=Focus on Selection
+TypeHierarchyViewPart_History_entry_label__0={0}
+TypeHierarchyViewPart_History_entry_label__0__1={0}, {1}
+TypeHierarchyViewPart_History_entry_label__0__1_more={0}, {1}, ...
+TypeHierarchyViewPart_Layout_automatic_action_text=&Automatic
+TypeHierarchyViewPart_Layout_automatic_action_tooltip=Automatic
+TypeHierarchyViewPart_Layout_horizontal_action_text=&Horizontal
+TypeHierarchyViewPart_Layout_horizontal_action_tooltip=Horizontal
+TypeHierarchyViewPart_Layout_menu=&Layout
+TypeHierarchyViewPart_Layout_single_action_text=&Hierarchy Only
+TypeHierarchyViewPart_Layout_single_action_tooltip=Hierarchy Only
+TypeHierarchyViewPart_Layout_vertical_action_text=&Vertical
+TypeHierarchyViewPart_Layout_vertical_action_tooltip=Vertical
+TypeHierarchyViewPart_No_hierarchy_to_display=No type hierarchy to display.
+TypeHierarchyViewPart_Open_selected_element=Open Selected Element
+TypeHierarchyViewPart_Refresh_action_text=Re&fresh View
+TypeHierarchyViewPart_Refresh_action_tooltip=Refresh View
+TypeHierarchyViewPart_Show_subtype_hierarchy_action_text=Su&btype Hierarchy
+TypeHierarchyViewPart_Show_subtype_hierarchy_action_tooltip=Show the Subtype Hierarchy
+TypeHierarchyViewPart_Show_supertype_hierarchy_action_text=Su&pertype Hierarchy
+TypeHierarchyViewPart_Show_supertype_hierarchy_action_tooltip=Show the Supertype Hierarchy
+TypeHierarchyViewPart_Show_type_hierarchy_action_text=&Type Hierarchy
+TypeHierarchyViewPart_Show_type_hierarchy_action_tooltip=Show the Type Hierarchy
diff --git a/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/package-info.java b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/package-info.java
new file mode 100644
index 0000000..d45a073
--- /dev/null
+++ b/org.eclipse.handly.ui/src/org/eclipse/handly/ui/typehierarchy/package-info.java
@@ -0,0 +1,20 @@
+/*******************************************************************************
+ * Copyright (c) 2021 1C-Soft LLC.
+ *
+ * This program and the accompanying materials are made available under
+ * the terms of the Eclipse Public License 2.0 which is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Vladimir Piskarev (1C) - initial API and implementation
+ *******************************************************************************/
+/**
+ * Provides a framework for type hierarchy views.
+ * <p>
+ * See <code>org.eclipse.handly.internal.examples.adapter.ui.typehierarchy</code>
+ * for usage example.
+ * </p>
+ */
+package org.eclipse.handly.ui.typehierarchy;