From 3f1689bf832602beaa56b9420cd25a61975c4c2f Mon Sep 17 00:00:00 2001 From: Patrick Ziegler Date: Fri, 19 Dec 2025 21:40:51 +0100 Subject: [PATCH] [GTK] Use Foreign Function & Memory API for GTK3 OS Support This converts the GTK-specific "org.eclipse.wb.os.linux" plugin into a multi-release jar, to take advantage of the foreign function and memory API, while still remaining compatible with older Java versions. Rather than calling the native methods via our shared library, the invocations are delegated to the Java linker. If any exception is thrown in native code, this then leads to a `Throwable` being thrown, rather than a crash of the entire IDE. --- .github/workflows/maven.yaml | 4 +- .mvn/maven.config | 2 +- Jenkinsfile | 2 +- .../html-src/whatsnew/v123.asciidoc | 6 + org.eclipse.wb.os.linux/.classpath | 7 +- org.eclipse.wb.os.linux/META-INF/MANIFEST.MF | 2 + .../wb/internal/os/linux/Activator.java | 125 +++++++++ .../org/eclipse/wb/internal/os/linux/GDK.java | 52 ++++ .../org/eclipse/wb/internal/os/linux/GTK.java | 115 +++++++++ .../wb/internal/os/linux/GdkRectangle.java | 73 ++++++ .../wb/internal/os/linux/GtkAllocation.java | 25 ++ .../os/linux/GtkRuntimeException.java | 26 ++ .../wb/internal/os/linux/GtkWidget.java | 82 ++++++ .../eclipse/wb/internal/os/linux/Native.java | 59 +++++ .../wb/internal/os/linux/OSSupportLinux.java | 209 +++++++++++++++ .../wb/internal/os/linux/ScreenshotMaker.java | 240 ++++++++++++++++++ .../wb/internal/os/linux/cairo/Cairo.java | 187 ++++++++++++++ .../internal/os/linux/cairo/CairoContext.java | 26 ++ .../internal/os/linux/cairo/CairoFormat.java | 76 ++++++ .../os/linux/cairo/CairoOperator.java | 173 +++++++++++++ .../internal/os/linux/cairo/CairoRegion.java | 22 ++ .../internal/os/linux/cairo/CairoSurface.java | 22 ++ .../wb/internal/os/linux/gtk3/GDK3.java | 134 ++++++++++ .../wb/internal/os/linux/gtk3/GTK3.java | 82 ++++++ .../os/linux/gtk3/GTK3ScreenshotMaker.java | 137 ++++++++++ .../wb/internal/os/linux/gtk3/GdkWindow.java | 22 ++ .../wb/internal/os/linux/gtk3/GtkWindow.java | 28 ++ org.eclipse.wb.os/META-INF/MANIFEST.MF | 3 +- .../wb/os/internal/OSRuntimeException.java | 24 ++ target-platform/mvn/wb-mvn.target | 12 + target-platform/wb-2024-06.target | 1 + 31 files changed, 1972 insertions(+), 6 deletions(-) create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/Activator.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GDK.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GTK.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GdkRectangle.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GtkAllocation.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GtkRuntimeException.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/GtkWidget.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/Native.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/OSSupportLinux.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/ScreenshotMaker.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/Cairo.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoContext.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoFormat.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoOperator.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoRegion.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/cairo/CairoSurface.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GDK3.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GTK3.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GTK3ScreenshotMaker.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GdkWindow.java create mode 100644 org.eclipse.wb.os.linux/src_java24/org/eclipse/wb/internal/os/linux/gtk3/GtkWindow.java create mode 100644 org.eclipse.wb.os/src/org/eclipse/wb/os/internal/OSRuntimeException.java 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 @@ +