| /******************************************************************************* |
| * 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 |
| } |
| } |
| } |
| } |