Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public class LayoutSpyDialog {
private TreeViewer widgetTree;
private Text details;
private Button selectWidgetButton;
private Button findClassButton;
private Text modelInfo;
private Shell overlay;

// Model
Expand Down Expand Up @@ -219,19 +221,29 @@ private void createContents(Composite container) {
{
selectWidgetButton = new Button(buttonBar, SWT.PUSH);
selectWidgetButton.setText(Messages.LayoutSpyDialog_button_select_control);
findClassButton = new Button(buttonBar, SWT.PUSH);
findClassButton.setText(Messages.LayoutSpyDialog_button_find_class);
Button refreshButton = new Button(buttonBar, SWT.PUSH);
refreshButton.setText(Messages.LayoutSpyDialog_button_refresh);
refreshButton.addListener(SWT.Selection, event -> refreshTree());

GridLayoutFactory.fillDefaults().numColumns(2).generateLayout(buttonBar);
GridLayoutFactory.fillDefaults().numColumns(3).generateLayout(buttonBar);
}
GridDataFactory.fillDefaults().align(SWT.CENTER, SWT.CENTER).applyTo(buttonBar);

// Result of "Find Class": model element and implementing class of a clicked control.
Label modelLabel = new Label(container, SWT.NONE);
modelLabel.setText(Messages.LayoutSpyDialog_label_model_element);
modelInfo = new Text(container, SWT.READ_ONLY | SWT.MULTI | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
modelInfo.setText(Messages.LayoutSpyDialog_model_prompt);
GridDataFactory.fillDefaults().hint(300, 90).grab(true, false).applyTo(modelInfo);

GridLayoutFactory.fillDefaults().margins(LayoutConstants.getMargins()).generateLayout(container);

// Attach listeners
container.addDisposeListener(event -> disposed());
selectWidgetButton.addListener(SWT.Selection, event -> selectControl());
findClassButton.addListener(SWT.Selection, event -> findClass());

// Set up the model
widgetTree.setContentProvider(new WidgetTreeContentProvider());
Expand Down Expand Up @@ -487,6 +499,28 @@ private void selectControl() {
});
}

/**
* Hides the spy, lets the user click a control and then shows the owning
* application-model element and its implementing class.
*/
private void findClass() {
this.controlSelectorOpen.setValue(true);
// Only hide our own dialog; as a part this shell is the workbench window.
boolean ownsShell = shell != null;
if (ownsShell) {
shell.setVisible(false);
}
new ControlSelector((@Nullable Control control) -> {
if (control != null && !modelInfo.isDisposed()) {
modelInfo.setText(ModelElementResolver.describe(control));
}
this.controlSelectorOpen.setValue(false);
if (ownsShell) {
shell.setVisible(true);
}
});
}

/**
* Copies the diagnostic information of the selected control and all of its
* descendants to the clipboard as text, for pasting into bug reports.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ public class Messages extends NLS {
public static String LayoutSpyDialog_shell_text;
public static String LayoutSpyDialog_button_refresh;
public static String LayoutSpyDialog_button_select_control;
public static String LayoutSpyDialog_button_find_class;
public static String LayoutSpyDialog_button_show_overlay;
public static String LayoutSpyDialog_button_show_coloring;
public static String LayoutSpyDialog_label_widget_tree;
public static String LayoutSpyDialog_label_layout;
public static String LayoutSpyDialog_label_model_element;
public static String LayoutSpyDialog_model_prompt;
public static String LayoutSpyDialog_menu_copy_widget_info;
public static String LayoutSpyDialog_label_no_control_selected;
public static String LayoutSpyDialog_label_not_a_composite;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*******************************************************************************
* Copyright (c) 2026 Vogella GmbH and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Lars Vogel <Lars.Vogel@vogella.com> - initial API and implementation
*******************************************************************************/
package org.eclipse.tools.layout.spy.internal.dialogs;

import java.lang.reflect.Method;

import org.eclipse.e4.ui.model.application.MContribution;
import org.eclipse.e4.ui.model.application.ui.MUIElement;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;

/**
* Resolves an SWT {@link Control} to the owning e4 application-model element and
* describes its implementing Java class.
* <p>
* The e4 SWT renderer tags each widget with its owning {@link MUIElement} under
* the data key {@code "modelElement"} ({@code AbstractPartRenderer.OWNING_ME}).
* That constant is in an {@code x-friends} package, so the literal is used here.
*/
public final class ModelElementResolver {

/** Widget data key set by the e4 SWT renderer, == AbstractPartRenderer.OWNING_ME. */
private static final String OWNING_MODEL_ELEMENT_KEY = "modelElement"; //$NON-NLS-1$

/** Wrapper for 3.x parts; unwrapped to reveal the real view/editor class. */
private static final String COMPATIBILITY_PART_CLASS = "org.eclipse.ui.internal.e4.compatibility.CompatibilityPart"; //$NON-NLS-1$

private static final String MODEL_PACKAGE_PREFIX = "org.eclipse.e4.ui.model."; //$NON-NLS-1$

private ModelElementResolver() {
}

/**
* Returns the closest model element owning the control, walking up its
* parents, or {@code null} if none (for example a plain JFace dialog).
*/
public static @Nullable MUIElement findModelElement(Control control) {
for (Control current = control; current != null; current = current.getParent()) {
if (current.getData(OWNING_MODEL_ELEMENT_KEY) instanceof MUIElement element) {
return element;
}
}
return null;
}

/** Builds a copyable description of the model element and implementing class. */
public static String describe(Control control) {
StringBuilder builder = new StringBuilder();
builder.append("Control class: ").append(control.getClass().getName()).append('\n'); //$NON-NLS-1$

MUIElement element = findModelElement(control);
if (element == null) {
describeWithoutModel(control, builder);
return builder.toString();
}

builder.append('\n');
builder.append("Model element: ").append(modelTypeName(element)); //$NON-NLS-1$
String id = element.getElementId();
if (id != null && !id.isEmpty()) {
builder.append(" (id=").append(id).append(')'); //$NON-NLS-1$
}
builder.append('\n');

if (element instanceof MContribution contribution) {
describeContribution(contribution, builder);
}
return builder.toString();
}

private static void describeWithoutModel(Control control, StringBuilder builder) {
builder.append('\n');
builder.append("No application model element is associated with this control.").append('\n'); //$NON-NLS-1$
Shell shell = control.getShell();
builder.append("Shell class: ").append(shell.getClass().getName()).append('\n'); //$NON-NLS-1$
Object shellData = shell.getData();
if (shellData != null) {
builder.append("Shell data class: ").append(shellData.getClass().getName()).append('\n'); //$NON-NLS-1$
}
}

private static void describeContribution(MContribution contribution, StringBuilder builder) {
String uri = contribution.getContributionURI();
if (uri != null && !uri.isEmpty()) {
builder.append("Contribution URI: ").append(uri).append('\n'); //$NON-NLS-1$
}
Object object = contribution.getObject();
if (object == null) {
return;
}
Object implementation = unwrapCompatibilityPart(object);
Class<?> implementationClass = implementation.getClass();
builder.append("Implementing class: ").append(implementationClass.getName()).append('\n'); //$NON-NLS-1$
if (implementation != object) {
builder.append("Wrapped by: ").append(object.getClass().getName()).append('\n'); //$NON-NLS-1$
}
Bundle bundle = FrameworkUtil.getBundle(implementationClass);
if (bundle != null) {
builder.append("Contributing bundle: ").append(bundle.getSymbolicName()).append('\n'); //$NON-NLS-1$
}
}

/** Most specific model interface name (e.g. {@code MPart}) of the element. */
private static String modelTypeName(MUIElement element) {
for (Class<?> iface : element.getClass().getInterfaces()) {
if (iface.getName().startsWith(MODEL_PACKAGE_PREFIX)) {
return iface.getSimpleName();
}
}
return element.getClass().getSimpleName();
}

/** Unwraps a 3.x compatibility part to its real {@code IWorkbenchPart}; else returns the object. */
private static Object unwrapCompatibilityPart(Object object) {
for (Class<?> type = object.getClass(); type != null; type = type.getSuperclass()) {
if (COMPATIBILITY_PART_CLASS.equals(type.getName())) {
try {
Method getPart = object.getClass().getMethod("getPart"); //$NON-NLS-1$
Object part = getPart.invoke(object);
if (part != null) {
return part;
}
} catch (ReflectiveOperationException e) {
// Best effort only; fall back to the wrapper class.
}
break;
}
}
return object;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
LayoutSpyDialog_shell_text=Layout Spy
LayoutSpyDialog_button_refresh=&Refresh
LayoutSpyDialog_button_select_control=Select &Control
LayoutSpyDialog_button_find_class=Find &Class
LayoutSpyDialog_button_show_overlay=Show &overlay
LayoutSpyDialog_button_show_coloring=Color controls and add tooltip with layout info (requires resize)
LayoutSpyDialog_label_widget_tree=Widget tree:
LayoutSpyDialog_label_layout=Layout:
LayoutSpyDialog_label_model_element=Selected model element:
LayoutSpyDialog_model_prompt=Press "Find Class" and then click a view, editor or dialog to see its application model element and implementing class.
LayoutSpyDialog_menu_copy_widget_info=Copy &Widget Info
LayoutSpyDialog_label_no_control_selected=No control selected
LayoutSpyDialog_label_not_a_composite=Selected control is not a Composite, so it has no layout.
Expand Down
Loading