Bug 498030 - Implement an SWT layout spy
Change-Id: Iaea155b7fe07bcabc79a977e28d8d77d320105fc
Signed-off-by: Stefan Xenos <sxenos@gmail.com>
diff --git a/ui/org.eclipse.pde.runtime/.classpath b/ui/org.eclipse.pde.runtime/.classpath
index 4c62a80..4f83b23 100644
--- a/ui/org.eclipse.pde.runtime/.classpath
+++ b/ui/org.eclipse.pde.runtime/.classpath
@@ -2,6 +2,6 @@
<classpath>
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
<classpathentry kind="src" path="src"/>
- <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/ui/org.eclipse.pde.runtime/.settings/org.eclipse.jdt.core.prefs b/ui/org.eclipse.pde.runtime/.settings/org.eclipse.jdt.core.prefs
index 62ec106..519f9d0 100644
--- a/ui/org.eclipse.pde.runtime/.settings/org.eclipse.jdt.core.prefs
+++ b/ui/org.eclipse.pde.runtime/.settings/org.eclipse.jdt.core.prefs
@@ -8,9 +8,10 @@
org.eclipse.jdt.core.classpath.exclusionPatterns=enabled
org.eclipse.jdt.core.classpath.multipleOutputLocations=enabled
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
-org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
@@ -103,7 +104,7 @@
org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=error
org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
-org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.compiler.source=1.8
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
diff --git a/ui/org.eclipse.pde.runtime/META-INF/MANIFEST.MF b/ui/org.eclipse.pde.runtime/META-INF/MANIFEST.MF
index 9847de0..778f97c 100644
--- a/ui/org.eclipse.pde.runtime/META-INF/MANIFEST.MF
+++ b/ui/org.eclipse.pde.runtime/META-INF/MANIFEST.MF
@@ -2,14 +2,19 @@
Bundle-ManifestVersion: 2
Bundle-Name: %name
Bundle-SymbolicName: org.eclipse.pde.runtime; singleton:=true
-Bundle-Version: 3.5.100.qualifier
+Bundle-Version: 3.6.0.qualifier
Bundle-Activator: org.eclipse.pde.internal.runtime.PDERuntimePlugin
Bundle-Vendor: %provider-name
Bundle-Localization: plugin
-Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.11.0,4.0.0)",
+Require-Bundle: org.eclipse.core.databinding;bundle-version="1.6.0",
+ org.eclipse.core.databinding.observable;bundle-version="1.6.0",
+ org.eclipse.core.databinding.property;bundle-version="1.6.0",
+ org.eclipse.core.runtime;bundle-version="[3.11.0,4.0.0)",
org.eclipse.ui;bundle-version="[3.3.0,4.0.0)",
org.eclipse.ui.forms;bundle-version="[3.3.0,4.0.0)",
+ org.eclipse.jdt.annotation;bundle-version="2.1.0",
org.eclipse.jdt.core;bundle-version="[3.3.0,4.0.0)";resolution:=optional,
+ org.eclipse.jface.databinding;bundle-version="1.8.0",
org.eclipse.core.resources;bundle-version="[3.3.0,4.0.0)";resolution:=optional,
org.eclipse.jdt.ui;bundle-version="[3.3.0,4.0.0)";resolution:=optional,
org.eclipse.pde.ui;bundle-version="[3.3.0,4.0.0)";resolution:=optional,
@@ -22,5 +27,5 @@
org.eclipse.pde.internal.runtime.spy.dialogs;x-internal:=true,
org.eclipse.pde.internal.runtime.spy.handlers;x-internal:=true,
org.eclipse.pde.internal.runtime.spy.sections;x-internal:=true
-Bundle-RequiredExecutionEnvironment: JavaSE-1.6
+Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-ActivationPolicy: lazy
diff --git a/ui/org.eclipse.pde.runtime/icons/elcl16/up_nav.png b/ui/org.eclipse.pde.runtime/icons/elcl16/up_nav.png
new file mode 100644
index 0000000..a9830bc
--- /dev/null
+++ b/ui/org.eclipse.pde.runtime/icons/elcl16/up_nav.png
Binary files differ
diff --git a/ui/org.eclipse.pde.runtime/icons/elcl16/up_nav@2x.png b/ui/org.eclipse.pde.runtime/icons/elcl16/up_nav@2x.png
new file mode 100644
index 0000000..adaef6d
--- /dev/null
+++ b/ui/org.eclipse.pde.runtime/icons/elcl16/up_nav@2x.png
Binary files differ
diff --git a/ui/org.eclipse.pde.runtime/icons/obj16/layoutspy_obj.png b/ui/org.eclipse.pde.runtime/icons/obj16/layoutspy_obj.png
new file mode 100644
index 0000000..c729f06
--- /dev/null
+++ b/ui/org.eclipse.pde.runtime/icons/obj16/layoutspy_obj.png
Binary files differ
diff --git a/ui/org.eclipse.pde.runtime/plugin.properties b/ui/org.eclipse.pde.runtime/plugin.properties
index 1439826..82eeec5 100644
--- a/ui/org.eclipse.pde.runtime/plugin.properties
+++ b/ui/org.eclipse.pde.runtime/plugin.properties
@@ -21,5 +21,7 @@
spy-category.name = Spy
spy-command.name = Plug-in Selection Spy
spy-command.description = Show the Plug-in Spy
+spy-layout-command.name = Layout Spy
+spy-layout-command.description = Show the Layout Spy
spy-menu-command.name = Plug-in Menu Spy
diff --git a/ui/org.eclipse.pde.runtime/plugin.xml b/ui/org.eclipse.pde.runtime/plugin.xml
index 0824cb2..605627a 100644
--- a/ui/org.eclipse.pde.runtime/plugin.xml
+++ b/ui/org.eclipse.pde.runtime/plugin.xml
@@ -46,6 +46,12 @@
id="org.eclipse.pde.runtime.spy.commands.menuSpyCommand"
name="%spy-menu-command.name">
</command>
+ <command
+ categoryId="org.eclipse.pde.runtime.spy.commands.category"
+ description="%spy-layout-command.description"
+ id="org.eclipse.pde.runtime.spy.commands.layoutSpyCommand"
+ name="%spy-layout-command.name">
+ </command>
</extension>
<extension
point="org.eclipse.ui.commandImages">
@@ -57,6 +63,10 @@
commandId="org.eclipse.pde.runtime.spy.commands.menuSpyCommand"
icon="$nl$/icons/obj16/menuspy_obj.gif">
</image>
+ <image
+ commandId="org.eclipse.pde.runtime.spy.commands.layoutSpyCommand"
+ icon="$nl$/icons/obj16/layoutspy_obj.png">
+ </image>
</extension>
<extension
point="org.eclipse.ui.bindings">
@@ -87,6 +97,12 @@
schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
sequence="M3+M2+F2">
</key>
+ <key
+ commandId="org.eclipse.pde.runtime.spy.commands.layoutSpyCommand"
+ contextId="org.eclipse.ui.contexts.dialogAndWindow"
+ schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
+ sequence="M1+M2+M3+F9">
+ </key>
</extension>
<extension
point="org.eclipse.ui.handlers">
@@ -98,6 +114,10 @@
class="org.eclipse.pde.internal.runtime.spy.handlers.MenuSpyHandler"
commandId="org.eclipse.pde.runtime.spy.commands.menuSpyCommand">
</handler>
+ <handler
+ class="org.eclipse.pde.internal.runtime.spy.handlers.LayoutSpyHandler"
+ commandId="org.eclipse.pde.runtime.spy.commands.layoutSpyCommand">
+ </handler>
</extension>
</plugin>
diff --git a/ui/org.eclipse.pde.runtime/pom.xml b/ui/org.eclipse.pde.runtime/pom.xml
index 0c15e27..a0aa311 100644
--- a/ui/org.eclipse.pde.runtime/pom.xml
+++ b/ui/org.eclipse.pde.runtime/pom.xml
@@ -20,7 +20,7 @@
</parent>
<groupId>org.eclipse.pde</groupId>
<artifactId>org.eclipse.pde.runtime</artifactId>
- <version>3.5.100-SNAPSHOT</version>
+ <version>3.6.0-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
<properties>
diff --git a/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/PDERuntimePluginImages.java b/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/PDERuntimePluginImages.java
index 1b83d09..a12c0b3 100644
--- a/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/PDERuntimePluginImages.java
+++ b/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/PDERuntimePluginImages.java
@@ -39,6 +39,7 @@
public static final String IMG_CONTEXTID_OBJ = "contextid_obj.gif"; //$NON-NLS-1$
public static final String IMG_SAVE_IMAGE_AS_OBJ = "save_image_as_obj.gif"; //$NON-NLS-1$
public static final String IMG_COPY_QNAME = "cpyqual_menu.gif"; //$NON-NLS-1$
+ public static final String IMG_UP_NAV = "up_nav.png"; //$NON-NLS-1$
public static final ImageDescriptor CLASS_OBJ = create(PATH_OBJ, IMG_CLASS_OBJ);
public static final ImageDescriptor INTERFACE_OBJ = create(PATH_OBJ, IMG_INTERFACE_OBJ);
@@ -50,6 +51,7 @@
public static final ImageDescriptor CONTEXTID_OBJ = create(PATH_OBJ, IMG_CONTEXTID_OBJ);
public static final ImageDescriptor SAVE_IMAGE_AS_OBJ = create(PATH_OBJ, IMG_SAVE_IMAGE_AS_OBJ);
public static final ImageDescriptor COPY_QNAME = create(PATH_LCL, IMG_COPY_QNAME);
+ public static final ImageDescriptor UP_NAV = create(PATH_LCL, IMG_UP_NAV);
public static final ImageDescriptor DESC_REFRESH_DISABLED = create(PATH_DCL, "refresh.gif"); //$NON-NLS-1$
public static final ImageDescriptor DESC_REFRESH = create(PATH_LCL, "refresh.gif"); //$NON-NLS-1$
diff --git a/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/ControlSelector.java b/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/ControlSelector.java
new file mode 100644
index 0000000..e508ec6
--- /dev/null
+++ b/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/ControlSelector.java
@@ -0,0 +1,197 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Google Inc and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Stefan Xenos (Google) - initial API and implementation
+ *
+ *******************************************************************************/
+package org.eclipse.pde.internal.runtime.spy.dialogs;
+
+import java.util.ArrayList;
+import java.util.function.Consumer;
+import org.eclipse.core.databinding.observable.value.WritableValue;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jface.databinding.swt.WidgetSideEffects;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.jface.util.Geometry;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.graphics.*;
+import org.eclipse.swt.widgets.*;
+
+/**
+ * Creates an overlay that allows the user to select any SWT control by moving
+ * the mouse and clicking. Once the user selects the control or presses ESC, the
+ * overlay is closed.
+ */
+public class ControlSelector {
+ private static final int EDGE_SIZE = 4;
+
+ private Shell overlay;
+ private LocalResourceManager resources;
+ private Color selectionRectangleColor;
+ private Display display;
+ private WritableValue<@Nullable Control> currentSelection = new WritableValue<>(null, null);
+ private Region region;
+ private Consumer<@Nullable Control> callback;
+ private Listener moveFilter = this::mouseMove;
+ private Listener selectFilter = this::select;
+
+ /**
+ * Instantiates and opens the control selector.
+ *
+ * @param resultCallback
+ * callback that will be invoked when the overlay is closed. If
+ * the user selected a control, that control is passed to the
+ * callback. If the user cancelled the overlay, null is passed.
+ */
+ public ControlSelector(Consumer<@Nullable Control> resultCallback) {
+ this.callback = resultCallback;
+ display = Display.getCurrent();
+ overlay = new Shell(SWT.ON_TOP | SWT.NO_TRIM);
+ overlay.addPaintListener(this::paint);
+ resources = new LocalResourceManager(JFaceResources.getResources(), overlay);
+ selectionRectangleColor = resources.createColor(new RGB(255, 255, 0));
+ display.addFilter(SWT.MouseMove, moveFilter);
+ display.addFilter(SWT.MouseDown, selectFilter);
+ overlay.addDisposeListener(this::disposed);
+ region = new Region();
+ WidgetSideEffects.createFactory(overlay).create(currentSelection::getValue, this::updateRegion);
+ }
+
+ private void updateRegion(@Nullable Control newControl) {
+ if (newControl == null) {
+ overlay.setVisible(false);
+ return;
+ }
+ overlay.setBounds(newControl.getMonitor().getClientArea());
+ Rectangle parentBoundsWrtDisplay = GeometryUtil.getDisplayBounds(newControl);
+ Rectangle parentBoundsWrtOverlay = Geometry.toControl(overlay, parentBoundsWrtDisplay);
+ Rectangle innerBoundsWrtOverlay = Geometry.copy(parentBoundsWrtOverlay);
+ Geometry.expand(innerBoundsWrtOverlay, -EDGE_SIZE, -EDGE_SIZE, -EDGE_SIZE, -EDGE_SIZE);
+ region.dispose();
+ region = new Region();
+ region.add(parentBoundsWrtOverlay);
+ region.subtract(innerBoundsWrtOverlay);
+ overlay.setRegion(region);
+ overlay.setVisible(true);
+ }
+
+ private void disposed(DisposeEvent e) {
+ currentSelection.dispose();
+ region.dispose();
+ display.removeFilter(SWT.MouseMove, moveFilter);
+ display.removeFilter(SWT.MouseDown, selectFilter);
+ }
+
+ private void select(Event e) {
+ closeWithResult(this.currentSelection.getValue());
+ e.doit = false;
+ }
+
+ private void closeWithResult(@Nullable Control result) {
+ display.removeFilter(SWT.MouseMove, moveFilter);
+ display.removeFilter(SWT.MouseDown, selectFilter);
+ overlay.dispose();
+ callback.accept(result);
+ }
+
+ /**
+ * Finds and returns the most specific SWT control at the given location.
+ * (Note: this does a DFS on the SWT widget hierarchy, which is slow).
+ *
+ * @param displayToSearch
+ * @param locationToFind
+ * @return the most specific SWT control at the given location
+ */
+ public static Control findControl(Display displayToSearch, Shell toIgnore, Point locationToFind) {
+ Shell[] shells = displayToSearch.getShells();
+
+ ArrayList<Shell> shellList = new ArrayList<>();
+ for (Shell next : shells) {
+ if (next == toIgnore) {
+ continue;
+ }
+ shellList.add(next);
+ }
+
+ shells = shellList.toArray(new Shell[shellList.size()]);
+
+ return findControl(shells, locationToFind);
+ }
+
+ /**
+ * Finds the control at the given location.
+ *
+ * @param toSearch
+ * @param locationToFind
+ * location (in display coordinates)
+ * @return the control at the given location
+ */
+ public static Control findControl(Composite toSearch, Point locationToFind) {
+ Control[] children = toSearch.getChildren();
+
+ return findControl(children, locationToFind);
+ }
+
+ /**
+ * Searches the given list of controls for a control containing the given
+ * point. If the array contains any composites, those composites will be
+ * recursively searched to find the most specific child that contains the
+ * point.
+ *
+ * @param toSearch
+ * an array of composites
+ * @param locationToFind
+ * a point (in display coordinates)
+ * @return the most specific Control that overlaps the given point, or null
+ * if none
+ */
+ public static Control findControl(Control[] toSearch, Point locationToFind) {
+ for (int idx = toSearch.length - 1; idx >= 0; idx--) {
+ Control next = toSearch[idx];
+
+ if (!next.isDisposed() && next.isVisible()) {
+
+ Rectangle bounds = GeometryUtil.getDisplayBounds(next);
+
+ if (bounds.contains(locationToFind)) {
+ if (next instanceof Composite) {
+ Control result = findControl((Composite) next, locationToFind);
+
+ if (result != null) {
+ return result;
+ }
+ }
+
+ return next;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private void mouseMove(Event e) {
+ Point globalPoint = new Point(e.x, e.y);
+ if (e.widget instanceof Control) {
+ Control control = (Control) e.widget;
+
+ globalPoint = control.toDisplay(globalPoint);
+ }
+ Control control = findControl(Display.getCurrent(), overlay, globalPoint);
+ currentSelection.setValue(control);
+ }
+
+ protected void paint(PaintEvent e) {
+ e.gc.setBackground(selectionRectangleColor);
+ e.gc.fillRectangle(overlay.getClientArea());
+ }
+
+}
diff --git a/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/GeometryUtil.java b/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/GeometryUtil.java
new file mode 100644
index 0000000..57d5ae8
--- /dev/null
+++ b/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/GeometryUtil.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Google Inc and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Stefan Xenos (Google) - initial API and implementation
+ *
+ *******************************************************************************/
+package org.eclipse.pde.internal.runtime.spy.dialogs;
+
+import org.eclipse.jface.util.Geometry;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+
+public class GeometryUtil {
+ public static Rectangle getDisplayBounds(Control boundsControl) {
+ Control parent = boundsControl.getParent();
+ if (parent == null || boundsControl instanceof Shell) {
+ return boundsControl.getBounds();
+ }
+
+ return Geometry.toDisplay(parent, boundsControl.getBounds());
+ }
+
+ public static Rectangle extrudeEdge(Rectangle innerBoundsWrtOverlay, int distanceToTop, int side) {
+ if (distanceToTop <= 0) {
+ return new Rectangle(0, 0, 0, 0);
+ }
+ return Geometry.getExtrudedEdge(innerBoundsWrtOverlay, distanceToTop, side);
+ }
+
+ public static int getBottom(Rectangle rect) {
+ return rect.y + rect.height;
+ }
+
+ public static int getRight(Rectangle rect) {
+ return rect.x + rect.width;
+ }
+}
diff --git a/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/LayoutSpyDialog.java b/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/LayoutSpyDialog.java
new file mode 100644
index 0000000..17252c3
--- /dev/null
+++ b/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/LayoutSpyDialog.java
@@ -0,0 +1,735 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Google Inc and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Stefan Xenos (Google) - initial API and implementation
+ *
+ *******************************************************************************/
+package org.eclipse.pde.internal.runtime.spy.dialogs;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.List;
+import org.eclipse.core.databinding.observable.list.ComputedList;
+import org.eclipse.core.databinding.observable.sideeffect.ISideEffectFactory;
+import org.eclipse.core.databinding.observable.value.WritableValue;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jface.databinding.swt.*;
+import org.eclipse.jface.databinding.viewers.*;
+import org.eclipse.jface.layout.*;
+import org.eclipse.jface.resource.*;
+import org.eclipse.jface.util.Geometry;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.pde.internal.runtime.PDERuntimePluginImages;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.graphics.*;
+import org.eclipse.swt.layout.*;
+import org.eclipse.swt.widgets.*;
+
+/**
+ * Implementation of the "layout spy" dialog, a diagnostic tool for fixing bugs
+ * related to control positioning and the implementation of SWT {@link Control}s
+ * and {@link Layout}s.
+ */
+public class LayoutSpyDialog {
+ private static final int EDGE_SIZE = 4;
+ private static final RGB SELECTED_PARENT_OVERLAY_COLOR = new RGB(255, 0, 0);
+ private static final RGB SELECTED_CHILD_OVERLAY_COLOR = new RGB(255, 255, 0);
+
+ /**
+ * Value used to indicate an unknown hint value
+ */
+ private static final int UNKNOWN = -2;
+ private Shell shell;
+
+ // Controls
+ private TableViewer childList;
+ private Text details;
+ private Button selectWidgetButton;
+ private Button goUpButton;
+ private Button goDownButton;
+ private Shell overlay;
+
+ // Model
+ private WritableValue<@Nullable Composite> parentControl = new WritableValue<>(null, null);
+ private WritableValue<Boolean> controlSelectorOpen = new WritableValue<>(Boolean.FALSE, null);
+ private ComputedList<Control> listContents;
+ private IViewerObservableValue selectedChild;
+ private Color parentRectangleColor;
+ private Color childRectangleColor;
+ private ResourceManager resources;
+ private Region region;
+ private ISWTObservableValue overlayEnabled;
+ private Image upImage;
+ private Text diagnostics;
+
+ /**
+ * Creates the dialog but does not make it visible.
+ *
+ * @param parentShell
+ * the parent shell
+ */
+ public LayoutSpyDialog(Shell parentShell) {
+ overlay = new Shell(SWT.ON_TOP | SWT.NO_TRIM);
+ {
+ overlay.addPaintListener(this::paintOverlay);
+ region = new Region();
+ overlay.addDisposeListener((DisposeEvent) -> {
+ region.dispose();
+ });
+ overlay.setRegion(region);
+ }
+
+ shell = new Shell(parentShell, SWT.SHELL_TRIM);
+
+ resources = new LocalResourceManager(JFaceResources.getResources(), shell);
+ parentRectangleColor = resources.createColor(SELECTED_PARENT_OVERLAY_COLOR);
+ childRectangleColor = resources.createColor(SELECTED_CHILD_OVERLAY_COLOR);
+ upImage = resources.createImage(PDERuntimePluginImages.UP_NAV);
+
+ Composite infoRegion = new Composite(shell, SWT.NONE);
+ {
+ Composite headerRegion = new Composite(infoRegion, SWT.NONE);
+ {
+ Button upButton = new Button(headerRegion, SWT.PUSH | SWT.CENTER);
+ upButton.setImage(upImage);
+ upButton.addListener(SWT.Selection, event -> goUp());
+ GridDataFactory.fillDefaults().applyTo(upButton);
+
+ Label childrenLabel = new Label(headerRegion, SWT.NONE);
+ childrenLabel.setText(Messages.LayoutSpyDialog_label_children);
+ }
+ GridLayoutFactory.fillDefaults().numColumns(2).generateLayout(headerRegion);
+
+ Label detailsLabel = new Label(infoRegion, SWT.NONE);
+ detailsLabel.setText(Messages.LayoutSpyDialog_label_layout);
+
+ childList = new TableViewer(infoRegion);
+ details = new Text(infoRegion, SWT.READ_ONLY | SWT.MULTI | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
+ GridDataFactory.fillDefaults().hint(300, 300).grab(true, true).applyTo(details);
+
+ GridLayoutFactory.fillDefaults().numColumns(2).equalWidth(true).generateLayout(infoRegion);
+ }
+
+ diagnostics = new Text(shell, SWT.READ_ONLY | SWT.MULTI | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
+ GridDataFactory.fillDefaults().hint(300, 300).grab(true, true).applyTo(diagnostics);
+
+ Button showOverlayButton = new Button(shell, SWT.CHECK);
+ showOverlayButton.setText(Messages.LayoutSpyDialog_button_show_overlay);
+
+ Composite buttonBar = new Composite(shell, SWT.NONE);
+ {
+ selectWidgetButton = new Button(buttonBar, SWT.PUSH);
+ selectWidgetButton.setText(Messages.LayoutSpyDialog_button_select_control);
+ goUpButton = new Button(buttonBar, SWT.PUSH);
+ goUpButton.setText(Messages.LayoutSpyDialog_button_open_parent);
+ goDownButton = new Button(buttonBar, SWT.PUSH);
+ goDownButton.setText(Messages.LayoutSpyDialog_button_open_child);
+
+ GridLayoutFactory.fillDefaults().numColumns(3).generateLayout(buttonBar);
+ }
+ GridDataFactory.fillDefaults().align(SWT.CENTER, SWT.CENTER).applyTo(buttonBar);
+
+ GridLayoutFactory.fillDefaults().margins(LayoutConstants.getMargins()).generateLayout(shell);
+
+ // Attach listeners
+ shell.addDisposeListener(event -> disposed());
+ selectWidgetButton.addListener(SWT.Selection, event -> selectControl());
+ goUpButton.addListener(SWT.Selection, event -> goUp());
+ goDownButton.addListener(SWT.Selection, event -> goDown());
+ childList.addOpenListener(event -> {
+ goDown();
+ });
+
+ // Set up the model
+ selectedChild = ViewerProperties.singleSelection().observe(childList);
+ overlayEnabled = WidgetProperties.selection().observe(showOverlayButton);
+ childList.setContentProvider(new ObservableListContentProvider());
+ listContents = new ComputedList<Control>() {
+ @Override
+ protected List<Control> calculate() {
+ Composite control = parentControl.getValue();
+ if (control == null) {
+ return Arrays.asList(Display.getCurrent().getShells());
+ }
+ return Arrays.asList(control.getChildren());
+ }
+ };
+ childList.setInput(listContents);
+ ISideEffectFactory sideEffectFactory = WidgetSideEffects.createFactory(shell);
+ sideEffectFactory.create(this::computeParentInfo, details::setText);
+ sideEffectFactory.create(this::computeChildInfo, diagnostics::setText);
+ sideEffectFactory.create(this::updateOverlay);
+
+ openComposite(parentShell);
+ }
+
+ /**
+ * Opens the dialog box, revealing it to the user.
+ */
+ public void open() {
+ this.shell.pack();
+ this.shell.open();
+ }
+
+ /**
+ * Disposes the dialog box.
+ */
+ public void close() {
+ this.shell.dispose();
+ }
+
+ /**
+ * Invoked as a callback when the main shell is disposed.
+ */
+ private void disposed() {
+ listContents.dispose();
+ selectedChild.dispose();
+ parentControl.dispose();
+
+ overlay.dispose();
+ }
+
+ private void openComposite(Composite composite) {
+ parentControl.setValue(composite);
+ }
+
+ /**
+ * Returns the currently-selected child control or null if none.
+ */
+ private @Nullable Control getSelectedChild() {
+ return (@Nullable Control) selectedChild.getValue();
+ }
+
+ /**
+ * Opens the given control in the layout spy.
+ */
+ @SuppressWarnings("unchecked")
+ private void openControl(Control control) {
+ Composite parent = control.getParent();
+
+ if (parent == null) {
+ if (control instanceof Composite) {
+ parentControl.setValue((Composite) control);
+ }
+ } else {
+ parentControl.setValue(parent);
+ selectedChild.setValue(control);
+ }
+ }
+
+ // Overlay management
+ // -----------------------------------------------------------------
+
+ /**
+ * This callback is used to update the bounds and visible region for the
+ * overlay shell. It is used as part of a side-effect, so if it makes use of
+ * any tracked getters, it will automatically be invoked again whenever one
+ * of those tracked getters changes state.
+ *
+ * @TrackedGetter
+ */
+ public void updateOverlay() {
+ @Nullable
+ Composite parent = parentControl.getValue();
+
+ boolean enabled = Boolean.TRUE.equals(overlayEnabled.getValue());
+
+ overlay.setVisible(parent != null && !controlSelectorOpen.getValue() && enabled);
+ if (parent == null) {
+ return;
+ }
+ Shell shell = parent.getShell();
+ Rectangle outerBounds = Geometry.copy(shell.getBounds());
+ overlay.setBounds(outerBounds);
+ Rectangle parentBoundsWrtDisplay = GeometryUtil.getDisplayBounds(parent);
+
+ Rectangle parentBoundsWrtOverlay = Geometry.toControl(overlay, parentBoundsWrtDisplay);
+ Rectangle innerBoundsWrtOverlay = Geometry.copy(parentBoundsWrtOverlay);
+ Geometry.expand(innerBoundsWrtOverlay, -EDGE_SIZE, -EDGE_SIZE, -EDGE_SIZE, -EDGE_SIZE);
+ region.dispose();
+ region = new Region();
+ @Nullable
+ Control child = (@Nullable Control) selectedChild.getValue();
+ if (child != null) {
+ Rectangle childBoundsWrtOverlay = Geometry.toControl(overlay, GeometryUtil.getDisplayBounds(child));
+ Rectangle childInnerBoundsWrtOverlay = Geometry.copy(childBoundsWrtOverlay);
+ Geometry.expand(childInnerBoundsWrtOverlay, -EDGE_SIZE, -EDGE_SIZE, -EDGE_SIZE, -EDGE_SIZE);
+ region.add(parentBoundsWrtOverlay);
+ int distanceToTop = childBoundsWrtOverlay.y - innerBoundsWrtOverlay.y;
+ subtractRect(region, GeometryUtil.extrudeEdge(innerBoundsWrtOverlay, distanceToTop, SWT.TOP));
+ int distanceToLeft = childBoundsWrtOverlay.x - innerBoundsWrtOverlay.x;
+ subtractRect(region, GeometryUtil.extrudeEdge(innerBoundsWrtOverlay, distanceToLeft, SWT.LEFT));
+ int distanceToRight = GeometryUtil.getRight(innerBoundsWrtOverlay) - GeometryUtil.getRight(childBoundsWrtOverlay);
+ subtractRect(region, GeometryUtil.extrudeEdge(innerBoundsWrtOverlay, distanceToRight, SWT.RIGHT));
+ int distanceToBottom = GeometryUtil.getBottom(innerBoundsWrtOverlay) - GeometryUtil.getBottom(childBoundsWrtOverlay);
+ subtractRect(region, GeometryUtil.extrudeEdge(innerBoundsWrtOverlay, distanceToBottom, SWT.BOTTOM));
+
+ subtractRect(region, childInnerBoundsWrtOverlay);
+ } else {
+ region.add(parentBoundsWrtOverlay);
+ region.subtract(innerBoundsWrtOverlay);
+ }
+
+ overlay.redraw();
+ overlay.setRegion(region);
+ }
+
+ /**
+ * Paint callback for the overlay shell. This draws rectangles around the
+ * selected layout and the selected child.
+ */
+ protected void paintOverlay(PaintEvent e) {
+ @Nullable
+ Composite parent = parentControl.getValue();
+ if (parent == null) {
+ return;
+ }
+ int halfSize = EDGE_SIZE / 2;
+ Rectangle parentDisplayBounds = GeometryUtil.getDisplayBounds(parent);
+ Rectangle parentBoundsWrtOverlay = Geometry.toControl(overlay, parentDisplayBounds);
+ Geometry.expand(parentBoundsWrtOverlay, -halfSize, -halfSize, -halfSize, -halfSize);
+
+ @Nullable
+ Control child = (@Nullable Control) selectedChild.getValue();
+ e.gc.setLineWidth(EDGE_SIZE);
+ e.gc.setForeground(parentRectangleColor);
+ e.gc.drawRectangle(parentBoundsWrtOverlay.x, parentBoundsWrtOverlay.y, parentBoundsWrtOverlay.width,
+ parentBoundsWrtOverlay.height);
+
+ if (child != null) {
+ Rectangle childBoundsWrtOverlay = Geometry.toControl(overlay, GeometryUtil.getDisplayBounds(child));
+ Geometry.expand(childBoundsWrtOverlay, -halfSize, -halfSize, -halfSize, -halfSize);
+ e.gc.setForeground(childRectangleColor);
+ e.gc.drawRectangle(childBoundsWrtOverlay.x, childBoundsWrtOverlay.y, childBoundsWrtOverlay.width,
+ childBoundsWrtOverlay.height);
+ }
+ }
+
+ // User gesture callbacks
+ // -----------------------------------------------------------
+
+ /**
+ * Invoked when the user clicks the "select control" button. It opens some
+ * UI that allows the user to select a new input control for the layout spy.
+ */
+ private void selectControl() {
+ this.controlSelectorOpen.setValue(true);
+ this.shell.setVisible(false);
+ new ControlSelector((@Nullable Control control) -> {
+ if (control != null) {
+ openControl(control);
+ }
+ this.controlSelectorOpen.setValue(false);
+ this.shell.setVisible(true);
+ });
+ }
+
+ /**
+ * Invoked when the user clicks the "go up" button, which opens the parent.
+ */
+ @SuppressWarnings("unchecked")
+ private void goUp() {
+ @Nullable
+ Composite parent = parentControl.getValue();
+ if (parent == null) {
+ return;
+ }
+ Composite ancestor = parent.getParent();
+ openComposite(ancestor);
+ this.selectedChild.setValue(parent);
+ }
+
+ /**
+ * Invoked when the user clicks the "go down" button, which opens the
+ * selected child.
+ */
+ private void goDown() {
+ Control child = getSelectedChild();
+ if (child instanceof Composite) {
+ Composite composite = (Composite) child;
+ openComposite(composite);
+ }
+ }
+
+ // Utility functions -----------------------------------------------------
+
+ /**
+ * Subtracts the given rectangle from the given region unless the rectangle
+ * is empty.
+ */
+ private static void subtractRect(Region region, Rectangle rect) {
+ if (rect.isEmpty()) {
+ return;
+ }
+ region.subtract(rect);
+ }
+
+ private String getWarningMessage(String string) {
+ return NLS.bind(Messages.LayoutSpyDialog_warning_prefix, string);
+ }
+
+ private static String printHint(int hint) {
+ if (hint == SWT.DEFAULT) {
+ return "SWT.DEFAULT"; //$NON-NLS-1$
+ }
+ return Integer.toString(hint);
+ }
+
+ private static String printPoint(Point toPrint) {
+ return NLS.bind("({0}, {1})", new Object[] { toPrint.x, toPrint.y }); //$NON-NLS-1$
+ }
+
+ // Control classification ------------------------------------------------
+
+ private static boolean isHorizontallyScrollable(Control child) {
+ return (child.getStyle() & SWT.H_SCROLL) != 0;
+ }
+
+ private static boolean isVerticallyScrollable(Control child) {
+ return (child.getStyle() & SWT.V_SCROLL) != 0;
+ }
+
+ /**
+ * Computes the values that should be subtracted off the width and height
+ * hints from computeSize on the given control.
+ */
+ private static Point computeHintAdjustment(Control control) {
+ int widthAdjustment;
+ int heightAdjustment;
+ if (control instanceof Scrollable) {
+ // For composites, subtract off the trim size
+ Scrollable composite = (Scrollable) control;
+ Rectangle trim = composite.computeTrim(0, 0, 0, 0);
+
+ widthAdjustment = trim.width;
+ heightAdjustment = trim.height;
+ } else {
+ // For non-composites, subtract off 2 * the border size
+ widthAdjustment = control.getBorderWidth() * 2;
+ heightAdjustment = widthAdjustment;
+ }
+
+ return new Point(widthAdjustment, heightAdjustment);
+ }
+
+ /**
+ * Returns true if the given control is a composite which can expand in the
+ * given dimension. Returns false if the control either cannot expand in the
+ * given dimension or if its growable characteristics cannot be computed in
+ * that dimension.
+ */
+ private static boolean isGrowableLayout(Control control, boolean horizontal) {
+ if (control instanceof Composite) {
+ Composite composite = (Composite) control;
+
+ Layout theLayout = composite.getLayout();
+ if (theLayout instanceof GridLayout) {
+ Control[] children = composite.getChildren();
+ for (int i = 0; i < children.length; i++) {
+ Control child = children[i];
+
+ GridData data = (GridData) child.getLayoutData();
+
+ if (data != null) {
+ if (horizontal) {
+ if (data.grabExcessHorizontalSpace) {
+ return true;
+ }
+ } else {
+ if (data.grabExcessVerticalSpace) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true iff another visible widget in the same shell overlaps the
+ * given control.
+ */
+ private static boolean overlapsSibling(Control toFind) {
+ Composite parent = toFind.getParent();
+ Control current = toFind;
+ Rectangle displayBounds = GeometryUtil.getDisplayBounds(toFind);
+
+ while (parent != null && !(parent instanceof Shell)) {
+ for (Control nextSibling : parent.getChildren()) {
+ if (nextSibling == current) {
+ continue;
+ }
+ if (!nextSibling.isVisible()) {
+ continue;
+ }
+ Rectangle nextSiblingBounds = GeometryUtil.getDisplayBounds(nextSibling);
+ if (nextSiblingBounds.intersects(displayBounds)) {
+ return true;
+ }
+ }
+ current = parent;
+ parent = parent.getParent();
+ }
+ return false;
+ }
+
+ /**
+ * Computes the string that will be shown in the text box which displays
+ * information about the selected child. This is a tracked getter -- if it
+ * reads from a databinding observable, the text box will automatically
+ * refresh in response to changes in that observable.
+ *
+ * @TrackedGetter
+ */
+ private String computeChildInfo() {
+ StringBuilder builder = new StringBuilder();
+ Control child = getSelectedChild();
+
+ if (child != null) {
+ builder.append(child.getClass().getName());
+ builder.append("\n\n"); //$NON-NLS-1$
+
+ int widthHintFromLayoutData = UNKNOWN;
+ int heightHintFromLayoutData = UNKNOWN;
+ Object layoutData = child.getLayoutData();
+ if (layoutData == null) {
+ builder.append("getLayoutData() == null\n"); //$NON-NLS-1$
+ } else if (layoutData instanceof GridData) {
+ GridData grid = (GridData) layoutData;
+ builder.append(GridDataFactory.createFrom(grid));
+ widthHintFromLayoutData = grid.widthHint;
+ heightHintFromLayoutData = grid.heightHint;
+
+ if (!grid.grabExcessHorizontalSpace) {
+ if (isHorizontallyScrollable(child) || isGrowableLayout(child, true)) {
+ builder.append(getWarningMessage(
+ Messages.LayoutSpyDialog_warning_grab_horizontally_scrolling));
+ }
+ }
+ if (!grid.grabExcessVerticalSpace) {
+ if (isVerticallyScrollable(child) || isGrowableLayout(child, false)) {
+ builder.append(getWarningMessage(
+ Messages.LayoutSpyDialog_warning_grab_vertical_scrolling));
+ }
+ }
+ } else if (layoutData instanceof FormData) {
+ FormData data = (FormData) layoutData;
+
+ widthHintFromLayoutData = data.width;
+ heightHintFromLayoutData = data.height;
+ } else {
+ describeObject(builder, "data", layoutData); //$NON-NLS-1$
+ }
+
+ if (isHorizontallyScrollable(child)) {
+ if (widthHintFromLayoutData == SWT.DEFAULT) {
+ builder.append(getWarningMessage(Messages.LayoutSpyDialog_warning_hint_for_horizontally_scrollable));
+ }
+ }
+
+ if (isVerticallyScrollable(child)) {
+ if (heightHintFromLayoutData == SWT.DEFAULT) {
+ builder.append(getWarningMessage(Messages.LayoutSpyDialog_warning_hint_for_vertically_scrollable));
+ }
+ }
+
+ builder.append("\n"); //$NON-NLS-1$
+
+ // Print the current dimensions
+ Rectangle bounds = child.getBounds();
+ builder.append(NLS.bind("getBounds() = {0}", bounds.toString())); //$NON-NLS-1$
+ builder.append("\n"); //$NON-NLS-1$
+
+ Point adjustment = computeHintAdjustment(child);
+
+ int widthHint = Math.max(0, bounds.width - adjustment.x);
+ int heightHint = Math.max(0, bounds.height - adjustment.y);
+
+ builder.append(NLS.bind("widthAdjustment = {0}, heightAdjustment = {1}", //$NON-NLS-1$
+ new Object[] { adjustment.x, adjustment.y }));
+ builder.append("\n\n"); //$NON-NLS-1$
+
+ // Print the default size
+ Point defaultSize = child.computeSize(SWT.DEFAULT, SWT.DEFAULT, false);
+ builder.append(NLS.bind("computeSize(SWT.DEFAULT, SWT.DEFAULT, false) = {0}", printPoint(defaultSize))); //$NON-NLS-1$
+ builder.append("\n"); //$NON-NLS-1$
+
+ // Print the preferred horizontally-wrapped size:
+ Point hWrappedSize = child.computeSize(widthHint, SWT.DEFAULT, false);
+ builder.append(NLS.bind("computeSize({0} - widthAdjustment, SWT.DEFAULT, false) = {1}", //$NON-NLS-1$
+ new Object[] { widthHint, printPoint(hWrappedSize) }));
+ builder.append("\n"); //$NON-NLS-1$
+
+ // Print the preferred vertically-wrapped size:
+ Point vWrappedSize = child.computeSize(SWT.DEFAULT, heightHint, false);
+ builder.append(NLS.bind("computeSize(SWT.DEFAULT, {0} - heightAdjustment, false) = {1}", //$NON-NLS-1$
+ new Object[] { heightHint, printPoint(vWrappedSize) }));
+ builder.append("\n"); //$NON-NLS-1$
+
+ // Check for warnings
+ Point noOpSize = child.computeSize(widthHint, heightHint, false);
+ if (noOpSize.x != bounds.width || noOpSize.y != bounds.height) {
+ builder.append(getWarningMessage(NLS.bind(Messages.LayoutSpyDialog_warning_unexpected_compute_size,
+ new Object[] { printHint(widthHint), printHint(heightHint), printPoint(noOpSize) })));
+ }
+
+ if (bounds.height < hWrappedSize.y) {
+ builder.append(
+ getWarningMessage(Messages.LayoutSpyDialog_warning_shorter_than_preferred_size));
+ }
+
+ printReasonControlIsInvisible(builder, child);
+ }
+ return builder.toString();
+ }
+
+ /**
+ * If the control cannot be seen by the user, this method adds a warning
+ * message to the given builder explaining the reason why the control cannot
+ * be seen.
+ */
+ private void printReasonControlIsInvisible(StringBuilder builder, Control control) {
+ if (!control.isVisible()) {
+ builder.append(getWarningMessage("isVisible() == false")); //$NON-NLS-1$
+ return;
+ }
+
+ Rectangle bounds = control.getBounds();
+ if (bounds.isEmpty()) {
+ builder.append(getWarningMessage(Messages.LayoutSpyDialog_warning_zero_size));
+ return;
+ }
+
+ Rectangle displayBounds = GeometryUtil.getDisplayBounds(control);
+
+ Composite parent = control.getParent();
+ if (parent != null) {
+ Rectangle parentDisplayBounds = GeometryUtil.getDisplayBounds(parent);
+
+ Rectangle intersection = displayBounds.intersection(parentDisplayBounds);
+ if (intersection.isEmpty()) {
+ builder.append(getWarningMessage(Messages.LayoutSpyDialog_warning_bounds_outside_parent));
+ return;
+ }
+
+ if (intersection.width < bounds.width || intersection.height < bounds.height) {
+ builder.append(getWarningMessage(Messages.LayoutSpyDialog_warning_control_partially_clipped));
+ return;
+ }
+
+ if (overlapsSibling(control)) {
+ builder.append(getWarningMessage(Messages.LayoutSpyDialog_warning_control_overlaps_siblings));
+ return;
+ }
+ }
+ }
+
+ /**
+ * Computes the string that will be shown in the text box which displays
+ * information about the selected layout. This is a tracked getter: if it
+ * reads from an observable, the text box will update automatically when the
+ * observable changes.
+ *
+ * @TrackedGetter
+ */
+ private String computeParentInfo() {
+ StringBuilder builder = new StringBuilder();
+ @Nullable
+ Composite parent = parentControl.getValue();
+
+ if (parent != null) {
+ builder.append(parent.getClass().getName());
+ builder.append("\n\n"); //$NON-NLS-1$
+
+ Rectangle parentBounds = GeometryUtil.getDisplayBounds(parent);
+ Layout layout = parent.getLayout();
+
+ if (layout != null) {
+ if (layout instanceof GridLayout) {
+ GridLayout grid = (GridLayout) layout;
+ builder.append(GridLayoutFactory.createFrom(grid));
+
+ boolean hasVerticallyTruncadeControls = false;
+ boolean hasHorizontallyTruncadeControls = false;
+
+ boolean hasHorizontalGrab = false;
+ boolean hasVerticalGrab = false;
+ for (Control next : parent.getChildren()) {
+ @Nullable
+ GridData data = (GridData) next.getLayoutData();
+ if (data == null) {
+ continue;
+ }
+
+ Rectangle childBounds = GeometryUtil.getDisplayBounds(parent);
+ Rectangle intersection = childBounds.intersection(parentBounds);
+
+ if (intersection.width < childBounds.width) {
+ hasHorizontallyTruncadeControls = true;
+ }
+
+ if (intersection.height < childBounds.height) {
+ hasVerticallyTruncadeControls = true;
+ }
+
+ hasHorizontalGrab = hasHorizontalGrab || data.grabExcessHorizontalSpace;
+ hasVerticalGrab = hasVerticalGrab || data.grabExcessVerticalSpace;
+ }
+
+ if (hasHorizontallyTruncadeControls && !hasHorizontalGrab) {
+ builder.append(getWarningMessage(
+ Messages.LayoutSpyDialog_warning_not_grabbing_horizontally));
+ }
+
+ if (hasVerticallyTruncadeControls && !hasVerticalGrab) {
+ builder.append(getWarningMessage(
+ Messages.LayoutSpyDialog_warning_not_grabbing_vertically));
+ }
+ } else {
+ describeObject(builder, "layout", layout); //$NON-NLS-1$
+ }
+ }
+ } else {
+ builder.append(Messages.LayoutSpyDialog_label_no_parent_control_selected);
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Uses reflection to print the values of the given object's public fields.
+ */
+ void describeObject(StringBuilder result, String variableName, Object toDescribe) {
+ @SuppressWarnings("rawtypes")
+ Class clazz = toDescribe.getClass();
+ result.append(clazz.getName());
+ result.append(" "); //$NON-NLS-1$
+ result.append(variableName);
+ result.append(";\n"); //$NON-NLS-1$
+ Field[] fields = clazz.getFields();
+
+ for (Field nextField : fields) {
+ int modifiers = nextField.getModifiers();
+ if (!Modifier.isPublic(modifiers)) {
+ continue;
+ }
+ try {
+ String next = variableName + "." + nextField.getName() + " = " + nextField.get(toDescribe) + ";"; //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
+ result.append(next);
+ result.append("\n"); //$NON-NLS-1$
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ // Don't care
+ }
+ }
+ }
+}
diff --git a/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/Messages.java b/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/Messages.java
new file mode 100644
index 0000000..145b1e2
--- /dev/null
+++ b/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/Messages.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Google Inc and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Stefan Xenos (Google) - initial API and implementation
+ *
+ *******************************************************************************/
+package org.eclipse.pde.internal.runtime.spy.dialogs;
+
+import org.eclipse.osgi.util.NLS;
+
+public class Messages extends NLS {
+ private static final String BUNDLE_NAME = "org.eclipse.pde.internal.runtime.spy.dialogs.messages"; //$NON-NLS-1$
+ public static String LayoutSpyDialog_button_open_child;
+ public static String LayoutSpyDialog_button_open_parent;
+ public static String LayoutSpyDialog_button_select_control;
+ public static String LayoutSpyDialog_button_show_overlay;
+ public static String LayoutSpyDialog_label_children;
+ public static String LayoutSpyDialog_label_layout;
+ public static String LayoutSpyDialog_label_no_parent_control_selected;
+ public static String LayoutSpyDialog_warning_bounds_outside_parent;
+ public static String LayoutSpyDialog_warning_control_overlaps_siblings;
+ public static String LayoutSpyDialog_warning_control_partially_clipped;
+ public static String LayoutSpyDialog_warning_grab_horizontally_scrolling;
+ public static String LayoutSpyDialog_warning_grab_vertical_scrolling;
+ public static String LayoutSpyDialog_warning_hint_for_horizontally_scrollable;
+ public static String LayoutSpyDialog_warning_hint_for_vertically_scrollable;
+ public static String LayoutSpyDialog_warning_not_grabbing_horizontally;
+ public static String LayoutSpyDialog_warning_not_grabbing_vertically;
+ public static String LayoutSpyDialog_warning_prefix;
+ public static String LayoutSpyDialog_warning_shorter_than_preferred_size;
+ public static String LayoutSpyDialog_warning_unexpected_compute_size;
+ public static String LayoutSpyDialog_warning_zero_size;
+ static {
+ // initialize resource bundle
+ NLS.initializeMessages(BUNDLE_NAME, Messages.class);
+ }
+
+ private Messages() {
+ }
+}
diff --git a/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/messages.properties b/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/messages.properties
new file mode 100644
index 0000000..55cbd25
--- /dev/null
+++ b/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/dialogs/messages.properties
@@ -0,0 +1,20 @@
+LayoutSpyDialog_button_open_child=Open &Child
+LayoutSpyDialog_button_open_parent=Open &Parent
+LayoutSpyDialog_button_select_control=Select &Control
+LayoutSpyDialog_button_show_overlay=Show &overlay
+LayoutSpyDialog_label_children=Children:
+LayoutSpyDialog_label_layout=Layout:
+LayoutSpyDialog_label_no_parent_control_selected=No parent control selected
+LayoutSpyDialog_warning_bounds_outside_parent=Control is located outside the bounds of its parent
+LayoutSpyDialog_warning_control_overlaps_siblings=Control overlaps one of its visible siblings
+LayoutSpyDialog_warning_control_partially_clipped=Control is being partially clipped by its parent
+LayoutSpyDialog_warning_grab_horizontally_scrolling=The grab horizontal flag is recommended for horizontally resizable controls
+LayoutSpyDialog_warning_grab_vertical_scrolling=The grab vertical flag is recommended for vertically resizable controls
+LayoutSpyDialog_warning_hint_for_horizontally_scrollable=Horizontally scrollable controls should have a width hint
+LayoutSpyDialog_warning_hint_for_vertically_scrollable=Vertically scrollable controls should have a height hint
+LayoutSpyDialog_warning_not_grabbing_horizontally=None of the controls use the horizontal grab flag, which may be the reason why some of the controls in the layout are truncated horizontally.
+LayoutSpyDialog_warning_not_grabbing_vertically=None of the controls use the vertical grab flag, which may be the reason why some of the controls in the layout are truncated vertically.
+LayoutSpyDialog_warning_prefix=WARNING: {0}\n
+LayoutSpyDialog_warning_shorter_than_preferred_size=The vertical size of this widget is shorter than its preferred size.
+LayoutSpyDialog_warning_unexpected_compute_size=computeSize({0}, {1}) returned unexpected size of {2}
+LayoutSpyDialog_warning_zero_size=Control has 0 size
diff --git a/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/handlers/LayoutSpyHandler.java b/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/handlers/LayoutSpyHandler.java
new file mode 100644
index 0000000..556fa48
--- /dev/null
+++ b/ui/org.eclipse.pde.runtime/src/org/eclipse/pde/internal/runtime/spy/handlers/LayoutSpyHandler.java
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Google Inc and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Stefan Xenos (Google) - initial API and implementation
+ *
+ *******************************************************************************/
+package org.eclipse.pde.internal.runtime.spy.handlers;
+
+import org.eclipse.core.commands.*;
+import org.eclipse.pde.internal.runtime.spy.dialogs.LayoutSpyDialog;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+public class LayoutSpyHandler extends AbstractHandler {
+ private LayoutSpyDialog popupDialog;
+
+ @Override
+ public Object execute(ExecutionEvent event) throws ExecutionException {
+ if (popupDialog != null) {
+ popupDialog.close();
+ }
+
+ Shell shell = HandlerUtil.getActiveShell(event);
+ if (shell != null) {
+ popupDialog = new LayoutSpyDialog(shell);
+ popupDialog.open();
+ }
+ return null;
+ }
+
+}