diff --git a/.github/workflows/maven.yaml b/.github/workflows/maven.yaml index b626ecfe4..7f6b51f07 100644 --- a/.github/workflows/maven.yaml +++ b/.github/workflows/maven.yaml @@ -23,9 +23,9 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - java: [ 21, 25 ] + java: [ 24, 25 ] include: - - java: 21 + - java: 24 target: 2024-06 - java: 25 target: master diff --git a/.mvn/maven.config b/.mvn/maven.config index 64b7db7c0..8e050cdbd 100644 --- a/.mvn/maven.config +++ b/.mvn/maven.config @@ -1,3 +1,3 @@ --Dtycho.version=5.0.0 +-Dtycho.version=5.0.1 -Djdk.xml.totalEntitySizeLimit=1000000 -Djdk.xml.maxGeneralEntitySizeLimit=1000000 \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index 72ea669a4..658c914c8 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -11,7 +11,7 @@ pipeline { tools { maven 'apache-maven-latest' - jdk 'temurin-jdk21-latest' + jdk 'temurin-jdk25-latest' } environment { diff --git a/org.eclipse.wb.doc.user/html-src/whatsnew/v123.asciidoc b/org.eclipse.wb.doc.user/html-src/whatsnew/v123.asciidoc index 911cfe0d4..61145f7b1 100644 --- a/org.eclipse.wb.doc.user/html-src/whatsnew/v123.asciidoc +++ b/org.eclipse.wb.doc.user/html-src/whatsnew/v123.asciidoc @@ -4,4 +4,10 @@ endif::[] = What's New - v1.23.0 +== General + +- [Linux] FFMA when using Java 24 or higher + +The Linux-specific fragment will use the Foreign Function and Memory API when used with Java 24 or higher. + What's new - xref:v122.adoc[*v1.22.0*] \ No newline at end of file diff --git a/org.eclipse.wb.os.linux/.classpath b/org.eclipse.wb.os.linux/.classpath index 375961e4d..e036bba6d 100644 --- a/org.eclipse.wb.os.linux/.classpath +++ b/org.eclipse.wb.os.linux/.classpath @@ -1,7 +1,12 @@ - + + + + + + diff --git a/org.eclipse.wb.os.linux/META-INF/MANIFEST.MF b/org.eclipse.wb.os.linux/META-INF/MANIFEST.MF index 90f254b3f..011eda114 100644 --- a/org.eclipse.wb.os.linux/META-INF/MANIFEST.MF +++ b/org.eclipse.wb.os.linux/META-INF/MANIFEST.MF @@ -18,6 +18,8 @@ Bundle-ActivationPolicy: lazy Bundle-Localization: plugin Import-Package: org.apache.commons.io;version="[2.16.1,3.0.0)", org.apache.commons.lang3;version="[3.14.0,4.0.0)", + org.apache.commons.lang3.function;version="[3.14.0,4.0.0)", org.eclipse.wb.os Automatic-Module-Name: org.eclipse.wb.os.linux +Multi-Release: true Provide-Capability: wbp;type=os diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/Activator.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/Activator.java new file mode 100644 index 000000000..47cd46a1c --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/Activator.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * Copyright (c) 2011, 2025 Google, Inc. 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Google, Inc. - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux; + +import org.eclipse.wb.internal.core.utils.reflect.ReflectionUtils; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Plugin; +import org.eclipse.core.runtime.Status; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.plugin.AbstractUIPlugin; + +import org.apache.commons.io.IOUtils; +import org.osgi.framework.BundleContext; + +import java.io.InputStream; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * The activator class controls the plug-in life cycle. + * + * @author mitin_aa + * @coverage os.linux + */ +public class Activator extends AbstractUIPlugin { + public static final String PLUGIN_ID = "org.eclipse.wb.os.linux"; + // + private static Activator m_plugin; + + //////////////////////////////////////////////////////////////////////////// + // + // Life cycle + // + //////////////////////////////////////////////////////////////////////////// + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + m_plugin = this; + } + + @Override + public void stop(BundleContext context) throws Exception { + m_plugin = null; + super.stop(context); + } + + /** + * Returns the shared instance. + */ + public static Activator getDefault() { + return m_plugin; + } + + public static void logError(String text, Throwable error) { + getDefault().getLog().log(new Status(IStatus.ERROR, PLUGIN_ID, text, error)); + } + + //////////////////////////////////////////////////////////////////////////// + // + // Files + // + //////////////////////////////////////////////////////////////////////////// + /** + * @return the {@link InputStream} for file from plugin directory. + */ + public static InputStream getFile(String path) { + try { + URL url = new URL(getInstallURL(), path); + return url.openStream(); + } catch (Throwable e) { + throw ReflectionUtils.propagate(e); + } + } + + /** + * @return the install {@link URL} for this {@link Plugin}. + */ + public static URL getInstallURL() { + return getInstallUrl(getDefault()); + } + + /** + * @return the install {@link URL} for given {@link Plugin}. + */ + private static URL getInstallUrl(Plugin plugin) { + return plugin.getBundle().getEntry("/"); + } + + //////////////////////////////////////////////////////////////////////////// + // + // Images + // + //////////////////////////////////////////////////////////////////////////// + private static final Map m_nameToIconMap = new HashMap<>(); + + /** + * @return the {@link Image} from "icons" directory. + */ + public static Image getImage(String path) { + Image image = m_nameToIconMap.get(path); + if (image == null) { + InputStream is = getFile("icons/" + path); + try { + image = new Image(Display.getCurrent(), is); + m_nameToIconMap.put(path, image); + } finally { + IOUtils.closeQuietly(is); + } + } + return image; + } +} \ No newline at end of file diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GDK.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GDK.java new file mode 100644 index 000000000..281fab2eb --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GDK.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux; + +import org.eclipse.wb.internal.os.linux.cairo.CairoContext; +import org.eclipse.wb.internal.os.linux.cairo.CairoRegion; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; + +/** + * The GDK toolkit + */ +public class GDK extends Native { + protected static final SymbolLookup GDK; + + static { + if (isGtk4()) { + GDK = SymbolLookup.libraryLookup("libgdk-4.so.0", Arena.ofAuto()); + } else { + GDK = SymbolLookup.libraryLookup("libgdk-3.so.0", Arena.ofAuto()); + } + } + + private static class InstanceHolder { + private static final MethodHandle gdk_cairo_region = createHandle(GDK, "gdk_cairo_region", + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + } + + /** + * Adds the given region to the current path of {@code cr}. + * + * @param cr A cairo context. + * @param region A {@code cairo_region_t}. + */ + public static void gdk_cairo_region(CairoContext cr, CairoRegion region) { + runSafe(() -> InstanceHolder.gdk_cairo_region.invoke(cr.segment(), region.segment())); + } +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GTK.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GTK.java new file mode 100644 index 000000000..00dd29261 --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GTK.java @@ -0,0 +1,115 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; + +/** + * The GTK toolkit + */ +public abstract class GTK extends Native { + protected static final SymbolLookup GTK; + + static { + if (isGtk4()) { + GTK = SymbolLookup.libraryLookup("libgtk-4.so.0", Arena.ofAuto()); + } else { + GTK = SymbolLookup.libraryLookup("libgtk-3.so.0", Arena.ofAuto()); + } + } + + private static class InstanceHolder { + private static final MethodHandle gtk_widget_get_allocated_baseline = createHandle(GTK, "gtk_widget_get_allocated_baseline", + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); + + private static final MethodHandle gtk_get_major_version = createHandle(GTK, "gtk_get_major_version", + FunctionDescriptor.of(ValueLayout.JAVA_INT)); + + private static final MethodHandle gtk_get_minor_version = createHandle(GTK, "gtk_get_minor_version", + FunctionDescriptor.of(ValueLayout.JAVA_INT)); + + private static final MethodHandle gtk_get_micro_version = createHandle(GTK, "gtk_get_micro_version", + FunctionDescriptor.of(ValueLayout.JAVA_INT)); + + private static final MethodHandle gtk_widget_get_allocation = createHandle(GTK, "gtk_widget_get_allocation", + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + + private static final MethodHandle gtk_widget_hide = createHandle(GTK, "gtk_widget_hide", + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)); + } + + /** + * @return The major version number of the GTK+ library. + */ + public static int gtk_get_major_version() { + return (int) callSafe(() -> InstanceHolder.gtk_get_major_version.invoke()); + } + + /** + * @return The minor version number of the GTK+ library. + */ + public static int gtk_get_minor_version() { + return (int) callSafe(() -> InstanceHolder.gtk_get_minor_version.invoke()); + } + + /** + * @return The micro version number of the GTK+ library. + */ + public static int gtk_get_micro_version() { + return (int) callSafe(() -> InstanceHolder.gtk_get_micro_version.invoke()); + } + + /** + * Retrieves the widget’s allocation. + * + * Note, when implementing a {@code GtkContainer}: a widget’s allocation will be + * its “adjusted” allocation, that is, the widget’s parent container typically + * calls {@code gtk_widget_size_allocate()} with an allocation, and that + * allocation is then adjusted (to handle margin and alignment for example) + * before assignment to the widget. + * + * {@code gtk_widget_get_allocation()} returns the adjusted allocation that was + * actually assigned to the widget. The adjusted allocation is guaranteed to be + * completely contained within the {@code gtk_widget_size_allocate()} + * allocation, however. + * + * So a {@code GtkContainer} is guaranteed that its children stay inside the + * assigned bounds, but not that they have exactly the bounds the container + * assigned. There is no way to get the original allocation assigned by + * {@code gtk_widget_size_allocate()}, since it isn’t stored; if a container + * implementation needs that information it will have to track it itself. + */ + public static void gtk_widget_get_allocation(GtkWidget widget, GtkAllocation allocation) { + runSafe(() -> InstanceHolder.gtk_widget_get_allocation.invoke(widget.segment(), allocation.segment())); + } + + /** + * Reverses the effects of {@code gtk_widget_show()}, causing the {@code widget} + * to be hidden (invisible to the user). + */ + public static void gtk_widget_hide(GtkWidget widget) { + runSafe(() -> InstanceHolder.gtk_widget_hide.invoke(widget.segment())); + } + + /** + * Returns the baseline that has currently been allocated to {@code widget} or + * -1, if none. + */ + public static int gtk_widget_get_allocated_baseline(GtkWidget widget) { + return (int) callSafe(() -> InstanceHolder.gtk_widget_get_allocated_baseline.invoke(widget.segment())); + } +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GdkRectangle.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GdkRectangle.java new file mode 100644 index 000000000..a30f7f6e7 --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GdkRectangle.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.StructLayout; +import java.lang.foreign.ValueLayout; + +/** + * A GdkRectangle data type for representing rectangles. + * + * GdkRectangle is identical to cairo_rectangle_t. Together with Cairo’s + * cairo_region_t data type, these are the central types for representing sets + * of pixels. + * + * The intersection of two rectangles can be computed with + * gdk_rectangle_intersect(); to find the union of two rectangles use + * gdk_rectangle_union(). + * + * The cairo_region_t type provided by Cairo is usually used for managing + * non-rectangular clipping of graphical operations. + * + * The Graphene library has a number of other data types for regions and volumes + * in 2D and 3D. + */ +public sealed class GdkRectangle permits GtkAllocation { + private static final StructLayout LAYOUT = MemoryLayout.structLayout( // + ValueLayout.JAVA_INT.withName("x"), // + ValueLayout.JAVA_INT.withName("y"), // + ValueLayout.JAVA_INT.withName("width"), // + ValueLayout.JAVA_INT.withName("height")); + + private final MemorySegment segment; + + public GdkRectangle(Arena arena) { + segment = arena.allocate(LAYOUT); + } + + public final int x() { + return segment.getAtIndex(ValueLayout.JAVA_INT, 0); + } + + public final int y() { + return segment.getAtIndex(ValueLayout.JAVA_INT, 1); + } + + public final int width() { + return segment.getAtIndex(ValueLayout.JAVA_INT, 2); + } + + public final int height() { + return segment.getAtIndex(ValueLayout.JAVA_INT, 3); + } + + /** + * The underlying memory segment allocated by this rectangle. + */ + public final MemorySegment segment() { + return segment; + } +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GtkAllocation.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GtkAllocation.java new file mode 100644 index 000000000..b63a0e2f0 --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GtkAllocation.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux; + +import java.lang.foreign.Arena; + +/** + * A GtkAllocation-struct of a widget represents region which has been allocated + * to the widget by its parent. It is a subregion of its parents allocation. + */ +public final class GtkAllocation extends GdkRectangle { + public GtkAllocation(Arena arena) { + super(arena); + } +} \ No newline at end of file diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GtkRuntimeException.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GtkRuntimeException.java new file mode 100644 index 000000000..a7ad06a2f --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GtkRuntimeException.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux; + +import org.eclipse.wb.os.internal.OSRuntimeException; + +/** + * Wrapper for all exceptions thrown while accessing native code. + */ +public class GtkRuntimeException extends OSRuntimeException { + private static final long serialVersionUID = 1L; + + public GtkRuntimeException(Throwable t) { + super(t); + } +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GtkWidget.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GtkWidget.java new file mode 100644 index 000000000..41c94017c --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GtkWidget.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux; + +import org.eclipse.wb.internal.core.utils.reflect.ReflectionUtils; + +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Widget; + +import java.lang.foreign.MemorySegment; +import java.util.Objects; + +/** + * GtkWidget is the base class all widgets in GTK+ derive from. It manages the + * widget lifecycle, states and style. + */ +public class GtkWidget { + private final MemorySegment segment; + + public GtkWidget(Widget widget) { + this(getHandle(widget)); + } + + public GtkWidget(long handle) { + segment = handle == 0L ? MemorySegment.NULL : MemorySegment.ofAddress(handle); + } + + public MemorySegment segment() { + return segment; + } + + @Override + public int hashCode() { + return Objects.hash(segment.address()); + } + + @Override + public boolean equals(Object o) { + if (o instanceof GtkWidget other) { + return segment.address() == other.segment.address(); + } + return false; + } + + /** + * @return the handle value of the {@link Widget} using reflection. + */ + private static long getHandle(Widget widget) { + long widgetHandle = getHandleValue(widget, "fixedHandle"); + if (widgetHandle == 0) { + // may be null, roll back to "handle" + if (widget instanceof Shell) { + widgetHandle = getHandleValue(widget, "shellHandle"); + } else { + widgetHandle = getHandleValue(widget, "handle"); + } + } + return widgetHandle; + } + + /** + * @return the widget as native pointer for native handles. Note: returns 0 if + * handle cannot be obtained. + */ + private static long getHandleValue(Object widget, String fieldName) { + if (ReflectionUtils.getFieldOptObject(widget, fieldName) instanceof Long longValue) { + return longValue; + } + // field might be shadowed (e.g. in ImageBasedFrame) + return 0L; + } +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/Native.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/Native.java new file mode 100644 index 000000000..1fcd47ab5 --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/Native.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux; + +import org.apache.commons.lang3.function.FailableRunnable; +import org.apache.commons.lang3.function.FailableSupplier; + +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.invoke.MethodHandle; + +/** + * Base class for all native methods calls using the FFM API. + */ +public abstract class Native { + + private static final Linker LINKER = Linker.nativeLinker(); + + protected static final boolean isGtk4() { + return "1".equals(System.getenv("SWT_GTK4")); + } + + protected static MethodHandle createHandle(SymbolLookup sl, String name, FunctionDescriptor descriptor) { + MemorySegment symbol = sl.find(name).orElseThrow(UnsatisfiedLinkError::new); + return LINKER.downcallHandle(symbol, descriptor); + } + + protected static Object callSafe(FailableSupplier s) { + try { + return s.get(); + } catch (Error e) { + throw e; + } catch (Throwable t) { + throw new GtkRuntimeException(t); + } + } + + protected static void runSafe(FailableRunnable r) { + try { + r.run(); + } catch (Error e) { + throw e; + } catch (Throwable t) { + throw new GtkRuntimeException(t); + } + } +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/OSSupportLinux.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/OSSupportLinux.java new file mode 100644 index 000000000..1723297f1 --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/OSSupportLinux.java @@ -0,0 +1,209 @@ +/******************************************************************************* + * Copyright (c) 2011, 2025 Google, Inc. 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Google, Inc. - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux; + +import org.eclipse.wb.internal.core.DesignerPlugin; +import org.eclipse.wb.internal.core.utils.reflect.ReflectionUtils; +import org.eclipse.wb.internal.os.linux.gtk3.GTK3; +import org.eclipse.wb.internal.os.linux.gtk3.GTK3ScreenshotMaker; +import org.eclipse.wb.internal.os.linux.gtk3.GtkWindow; +import org.eclipse.wb.internal.swt.VisualDataMockupProvider; +import org.eclipse.wb.os.OSSupport; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.TabItem; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.Widget; + +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.Version; + +import java.lang.foreign.Arena; +import java.util.List; + +public final class OSSupportLinux extends OSSupport { + private static Version SWT_VERSION_3_126 = new Version(3, 126, 0); + private final VisualDataMockupProvider mockupProvider = new VisualDataMockupProvider(); + private final ScreenshotMaker screenshotMaker = new GTK3ScreenshotMaker(); + private Shell m_eclipseShell; + + //////////////////////////////////////////////////////////////////////////// + // + // Instance + // + //////////////////////////////////////////////////////////////////////////// + protected static final OSSupport INSTANCE = new OSSupportLinux(); + + //////////////////////////////////////////////////////////////////////////// + // + // Screen shot + // + //////////////////////////////////////////////////////////////////////////// + + @Override + public void beginShot(Control control) { + Shell shell = layoutShell(control); + // setup key title to be used by compiz WM (if enabled) + if (!isWorkaroundsDisabled()) { + // prepare + GTK3.gtk_widget_show_now(new GtkWidget(shell)); + try { + Version currentVersion = FrameworkUtil.getBundle(SWT.class).getVersion(); + // Bug/feature is SWT: since the widget is already shown, the Shell.setVisible() + // invocation + // has no effect, so we've end up with wrong shell trimming. + // The workaround is to call adjustTrim() explicitly. + if (currentVersion.compareTo(SWT_VERSION_3_126) < 0) { + ReflectionUtils.invokeMethod(shell, "adjustTrim()", new Object[0]); + } else { + ReflectionUtils.invokeMethod(shell, "adjustTrim(int,int)", + new Object[] { SWT.DEFAULT, SWT.DEFAULT }); + } + } catch (Throwable e) { + DesignerPlugin.log(e); + } + m_eclipseShell = DesignerPlugin.getShell(); + // sometimes can be null, don't know why. + if (m_eclipseShell != null) { + GTK3.gtk_window_set_keep_above(new GtkWindow(m_eclipseShell), true); + } + } + shell.setLocation(10000, 10000); + shell.setVisible(true); + } + + @Override + public void endShot(Control control) { + // hide shell. The shell should be visible during all the period of fetching visual data. + super.endShot(control); + Shell shell = control.getShell(); + if (!isWorkaroundsDisabled()) { + GTK.gtk_widget_hide(new GtkWidget(shell)); + if (m_eclipseShell != null) { + GTK3.gtk_window_set_keep_above(new GtkWindow(m_eclipseShell), false); + } + } + } + + @Override + public void makeShots(Control control) throws Exception { + screenshotMaker.makeShots(control); + } + + @Override + public Image makeShot(Control control) throws Exception { + Shell shell = control.getShell(); + shell.setLocation(10000, 10000); + shell.setVisible(true); + + Rectangle controlBounds = control.getBounds(); + if (controlBounds.width == 0 || controlBounds.height == 0) { + return null; + } + + try { + // apply shot magic + return screenshotMaker.makeShot(shell, null); + } finally { + shell.setVisible(false); + } + } + + //////////////////////////////////////////////////////////////////////////// + // + // Menu + // + //////////////////////////////////////////////////////////////////////////// + + @Override + public Image getMenuPopupVisualData(Menu menu, int[] bounds) throws Exception { + return mockupProvider.mockMenuPopupVisualData(menu, bounds); + } + + @Override + public Image getMenuBarVisualData(Menu menu, List bounds) { + for (int i = 0; i < menu.getItemCount(); ++i) { + MenuItem item = menu.getItem(i); + bounds.add(getWidgetBounds(item)); + } + return null; + } + + @Override + public Rectangle getMenuBarBounds(Menu menu) { + Rectangle bounds = getWidgetBounds(menu); + Shell shell = menu.getShell(); + Point p = shell.toControl(shell.getLocation()); + p.x = -p.x; + p.y = -p.y - bounds.height; + return new Rectangle(p.x, p.y, bounds.width, bounds.height); + } + + @Override + public int getDefaultMenuBarHeight() { + // no way :( + return 24; + } + + //////////////////////////////////////////////////////////////////////////// + // + // TabItem + // + //////////////////////////////////////////////////////////////////////////// + + @Override + public Rectangle getTabItemBounds(TabItem item) { + return getWidgetBounds(item); + } + + //////////////////////////////////////////////////////////////////////////// + // + // Tree + // + //////////////////////////////////////////////////////////////////////////// + + @Override + public boolean isPlusMinusTreeClick(Tree tree, int x, int y) { + return false; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Troubleshooting + // + //////////////////////////////////////////////////////////////////////////// + private boolean isWorkaroundsDisabled() { + return Boolean.parseBoolean(System.getProperty("__wbp.linux.disableScreenshotWorkarounds")); + } + + //////////////////////////////////////////////////////////////////////////// + // + // Utils + // + //////////////////////////////////////////////////////////////////////////// + public static Rectangle getWidgetBounds(Widget w) { + GtkWidget widget = new GtkWidget(w); + try (Arena arena = Arena.ofConfined()) { + GtkAllocation allocation = new GtkAllocation(arena); + GTK.gtk_widget_get_allocation(widget, allocation); + return new Rectangle(allocation.x(), allocation.y(), allocation.width(), allocation.height()); + } + } +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/ScreenshotMaker.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/ScreenshotMaker.java new file mode 100644 index 000000000..bc63b0d6f --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/ScreenshotMaker.java @@ -0,0 +1,240 @@ +/******************************************************************************* + * Copyright (c) 2011, 2025 Google, Inc. 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Google, Inc. - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux; + +import org.eclipse.wb.internal.core.utils.ui.DrawUtils; +import org.eclipse.wb.os.OSSupport; + +import org.eclipse.draw2d.ColorConstants; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +import java.lang.foreign.MemorySegment; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +/** + * Creates a screenshot of a given widget. + */ +public abstract class ScreenshotMaker { + // constants + private static final Color TITLE_BORDER_COLOR_DARKEST = DrawUtils.getShiftedColor(ColorConstants.titleBackground, + -24); + private static final Color TITLE_BORDER_COLOR_DARKER = DrawUtils.getShiftedColor(ColorConstants.titleBackground, + -16); + + //////////////////////////////////////////////////////////////////////////// + // + // Utils + // + //////////////////////////////////////////////////////////////////////////// + private Map m_controlsRegistry; + + /** + * Prepares to screen shot: register controls. See + * {@link #registerControl(Control)} for details. + */ + private void prepareScreenshot(Shell shell) { + m_controlsRegistry = new HashMap<>(); + registerControl(shell); + registerByHandle(shell); + } + + /** + * Registers the control to be checked in screen shot callback. Every control + * can be registered multiple times. The first image handle received for this + * control in callback is "root" for this control and should be bound as + * {@link Image}. + */ + private void registerControl(Control control) { + // check size + Point size = control.getSize(); + if (size.x == 0 || size.y == 0) { + return; + } + { + registerByHandle(control); + registerByHandle(control); + } + control.setData(OSSupport.WBP_IMAGE, null); + // traverse children + if (control instanceof Composite composite) { + for (Control child : composite.getChildren()) { + registerControl(child); + } + } + } + + /** + * Tries to get the {@code handle} from {@code control}. If the handle exists, + * fills {@code m_needsImage}. + */ + private void registerByHandle(Control control) { + GtkWidget widget = new GtkWidget(control); + if (widget.segment() != MemorySegment.NULL) { + m_controlsRegistry.put(widget, control); + } + } + + //////////////////////////////////////////////////////////////////////////// + // + // Screen shot + // + //////////////////////////////////////////////////////////////////////////// + + public void makeShots(Control control) { + Shell shell = control.getShell(); + makeShots0(shell); + // check for decorations and draw if needed + drawDecorations(shell, shell.getDisplay()); + } + + /** + * Screen shot algorithm is the following: + * + *
+	 * 1. Register controls which requires the image. See {@link #registerControl(Control)}.
+	 * 2. Create the callback, binding the screenshot to the model. See {@link #makeShot(Shell, BiConsumer)
+	 * 3. While traversing the gtk widgets/gdk windows, the callback returns native widget handle and the
+	 *    image. If the control corresponding to the widget handle has been found in the registry the
+	 *    received image is bound to the control (see {@link #bindImage(Display, Control, int)}).
+	 *    Otherwise, the image is disposed later (because it may be used in drawing).
+	 * 
+ */ + private void makeShots0(final Shell shell) { + prepareScreenshot(shell); + final Set disposeImages = new HashSet<>(); + // apply shot magic + makeShot(shell, (handle, image) -> { + // get the registered control by handle + Control imageForControl = m_controlsRegistry.get(handle); + if (imageForControl == null || !bindImage(imageForControl, image)) { + // this means given image handle used to draw the gtk widget internally + disposeImages.add(image); + } + }); + // done, dispose image handles needed to draw internally. + for (Image image : disposeImages) { + image.dispose(); + } + } + + /** + * Causes taking the screen shot. + * + * @param shell the root {@link Shell} to capture. + * @param callback the callback instance for binding the snapshot to the data + * model. Can be null. + * @return the GdkPixmap* or cairo_surface_t* of {@link Shell}. + */ + protected abstract Image makeShot(Shell shell, BiConsumer callback); + + private boolean bindImage(final Control control, final Image image) { + if (control.getData(OSSupport.WBP_NEED_IMAGE) != null && control.getData(OSSupport.WBP_IMAGE) == null) { + control.setData(OSSupport.WBP_IMAGE, image); + return true; + } + return false; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Utils + // + //////////////////////////////////////////////////////////////////////////// + + /** + * Draws decorations if available/applicable. + */ + private void drawDecorations(Shell shell, final Display display) { + Image shellImage = (Image) shell.getData(OSSupport.WBP_IMAGE); + // draw title if any + if (shellImage != null && (shell.getStyle() & SWT.TITLE) != 0) { + Rectangle shellBounds = shell.getBounds(); + Rectangle imageBounds = shellImage.getBounds(); + Point offset = shell.toControl(shell.getLocation()); + offset.x = -offset.x; + offset.y = -offset.y; + // adjust by menu bar size + if (shell.getMenuBar() != null) { + offset.y -= OSSupportLinux.getWidgetBounds(shell.getMenuBar()).height; + } + // draw + Image decoratedShellImage = new Image(display, shellBounds.width, shellBounds.height); + GC gc = new GC(decoratedShellImage); + // draw background + gc.setBackground(ColorConstants.titleBackground); + gc.fillRectangle(0, 0, shellBounds.width, shellBounds.height); + // title area gradient + gc.setForeground(ColorConstants.titleGradient); + gc.fillGradientRectangle(0, 0, shellBounds.width, offset.y, true); + int buttonGapX = offset.x - 1; + int nextPositionX; + // buttons and title + { + // menu button + Image buttonImage = Activator.getImage("decorations/button-menu-icon.png"); + Rectangle buttonImageBounds = buttonImage.getBounds(); + int buttonOffsetY = offset.y / 2 - buttonImageBounds.height / 2; + gc.drawImage(buttonImage, buttonGapX, buttonOffsetY); + nextPositionX = buttonGapX + buttonImageBounds.width + buttonGapX; + } + { + // close button + Image buttonImage = Activator.getImage("decorations/button-close-icon.png"); + Rectangle buttonImageBounds = buttonImage.getBounds(); + nextPositionX = shellBounds.width - buttonImageBounds.width - buttonGapX; + int buttonOffsetY = offset.y / 2 - buttonImageBounds.height / 2; + gc.drawImage(buttonImage, nextPositionX, buttonOffsetY); + nextPositionX -= buttonGapX + buttonImageBounds.width; + } + { + // maximize button + Image buttonImage = Activator.getImage("decorations/button-max-icon.png"); + Rectangle buttonImageBounds = buttonImage.getBounds(); + int buttonOffsetY = offset.y / 2 - buttonImageBounds.height / 2; + gc.drawImage(buttonImage, nextPositionX, buttonOffsetY); + nextPositionX -= buttonGapX + buttonImageBounds.width; + } + { + // minimize button + Image buttonImage = Activator.getImage("decorations/button-min-icon.png"); + Rectangle buttonImageBounds = buttonImage.getBounds(); + int buttonOffsetY = offset.y / 2 - buttonImageBounds.height / 2; + gc.drawImage(buttonImage, nextPositionX, buttonOffsetY); + } + // outline + gc.setForeground(TITLE_BORDER_COLOR_DARKEST); + gc.drawRectangle(offset.x - 1, offset.y - 1, imageBounds.width + 1, imageBounds.height + 1); + gc.setForeground(TITLE_BORDER_COLOR_DARKER); + gc.drawRectangle(offset.x - 2, offset.y - 2, imageBounds.width + 3, imageBounds.height + 3); + // shell screen shot + gc.drawImage(shellImage, offset.x, offset.y); + // done + gc.dispose(); + shellImage.dispose(); + shell.setData(OSSupport.WBP_IMAGE, decoratedShellImage); + } + } +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/Cairo.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/Cairo.java new file mode 100644 index 000000000..5ce6f8ad9 --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/Cairo.java @@ -0,0 +1,187 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux.cairo; + +import org.eclipse.wb.internal.os.linux.Native; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; + +/** + * The Cairo toolkit. + */ +public class Cairo extends Native { + private static final SymbolLookup CAIRO = SymbolLookup.libraryLookup("libcairo.so.2", Arena.ofAuto()); + + private static final MethodHandle cairo_clip = createHandle(CAIRO, "cairo_clip", + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)); + + private static final MethodHandle cairo_create = createHandle(CAIRO, "cairo_create", + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + + private static final MethodHandle cairo_destroy = createHandle(CAIRO, "cairo_destroy", + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)); + + private static final MethodHandle cairo_image_surface_create = createHandle(CAIRO, "cairo_image_surface_create", + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)); + + private static final MethodHandle cairo_paint = createHandle(CAIRO, "cairo_paint", + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)); + + private static final MethodHandle cairo_region_destroy = createHandle(CAIRO, "cairo_region_destroy", + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)); + + private static final MethodHandle cairo_set_operator = createHandle(CAIRO, "cairo_set_operator", + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); + + private static final MethodHandle cairo_surface_flush = createHandle(CAIRO, "cairo_surface_flush", + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)); + + /** + * Establishes a new clip region by intersecting the current clip region with + * the current path as it would be filled by {@code cairo_fill()} and according + * to the current fill rule (see {@code cairo_set_fill_rule()}). + * + * After {@code cairo_clip()}, the current path will be cleared from the cairo + * context. + * + * The current clip region affects all drawing operations by effectively masking + * out any changes to the surface that are outside the current clip region. + * + * Calling {@code cairo_clip()} can only make the clip region smaller, never + * larger. But the current clip is part of the graphics state, so a temporary + * restriction of the clip region can be achieved by calling + * {@code cairo_clip()} within a {@code cairo_save()}/{@code cairo_restore()} + * pair. The only other means of increasing the size of the clip region is + * {@code cairo_reset_clip()}. + * + * @param cr a cairo context + */ + public static void cairo_clip(CairoContext cr) { + runSafe(() -> cairo_clip.invoke(cr.segment())); + } + + /** + * Creates a new {@code cairo_t} with all graphics state parameters set to + * default values and with target as a target surface. The target surface should + * be constructed with a backend-specific function such as + * {@code cairo_image_surface_create()} (or any other + * {@code cairo_backend_surface_create()} variant). + * + * This function references {@code target}, so you can immediately call + * {@code cairo_surface_destroy()} on it if you don't need to maintain a + * separate reference to it. + * + * @param target target surface for the context + * @return a newly allocated {@code cairo_t} with a reference count of 1. The + * initial reference count should be released with + * {@code cairo_destroy()} when you are done using the cairo_t. This + * function never returns NULL. If memory cannot be allocated, a special + * {@code cairo_t} object will be returned on which + * {@code cairo_status()} returns {@code CAIRO_STATUS_NO_MEMORY}. If you + * attempt to target a surface which does not support writing (such as + * {@code cairo_mime_surface_t}) then a {@code CAIRO_STATUS_WRITE_ERROR} + * will be raised. You can use this object normally, but no drawing will + * be done. + */ + public static CairoContext cairo_create(CairoSurface target) { + MemorySegment handle = (MemorySegment) callSafe(() -> cairo_create.invoke(target.segment())); + return new CairoContext(handle); + } + + /** + * Decreases the reference count on {@code cr} by one. If the result is zero, + * then {@code cr} and all associated resources are freed. See + * cairo_reference(). + * + * @param cr a {@code cairo_t} + */ + public static void cairo_destroy(CairoContext cr) { + runSafe(() -> cairo_destroy.invoke(cr.segment())); + } + + /** + * Creates an image surface of the specified format and dimensions. Initially + * the surface contents are set to 0. (Specifically, within each pixel, each + * color or alpha channel belonging to format will be 0. The contents of bits + * within a pixel, but not belonging to the given format are undefined). + * + * @param format format of pixels in the surface to create + * @param width width of the surface, in pixels + * @param height height of the surface, in pixels + * @return a pointer to the newly created surface. The caller owns the surface + * and should call {@code cairo_surface_destroy()} when done with it. + *

+ * This function always returns a valid pointer, but it will return a + * pointer to a "nil" surface if an error such as out of memory occurs. + * You can use {@code cairo_surface_status()} to check for this. + *

+ */ + public static CairoSurface cairo_image_surface_create(CairoFormat format, int width, int height) { + MemorySegment handle = (MemorySegment) callSafe(() -> cairo_image_surface_create.invoke(format.getValue(), width, height)); + return new CairoSurface(handle); + } + + /** + * A drawing operator that paints the current source everywhere within the + * current clip region. + * + * @param cr a cairo context + */ + public static void cairo_paint(CairoContext cr) { + runSafe(() -> cairo_paint.invoke(cr.segment())); + } + + /** + * Destroys a {@code cairo_region_t} object created with + * {@code cairo_region_create()}, {@code cairo_region_copy()}, or or + * {@code cairo_region_create_rectangle()}. + * + * @param region a {@code cairo_region_t} + */ + public static void cairo_region_destroy(CairoRegion region) { + runSafe(() -> cairo_region_destroy.invoke(region.segment())); + } + + /** + * Sets the compositing operator to be used for all drawing operations. See + * {@code cairo_operator_t} for details on the semantics of each available + * compositing operator. + *

+ * The default operator is {@code CAIRO_OPERATOR_OVER}. + *

+ * + * @param cr a {@code cairo_t} + * @param op a compositing operator, specified as a {@code cairo_operator_t} + */ + public static void cairo_set_operator(CairoContext cr, CairoOperator op) { + runSafe(() -> cairo_set_operator.invoke(cr.segment(), op.getValue())); + } + + /** + * Do any pending drawing for the surface and also restore any temporary + * modifications cairo has made to the surface's state. This function must be + * called before switching from drawing on the surface with cairo to drawing on + * it directly with native APIs, or accessing its memory outside of Cairo. If + * the surface doesn't support direct access, then this function does nothing. + * + * @param surface a {@code cairo_surface_t} + */ + public static void cairo_surface_flush(CairoSurface surface) { + runSafe(() -> cairo_surface_flush.invoke(surface.segment())); + } +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoContext.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoContext.java new file mode 100644 index 000000000..e0035d4bf --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoContext.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux.cairo; + +import java.lang.foreign.MemorySegment; + +/** + * A {@code cairo_t} contains the current state of the rendering device, + * including coordinates of yet to be drawn shapes. + * + * Cairo contexts, as {@code cairo_t} objects are named, are central to cairo + * and all drawing with cairo is always done to a cairo_t object. + */ +public record CairoContext(MemorySegment segment) { + +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoFormat.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoFormat.java new file mode 100644 index 000000000..261495e5f --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoFormat.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux.cairo; + +/** + * {@code cairo_format_t} is used to identify the memory format of image data. + * + * New entries may be added in future versions. + */ +public enum CairoFormat { + /** + * no such format exists or is supported. + */ + CAIRO_FORMAT_INVALID(-1), + /** + * each pixel is a 32-bit quantity, with alpha in the upper 8 bits, then red, + * then green, then blue. The 32-bit quantities are stored native-endian. + * Pre-multiplied alpha is used. (That is, 50% transparent red is 0x80800000, + * not 0x80ff0000.) (Since 1.0) + */ + CAIRO_FORMAT_ARGB32(0), + /** + * each pixel is a 32-bit quantity, with the upper 8 bits unused. Red, Green, + * and Blue are stored in the remaining 24 bits in that order. (Since 1.0) + */ + CAIRO_FORMAT_RGB24(1), + /** + * each pixel is a 8-bit quantity holding an alpha value. (Since 1.0) + */ + CAIRO_FORMAT_A8(2), + /** + * each pixel is a 1-bit quantity holding an alpha value. Pixels are packed + * together into 32-bit quantities. The ordering of the bits matches the + * endianness of the platform. On a big-endian machine, the first pixel is in + * the uppermost bit, on a little-endian machine the first pixel is in the + * least-significant bit. (Since 1.0) + */ + CAIRO_FORMAT_A1(3), + /** + * each pixel is a 16-bit quantity with red in the upper 5 bits, then green in + * the middle 6 bits, and blue in the lower 5 bits. (Since 1.2) + */ + CAIRO_FORMAT_RGB16_565(4), + /** + * like RGB24 but with 10bpc. (Since 1.12) + */ + CAIRO_FORMAT_RGB30(5), + /** + * 3 floats, R, G, B. (Since 1.17.2) + */ + CAIRO_FORMAT_RGB96F(6), + /** + * 4 floats, R, G, B, A. (Since 1.17.2) + */ + CAIRO_FORMAT_RGBA128F(7); + + private final int value; + + private CairoFormat(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoOperator.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoOperator.java new file mode 100644 index 000000000..d1a1a5410 --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoOperator.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux.cairo; + +/** + * {@code CairoOperator} is used to set the compositing operator for all cairo + * drawing operations. + * + * The default operator is {@link #CAIRO_OPERATOR_OVER}. + * + * The operators marked as unbounded modify their destination even outside of + * the mask layer (that is, their effect is not bound by the mask layer). + * However, their effect can still be limited by way of clipping. + * + * To keep things simple, the operator descriptions here document the behavior + * for when both source and destination are either fully transparent or fully + * opaque. The actual implementation works for translucent layers too. For a + * more detailed explanation of the effects of each operator, including the + * mathematical definitions, see https://cairographics.org/operators/. + */ +public enum CairoOperator { + /** + * clear destination layer (bounded) (Since 1.0) + */ + CAIRO_OPERATOR_CLEAR(0), + /** + * replace destination layer (bounded) (Since 1.0) + */ + CAIRO_OPERATOR_SOURCE(1), + /** + * draw source layer on top of destination layer (bounded) (Since 1.0) + */ + CAIRO_OPERATOR_OVER(2), + /** + * draw source where there was destination content (unbounded) (Since 1.0) + */ + CAIRO_OPERATOR_IN(3), + /** + * draw source where there was no destination content (unbounded) (Since 1.0) + */ + CAIRO_OPERATOR_OUT(4), + /** + * draw source on top of destination content and only there (Since 1.0) + */ + CAIRO_OPERATOR_ATOP(5), + /** + * ignore the source (Since 1.0) + */ + CAIRO_OPERATOR_DEST(6), + /** + * draw destination on top of source (Since 1.0) + */ + CAIRO_OPERATOR_DEST_OVER(7), + /** + * leave destination only where there was source content (unbounded) (Since 1.0) + */ + CAIRO_OPERATOR_DEST_IN(8), + /** + * leave destination only where there was no source content (Since 1.0) + */ + CAIRO_OPERATOR_DEST_OUT(9), + /** + * leave destination on top of source content and only there (unbounded) (Since + * 1.0) + */ + CAIRO_OPERATOR_DEST_ATOP(10), + /** + * source and destination are shown where there is only one of them (Since 1.0) + */ + CAIRO_OPERATOR_XOR(11), + /** + * source and destination layers are accumulated (Since 1.0) + */ + CAIRO_OPERATOR_ADD(12), + /** + * like over, but assuming source and dest are disjoint geometries (Since 1.0) + */ + CAIRO_OPERATOR_SATURATE(13), + /** + * source and destination layers are multiplied. This causes the result to be at + * least as dark as the darker inputs. (Since 1.10) + */ + CAIRO_OPERATOR_MULTIPLY(14), + /** + * source and destination are complemented and multiplied. This causes the + * result to be at least as light as the lighter inputs. (Since 1.10) + */ + CAIRO_OPERATOR_SCREEN(15), + /** + * multiplies or screens, depending on the lightness of the destination color. + * (Since 1.10) + */ + CAIRO_OPERATOR_OVERLAY(16), + /** + * replaces the destination with the source if it is darker, otherwise keeps the + * source. (Since 1.10) + */ + CAIRO_OPERATOR_DARKEN(17), + /** + * replaces the destination with the source if it is lighter, otherwise keeps + * the source. (Since 1.10) + */ + CAIRO_OPERATOR_LIGHTEN(18), + /** + * brightens the destination color to reflect the source color. (Since 1.10) + */ + CAIRO_OPERATOR_COLOR_DODGE(19), + /** + * darkens the destination color to reflect the source color. (Since 1.10) + */ + CAIRO_OPERATOR_COLOR_BURN(20), + /** + * Multiplies or screens, dependent on source color. (Since 1.10) + */ + CAIRO_OPERATOR_HARD_LIGHT(21), + /** + * Darkens or lightens, dependent on source color. (Since 1.10) + */ + CAIRO_OPERATOR_SOFT_LIGHT(22), + /** + * Takes the difference of the source and destination color. (Since 1.10) + */ + CAIRO_OPERATOR_DIFFERENCE(23), + /** + * Produces an effect similar to difference, but with lower contrast. (Since + * 1.10) + */ + CAIRO_OPERATOR_EXCLUSION(24), + /** + * Creates a color with the hue of the source and the saturation and luminosity + * of the target. (Since 1.10) + */ + CAIRO_OPERATOR_HSL_HUE(25), + /** + * Creates a color with the saturation of the source and the hue and luminosity + * of the target. Painting with this mode onto a gray area produces no change. + * (Since 1.10) + */ + CAIRO_OPERATOR_HSL_SATURATION(26), + /** + * Creates a color with the hue and saturation of the source and the luminosity + * of the target. This preserves the gray levels of the target and is useful for + * coloring monochrome images or tinting color images. (Since 1.10) + */ + CAIRO_OPERATOR_HSL_COLOR(27), + /** + * Creates a color with the luminosity of the source and the hue and saturation + * of the target. This produces an inverse effect to + * {@link CAIRO_OPERATOR_HSL_COLOR} . (Since 1.10) + */ + CAIRO_OPERATOR_HSL_LUMINOSITY(28); + + private final int value; + + private CairoOperator(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoRegion.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoRegion.java new file mode 100644 index 000000000..b0cbcc512 --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoRegion.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux.cairo; + +import java.lang.foreign.MemorySegment; + +/** + * A {@code cairo_region_t}. + */ +public record CairoRegion(MemorySegment segment) { + +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoSurface.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoSurface.java new file mode 100644 index 000000000..b66632d2d --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoSurface.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux.cairo; + +import java.lang.foreign.MemorySegment; + +/** + * Base class for surfaces. + */ +public record CairoSurface(MemorySegment segment) { + +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GDK3.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GDK3.java new file mode 100644 index 000000000..d1045f5c5 --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GDK3.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux.gtk3; + +import org.eclipse.wb.internal.os.linux.GDK; +import org.eclipse.wb.internal.os.linux.cairo.CairoContext; +import org.eclipse.wb.internal.os.linux.cairo.CairoRegion; + +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; + +/** + * The GDK toolkit compatible with GDK 3.x + */ +public class GDK3 extends GDK { + + private static class InstanceHolder { + private static final MethodHandle gdk_cairo_set_source_window = createHandle(GDK, "gdk_cairo_set_source_window", + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_DOUBLE, ValueLayout.JAVA_DOUBLE)); + + private static final MethodHandle gdk_window_get_height = createHandle(GDK, "gdk_window_get_height", + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); + + private static final MethodHandle gdk_window_get_visible_region = createHandle(GDK, "gdk_window_get_visible_region", + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + + private static final MethodHandle gdk_window_get_width = createHandle(GDK, "gdk_window_get_width", + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)); + + private static final MethodHandle gdk_window_is_visible = createHandle(GDK, "gdk_window_is_visible", + FunctionDescriptor.of(ValueLayout.JAVA_BOOLEAN, ValueLayout.ADDRESS)); + + private static final MethodHandle gdk_window_process_updates = createHandle(GDK, "gdk_window_process_updates", + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.JAVA_BOOLEAN)); + } + + /** + * Sets the given window as the source pattern for {@code cr}. + * + * The pattern has an extend mode of {@code CAIRO_EXTEND_NONE} and is aligned so + * that the origin of window is {@code x}, {@code y}. The window contains all + * its subwindows when rendering. + * + * Note that the contents of window are undefined outside of the visible part of + * {@code window}, so use this function with care. + * + * @param cr A cairo context. + * @param window A {@link GdkWindow}. + * @param x X coordinate of location to place upper left corner of + * {@code window}. + * @param y Y coordinate of location to place upper left corner of + * {@code window}. + */ + public static void gdk_cairo_set_source_window(CairoContext cr, GdkWindow window, double x, double y) { + runSafe(() -> InstanceHolder.gdk_cairo_set_source_window.invoke(cr.segment(), window.segment(), x, y)); + } + + /** + * Returns the height of the given window. + * + * On the X11 platform the returned size is the size reported in the + * most-recently-processed configure event, rather than the current size on the + * X server. + * + * @return The height of {@code window}. + */ + public static int gdk_window_get_height(GdkWindow window) { + return (int) callSafe(() -> InstanceHolder.gdk_window_get_height.invoke(window.segment())); + } + + /** + * Computes the region of the {@code window} that is potentially visible. This + * does not necessarily take into account if the window is obscured by other + * windows, but no area outside of this region is visible. + * + * @return A {@link CairoRegion}. This must be freed with cairo_region_destroy() + * when you are done. + */ + public static CairoRegion gdk_window_get_visible_region(GdkWindow window) { + MemorySegment handle = (MemorySegment) callSafe(() -> InstanceHolder.gdk_window_get_visible_region.invoke(window.segment())); + return new CairoRegion(handle); + } + + /** + * Returns the width of the given window. + * + * On the X11 platform the returned size is the size reported in the + * most-recently-processed configure event, rather than the current size on the + * X server. + * + * @return The width of {@code window}. + */ + public static int gdk_window_get_width(GdkWindow window) { + return (int) callSafe(() -> InstanceHolder.gdk_window_get_width.invoke(window.segment())); + } + + /** + * Checks whether the window has been mapped (with {@code gdk_window_show()} or + * {@code gdk_window_show_unraised())}. + * + * @return {@code true} if the window is mapped. + */ + public static boolean gdk_window_is_visible(GdkWindow window) { + return (boolean) callSafe(() -> InstanceHolder.gdk_window_is_visible.invoke(window.segment())); + } + + /** + * Sends one or more expose events to {@code window}. The areas in each expose + * event will cover the entire update area for the window (see + * {@code gdk_window_invalidate_region()} for details). Normally GDK calls + * {@code gdk_window_process_all_updates()} on your behalf, so there’s no need + * to call this function unless you want to force expose events to be delivered + * immediately and synchronously (vs. the usual case, where GDK delivers them in + * an idle handler). Occasionally this is useful to produce nicer scrolling + * behavior, for example. + * + * @param update_children Whether to also process updates for child windows. + */ + public static void gdk_window_process_updates(GdkWindow window, boolean update_children) { + runSafe(() -> InstanceHolder.gdk_window_process_updates.invoke(window.segment(), update_children)); + } +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GTK3.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GTK3.java new file mode 100644 index 000000000..903e400a8 --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GTK3.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux.gtk3; + +import org.eclipse.wb.internal.os.linux.GTK; +import org.eclipse.wb.internal.os.linux.GtkWidget; + +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; + +/** + * The GTK toolkit compatible with GTK 3.x + */ +public final class GTK3 extends GTK { + + private static class InstanceHolder { + private static final MethodHandle gtk_widget_get_window = createHandle(GTK, "gtk_widget_get_window", + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)); + + private static final MethodHandle gtk_window_set_keep_above = createHandle(GTK, "gtk_window_set_keep_above", + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.JAVA_BOOLEAN)); + + private static final MethodHandle gtk_widget_show_now = createHandle(GTK, "gtk_widget_show_now", + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)); + } + + /** + * Returns the widget’s window if it is realized, {@code NULL} otherwise. + * + * @return {@code widget}’s window. + */ + public static GdkWindow gtk_widget_get_window(GtkWidget widget) { + MemorySegment handle = (MemorySegment) callSafe(() -> InstanceHolder.gtk_widget_get_window.invoke(widget.segment())); + return new GdkWindow(handle); + } + + /** + * Shows a widget. If the widget is an unmapped toplevel widget (i.e. a + * {@code GtkWindow} that has not yet been shown), enter the main loop and wait + * for the window to actually be mapped. Be careful; because the main loop is + * running, anything can happen during this function. + */ + public static void gtk_widget_show_now(GtkWidget widget) { + runSafe(() -> InstanceHolder.gtk_widget_show_now.invoke(widget.segment())); + } + + /** + * Asks to keep {@code window} above, so that it stays on top. Note that you + * shouldn’t assume the window is definitely above afterward, because other + * entities (e.g. the user or [window manager][gtk-X11-arch]) could not keep it + * above, and not all window managers support keeping windows above. But + * normally the window will end kept above. Just don’t write code that crashes + * if not. + * + * It’s permitted to call this function before showing a window, in which case + * the window will be kept above when it appears onscreen initially. + * + * You can track the above state via the “window-state-event” signal on + * {@code GtkWidget}. + * + * Note that, according to the Extended Window Manager Hints Specification, the + * above state is mainly meant for user preferences and should not be used by + * applications e.g. for drawing attention to their dialogs. + * + * @param setting Whether to keep {@code window} above other windows. + */ + public static void gtk_window_set_keep_above(GtkWindow window, boolean setting) { + runSafe(() -> InstanceHolder.gtk_window_set_keep_above.invoke(window.segment(), setting)); + } +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GTK3ScreenshotMaker.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GTK3ScreenshotMaker.java new file mode 100644 index 000000000..e6ff0436e --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GTK3ScreenshotMaker.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) 2011, 2025 Google, Inc. 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Google, Inc. - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux.gtk3; + +import org.eclipse.wb.internal.core.utils.reflect.ReflectionUtils; +import org.eclipse.wb.internal.os.linux.GDK; +import org.eclipse.wb.internal.os.linux.GtkRuntimeException; +import org.eclipse.wb.internal.os.linux.GtkWidget; +import org.eclipse.wb.internal.os.linux.ScreenshotMaker; +import org.eclipse.wb.internal.os.linux.cairo.Cairo; +import org.eclipse.wb.internal.os.linux.cairo.CairoContext; +import org.eclipse.wb.internal.os.linux.cairo.CairoFormat; +import org.eclipse.wb.internal.os.linux.cairo.CairoOperator; +import org.eclipse.wb.internal.os.linux.cairo.CairoRegion; +import org.eclipse.wb.internal.os.linux.cairo.CairoSurface; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Device; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Widget; + +import java.util.function.BiConsumer; + +/** + * Creates a screenshot of a given widget using the GTK3 API. + */ +public class GTK3ScreenshotMaker extends ScreenshotMaker { + + @Override + protected Image makeShot(Shell shell, BiConsumer callback) { + return traverse(shell, callback); + } + + private Image traverse(Widget widget, BiConsumer callback) { + Image image = getImageSurface(new GtkWidget(widget), callback); + if (image == null) { + return null; + } + if (widget instanceof Composite composite) { + for (Control childWidget : composite.getChildren()) { + Image childImage = traverse(childWidget, callback); + if (childImage == null) { + continue; + } + if (callback == null) { + childImage.dispose(); + } + } + } + return image; + } + + protected Image getImageSurface(GtkWidget widget, BiConsumer callback) { + GdkWindow window = GTK3.gtk_widget_get_window(widget); + if (!GDK3.gdk_window_is_visible(window)) { + // don't deal with unmapped windows + return null; + } + + int width = GDK3.gdk_window_get_width(window); + int height = GDK3.gdk_window_get_height(window); + // force paint. Note, not all widgets do this completely, known so far is + // GtkTreeViewer. + GDK3.gdk_window_process_updates(window, true); + // take screenshot + Image image = createImage(window, width, height); + // get Java code notified + if (callback != null) { + callback.accept(widget, image); + } + // done + return image; + } + + private Image createImage(GdkWindow sourceWindow, int width, int height) { + // Create the Cairo surface on which the snapshot is drawn on + CairoSurface targetSurface = Cairo.cairo_image_surface_create(CairoFormat.CAIRO_FORMAT_ARGB32, width, height); + CairoContext cr = Cairo.cairo_create(targetSurface); + // Get the visible region of the window + // Wayland: Trying to take a screenshot of a partially unmapped widget + // results in a SIGFAULT. + CairoRegion visibleRegion = GDK3.gdk_window_get_visible_region(sourceWindow); + // Set the visible region as the clip for the Cairo context + GDK.gdk_cairo_region(cr, visibleRegion); + Cairo.cairo_clip(cr); + // Paint the surface + GDK3.gdk_cairo_set_source_window(cr, sourceWindow, 0, 0); + Cairo.cairo_set_operator(cr, CairoOperator.CAIRO_OPERATOR_SOURCE); + Cairo.cairo_paint(cr); + // Cleanup + Cairo.cairo_destroy(cr); + Cairo.cairo_surface_flush(targetSurface); + Cairo.cairo_region_destroy(visibleRegion); + return createImage(targetSurface.segment().address()); + } + + private Image createImage(long imageHandle) { + Image image = createImage0(imageHandle); + // BUG in SWT: Image instance is not fully initialized + // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=382175 + Image newImage = new Image(null, image.getImageData()); + image.dispose(); + return newImage; + } + + private Image createImage0(long imageHandle) { + try { + return (Image) ReflectionUtils.invokeMethod2( // + Image.class, // + "gtk_new", // + Device.class, // + int.class, // + long.class, // + long.class, // + null, // + SWT.BITMAP, // + imageHandle, // + 0); + } catch (Exception e) { + throw new GtkRuntimeException(e); + } + } + +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GdkWindow.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GdkWindow.java new file mode 100644 index 000000000..350687754 --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GdkWindow.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux.gtk3; + +import java.lang.foreign.MemorySegment; + +/** + * A GDK window. + */ +public record GdkWindow(MemorySegment segment) { + +} diff --git a/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GtkWindow.java b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GtkWindow.java new file mode 100644 index 000000000..ac0af1210 --- /dev/null +++ b/org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GtkWindow.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.internal.os.linux.gtk3; + +import org.eclipse.wb.internal.os.linux.GtkWidget; + +import org.eclipse.swt.widgets.Shell; + +/** + * A GtkWindow is a toplevel window which can contain other widgets. Windows + * normally have decorations that are under the control of the windowing system + * and allow the user to manipulate the window (resize it, move it, close it,…). + */ +public class GtkWindow extends GtkWidget { + public GtkWindow(Shell shell) { + super(shell); + } +} diff --git a/org.eclipse.wb.os/META-INF/MANIFEST.MF b/org.eclipse.wb.os/META-INF/MANIFEST.MF index 8f880ea30..41aca42d0 100644 --- a/org.eclipse.wb.os/META-INF/MANIFEST.MF +++ b/org.eclipse.wb.os/META-INF/MANIFEST.MF @@ -8,7 +8,8 @@ Bundle-Vendor: %providerName Require-Bundle: org.eclipse.ui;bundle-version="[3.206.0,4.0.0)", org.eclipse.core.runtime;bundle-version="[3.31.100,4.0.0)" Bundle-RequiredExecutionEnvironment: JavaSE-21 -Export-Package: org.eclipse.wb.os +Export-Package: org.eclipse.wb.os, + org.eclipse.wb.os.internal;x-friends:="org.eclipse.wb.os.linux" Bundle-ActivationPolicy: lazy Bundle-Localization: plugin Automatic-Module-Name: org.eclipse.wb.os diff --git a/org.eclipse.wb.os/src/org/eclipse/wb/os/internal/OSRuntimeException.java b/org.eclipse.wb.os/src/org/eclipse/wb/os/internal/OSRuntimeException.java new file mode 100644 index 000000000..8fd892e55 --- /dev/null +++ b/org.eclipse.wb.os/src/org/eclipse/wb/os/internal/OSRuntimeException.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler 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 + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.wb.os.internal; + +/** + * Wrapper for all exceptions thrown while accessing native code. + */ +public class OSRuntimeException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public OSRuntimeException(Throwable t) { + super(t); + } +} diff --git a/target-platform/mvn/wb-mvn.target b/target-platform/mvn/wb-mvn.target index 6268f7609..d74fa5124 100644 --- a/target-platform/mvn/wb-mvn.target +++ b/target-platform/mvn/wb-mvn.target @@ -52,6 +52,18 @@ 4.0.4 jar + + net.bytebuddy + byte-buddy-agent + 1.18.1 + jar + + + net.bytebuddy + byte-buddy + 1.18.1 + jar + org.apache.commons commons-text diff --git a/target-platform/wb-2024-06.target b/target-platform/wb-2024-06.target index 404b61fd5..b09a289ac 100644 --- a/target-platform/wb-2024-06.target +++ b/target-platform/wb-2024-06.target @@ -21,6 +21,7 @@ +