diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/NSProgressIndicator.java b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/NSProgressIndicator.java
index 3397ada72d7..d430ad015e9 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/NSProgressIndicator.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/NSProgressIndicator.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2000, 2019 IBM Corporation and others.
+ * Copyright (c) 2000, 2026 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
@@ -7,9 +7,6 @@
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
- *
- * Contributors:
- * IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.swt.internal.cocoa;
@@ -63,6 +60,10 @@ public void setUsesThreadedAnimation(boolean usesThreadedAnimation) {
OS.objc_msgSend(this.id, OS.sel_setUsesThreadedAnimation_, usesThreadedAnimation);
}
+public void sizeToFit() {
+ OS.objc_msgSend(this.id, OS.sel_sizeToFit);
+}
+
public void startAnimation(id sender) {
OS.objc_msgSend(this.id, OS.sel_startAnimation_, sender != null ? sender.id : 0);
}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/NSWorkspace.java b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/NSWorkspace.java
index 9bfa02eae83..16d5910854f 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/NSWorkspace.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/NSWorkspace.java
@@ -7,9 +7,6 @@
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
- *
- * Contributors:
- * IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.swt.internal.cocoa;
@@ -27,6 +24,16 @@ public NSWorkspace(id id) {
super(id);
}
+public NSURL URLForApplicationToOpenURL(NSURL url) {
+ long result = OS.objc_msgSend(this.id, OS.sel_URLForApplicationToOpenURL_, url != null ? url.id : 0);
+ return result != 0 ? new NSURL(result) : null;
+}
+
+public NSURL URLForApplicationToOpenContentType(UTType contentType) {
+ long result = OS.objc_msgSend(this.id, OS.sel_URLForApplicationToOpenContentType_, contentType != null ? contentType.id : 0);
+ return result != 0 ? new NSURL(result) : null;
+}
+
public NSString fullPathForApplication(NSString appName) {
long result = OS.objc_msgSend(this.id, OS.sel_fullPathForApplication_, appName != null ? appName.id : 0);
return result != 0 ? new NSString(result) : null;
@@ -45,16 +52,6 @@ public boolean openURL(NSURL url) {
return OS.objc_msgSend_bool(this.id, OS.sel_openURL_, url != null ? url.id : 0);
}
-public NSURL urlForApplicationToOpenURL(NSURL url) {
- long result = OS.objc_msgSend(this.id, OS.sel_URLForApplicationToOpenURL_, url != null ? url.id : 0);
- return result != 0 ? new NSURL(result) : null;
-}
-
-public NSURL urlForApplicationToOpenContentType(long contentType) {
- long result = OS.objc_msgSend(this.id, OS.sel_URLForApplicationToOpenContentType_, contentType);
- return result != 0 ? new NSURL(result) : null;
-}
-
public boolean openURLs(NSArray urls, NSString bundleIdentifier, long options, NSAppleEventDescriptor descriptor, long identifiers) {
return OS.objc_msgSend_bool(this.id, OS.sel_openURLs_withAppBundleIdentifier_options_additionalEventParamDescriptor_launchIdentifiers_, urls != null ? urls.id : 0, bundleIdentifier != null ? bundleIdentifier.id : 0, options, descriptor != null ? descriptor.id : 0, identifiers);
}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/OS.java b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/OS.java
index 40c830f8464..ea852de5a13 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/OS.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/OS.java
@@ -767,6 +767,7 @@ public static boolean isSystemDarkAppearance() {
public static final long class_NSWorkspace = objc_getClass("NSWorkspace");
public static final long class_SFCertificatePanel = objc_getClass("SFCertificatePanel");
public static final long class_SFCertificateTrustPanel = objc_getClass("SFCertificateTrustPanel");
+public static final long class_UTType = objc_getClass("UTType");
public static final long class_WebDataSource = objc_getClass("WebDataSource");
public static final long class_WebFrame = objc_getClass("WebFrame");
public static final long class_WebFrameView = objc_getClass("WebFrameView");
@@ -830,9 +831,9 @@ public static Selector getSelector (long value) {
public static final long sel_PMPrintSettings = Selector.sel_PMPrintSettings.value;
public static final long sel_TIFFRepresentation = Selector.sel_TIFFRepresentation.value;
public static final long sel_URL = Selector.sel_URL.value;
-public static final long sel_URLFromPasteboard_ = Selector.sel_URLFromPasteboard_.value;
-public static final long sel_URLForApplicationToOpenURL_ = Selector.sel_URLForApplicationToOpenURL_.value;
public static final long sel_URLForApplicationToOpenContentType_ = Selector.sel_URLForApplicationToOpenContentType_.value;
+public static final long sel_URLForApplicationToOpenURL_ = Selector.sel_URLForApplicationToOpenURL_.value;
+public static final long sel_URLFromPasteboard_ = Selector.sel_URLFromPasteboard_.value;
public static final long sel_URLWithString_ = Selector.sel_URLWithString_.value;
public static final long sel_UTF8String = Selector.sel_UTF8String.value;
public static final long sel_abortEditing = Selector.sel_abortEditing.value;
@@ -2143,6 +2144,9 @@ public static Selector getSelector (long value) {
/** Constants */
public static final int NSAlertFirstButtonReturn = 1000;
public static final int NSAlertSecondButtonReturn = 1001;
+public static final int NSAlertStyleCritical = 2;
+public static final int NSAlertStyleInformational = 1;
+public static final int NSAlertStyleWarning = 0;
public static final int NSAlertThirdButtonReturn = 1002;
public static final int NSAlphaFirstBitmapFormat = 1;
public static final int NSAlphaNonpremultipliedBitmapFormat = 2;
@@ -2164,7 +2168,6 @@ public static Selector getSelector (long value) {
public static final int NSBezelStylePushDisclosure = 14;
public static final int NSBezelStyleSmallSquare = 6;
public static final int NSBoldFontMask = 2;
-
public static final int NSBottomTabsBezelBorder = 2;
public static final int NSBoxCustom = 4;
public static final int NSBoxSeparator = 2;
@@ -2174,21 +2177,19 @@ public static Selector getSelector (long value) {
public static final int NSButtonTypeRadio = 4;
public static final int NSButtonTypeSwitch = 3;
public static final int NSCarriageReturnCharacter = 13;
-public static final int NSDatePickerStyleClockAndCalendar = 1;
-
public static final int NSClosePathBezierPathElement = 3;
-public static final int NSCommandKeyMask = 1048576;
+public static final int NSColorSpaceModelRGB = 1;
public static final int NSCompositingOperationClear = 0;
public static final int NSCompositingOperationCopy = 1;
public static final int NSCompositingOperationSourceAtop = 5;
public static final int NSCompositingOperationSourceOver = 2;
public static final int NSContentsCellMask = 1;
-public static final int NSControlKeyMask = 262144;
-public static final int NSCriticalAlertStyle = 2;
public static final int NSCurveToBezierPathElement = 2;
+public static final int NSDatePickerStyleClockAndCalendar = 1;
+public static final int NSDatePickerStyleTextField = 2;
+public static final int NSDatePickerStyleTextFieldAndStepper = 0;
public static final int NSDeleteCharacter = 127;
public static final long NSDeviceIndependentModifierFlagsMask = 4294901760L;
-
public static final int NSDragOperationCopy = 1;
public static final int NSDragOperationDelete = 32;
public static final long NSDragOperationEvery = -1L;
@@ -2197,6 +2198,10 @@ public static Selector getSelector (long value) {
public static final int NSDragOperationNone = 0;
public static final int NSEnterCharacter = 3;
public static final int NSEvenOddWindingRule = 1;
+public static final int NSEventModifierFlagCommand = 1048576;
+public static final int NSEventModifierFlagControl = 262144;
+public static final int NSEventModifierFlagHelp = 4194304;
+public static final int NSEventModifierFlagShift = 131072;
public static final int NSEventPhaseBegan = 1;
public static final int NSEventPhaseCancelled = 16;
public static final int NSEventPhaseEnded = 8;
@@ -2212,9 +2217,7 @@ public static Selector getSelector (long value) {
public static final int NSFocusRingTypeNone = 1;
public static final int NSFontPanelModeMaskAllEffects = 1048320;
public static final int NSFontPanelModeMaskAllModes = -1;
-
public static final int NSHelpFunctionKey = 63302;
-public static final int NSHelpKeyMask = 4194304;
public static final int NSHourMinuteDatePickerElementFlag = 12;
public static final int NSHourMinuteSecondDatePickerElementFlag = 14;
public static final int NSImageAbove = 5;
@@ -2229,7 +2232,7 @@ public static Selector getSelector (long value) {
public static final int NSImageLeft = 2;
public static final int NSImageOnly = 1;
public static final int NSImageOverlaps = 6;
-public static final int NSInformationalAlertStyle = 1;
+public static final int NSImageScaleNone = 2;
public static final int NSItalicFontMask = 1;
public static final int NSKeyDown = 10;
public static final int NSKeyUp = 11;
@@ -2246,10 +2249,13 @@ public static Selector getSelector (long value) {
public static final int NSLineBreakByWordWrapping = 0;
public static final int NSLineToBezierPathElement = 1;
public static final int NSControlSizeMini = 2;
-
+public static final int NSControlSizeRegular = 0;
+public static final int NSControlSizeSmall = 1;
+public static final int NSControlStateValueMixed = -1;
+public static final int NSControlStateValueOff = 0;
+public static final int NSControlStateValueOn = 1;
public static final int NSMiterLineJoinStyle = 0;
public static final int NSModalResponseCancel = 0;
-public static final int NSControlStateValueMixed = -1;
public static final int NSMouseEntered = 8;
public static final int NSMouseExited = 9;
public static final int NSMouseMoved = 5;
@@ -2259,9 +2265,6 @@ public static Selector getSelector (long value) {
public static final int NSNoImage = 0;
public static final int NSNoTitle = 0;
public static final int NSNonZeroWindingRule = 0;
-
-public static final int NSControlStateValueOff = 0;
-public static final int NSControlStateValueOn = 1;
public static final int NSOpenGLCPSurfaceOrder = 235;
public static final int NSOpenGLPFAAccumSize = 14;
public static final int NSOpenGLPFAAlphaSize = 11;
@@ -2280,9 +2283,6 @@ public static Selector getSelector (long value) {
public static final int NSPortraitOrientation = 0;
public static final int NSPrintPanelShowsPageSetupAccessory = 256;
public static final int NSPrintPanelShowsPrintSelection = 32;
-public static final int NSProgressIndicatorPreferredThickness = 14;
-public static final int NSColorSpaceModelRGB = 1;
-public static final int NSControlSizeRegular = 0;
public static final int NSRegularSquareBezelStyle = 2;
public static final int NSResizableWindowMask = 8;
public static final int NSRightMouseDown = 3;
@@ -2290,19 +2290,14 @@ public static Selector getSelector (long value) {
public static final int NSRightMouseUp = 4;
public static final int NSRoundLineCapStyle = 1;
public static final int NSRoundLineJoinStyle = 1;
-public static final int NSImageScaleNone = 2;
public static final int NSScrollElasticityNone = 1;
public static final int NSScrollWheel = 22;
-public static final int NSScrollerDecrementLine = 4;
public static final int NSScrollerDecrementPage = 1;
-public static final int NSScrollerIncrementLine = 5;
public static final int NSScrollerIncrementPage = 3;
public static final int NSScrollerKnob = 2;
public static final int NSScrollerKnobSlot = 6;
public static final int NSScrollerStyleLegacy = 0;
public static final int NSScrollerStyleOverlay = 1;
-public static final int NSShiftKeyMask = 131072;
-public static final int NSControlSizeSmall = 1;
public static final int NSSquareLineCapStyle = 2;
public static final int NSStatusWindowLevel = 25;
public static final int NSStringDrawingUsesLineFragmentOrigin = 1;
@@ -2322,9 +2317,6 @@ public static Selector getSelector (long value) {
public static final int NSTextAlignmentJustified = 3;
public static final int NSTextAlignmentLeft = 0;
public static final int NSTextAlignmentRight = IS_X86_64 ? 1 : 2;
-public static final int NSDatePickerStyleTextFieldAndStepper = 0;
-public static final int NSDatePickerStyleTextField = 2;
-
public static final int NSToolbarDisplayModeIconOnly = 2;
public static final long NSTouchPhaseAny = -1L;
public static final int NSTouchPhaseBegan = 1;
@@ -2336,14 +2328,12 @@ public static Selector getSelector (long value) {
public static final int NSUnderlineStyleNone = 0;
public static final int NSUnderlineStyleSingle = 1;
public static final int NSUnderlineStyleThick = 2;
-
public static final int NSViewHeightSizable = 16;
public static final int NSViewMaxXMargin = 4;
public static final int NSViewMaxYMargin = 32;
public static final int NSViewMinXMargin = 1;
public static final int NSViewMinYMargin = 8;
public static final int NSViewWidthSizable = 2;
-public static final int NSWarningAlertStyle = 0;
public static final int NSWindowAbove = 1;
public static final int NSWindowBelow = -1;
public static final int NSWindowCollectionBehaviorFullScreenAuxiliary = 256;
@@ -3183,12 +3173,6 @@ public static Selector getSelector (long value) {
* @param image cast=(CGImageRef)
*/
public static final native void CGImageRelease(long image);
-/**
- * @param url cast=(CFURLRef)
- * @param mediaBox cast=(const CGRect *)
- * @param auxiliaryInfo cast=(CFDictionaryRef)
- */
-public static final native long CGPDFContextCreateWithURL(long url, CGRect mediaBox, long auxiliaryInfo);
/**
* @param context cast=(CGContextRef)
* @param pageInfo cast=(CFDictionaryRef)
@@ -3197,11 +3181,17 @@ public static Selector getSelector (long value) {
/**
* @param context cast=(CGContextRef)
*/
-public static final native void CGPDFContextEndPage(long context);
+public static final native void CGPDFContextClose(long context);
+/**
+ * @param url cast=(CFURLRef)
+ * @param mediaBox cast=(CGRect*)
+ * @param auxiliaryInfo cast=(CFDictionaryRef)
+ */
+public static final native long CGPDFContextCreateWithURL(long url, CGRect mediaBox, long auxiliaryInfo);
/**
* @param context cast=(CGContextRef)
*/
-public static final native void CGPDFContextClose(long context);
+public static final native void CGPDFContextEndPage(long context);
/**
* @param path cast=(CGMutablePathRef)
* @param m cast=(CGAffineTransform*)
diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/Selector.java b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/Selector.java
index 589d736607e..09e0ae8c00c 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/Selector.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/Selector.java
@@ -96,9 +96,9 @@ public enum Selector {
, sel_PMPrintSettings("PMPrintSettings")
, sel_TIFFRepresentation("TIFFRepresentation")
, sel_URL("URL")
- , sel_URLFromPasteboard_("URLFromPasteboard:")
- , sel_URLForApplicationToOpenURL_("URLForApplicationToOpenURL:")
, sel_URLForApplicationToOpenContentType_("URLForApplicationToOpenContentType:")
+ , sel_URLForApplicationToOpenURL_("URLForApplicationToOpenURL:")
+ , sel_URLFromPasteboard_("URLFromPasteboard:")
, sel_URLWithString_("URLWithString:")
, sel_UTF8String("UTF8String")
, sel_abortEditing("abortEditing")
diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/UTType.java b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/UTType.java
new file mode 100644
index 00000000000..b836c18703f
--- /dev/null
+++ b/bundles/org.eclipse.swt/Eclipse SWT PI/cocoa/org/eclipse/swt/internal/cocoa/UTType.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * Copyright (c) 2026 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *******************************************************************************/
+package org.eclipse.swt.internal.cocoa;
+
+public class UTType extends NSObject {
+
+public UTType() {
+ super();
+}
+
+public UTType(long id) {
+ super(id);
+}
+
+public UTType(id id) {
+ super(id);
+}
+
+public static UTType typeWithFilenameExtension(NSString filenameExtension) {
+ long result = OS.objc_msgSend(OS.class_UTType, OS.sel_typeWithFilenameExtension_, filenameExtension != null ? filenameExtension.id : 0);
+ return result != 0 ? new UTType(result) : null;
+}
+
+}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/common/org/eclipse/swt/internal/Library.java b/bundles/org.eclipse.swt/Eclipse SWT PI/common/org/eclipse/swt/internal/Library.java
index 62927e205b4..0b1069113f5 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT PI/common/org/eclipse/swt/internal/Library.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT PI/common/org/eclipse/swt/internal/Library.java
@@ -35,7 +35,7 @@ public class Library {
/**
* SWT revision number (must be >= 0)
*/
- static int REVISION = 7;
+ static int REVISION = 10;
/**
* The JAVA and SWT versions
diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/gtk4.c b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/gtk4.c
index d290dcf83fb..4f41025f6c9 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/gtk4.c
+++ b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/gtk4.c
@@ -359,6 +359,18 @@ JNIEXPORT jlong JNICALL GTK4_NATIVE(gdk_1content_1formats_1builder_1new)
}
#endif
+#ifndef NO_gdk_1content_1formats_1contain_1gtype
+JNIEXPORT jboolean JNICALL GTK4_NATIVE(gdk_1content_1formats_1contain_1gtype)
+ (JNIEnv *env, jclass that, jlong arg0, jlong arg1)
+{
+ jboolean rc = 0;
+ GTK4_NATIVE_ENTER(env, that, gdk_1content_1formats_1contain_1gtype_FUNC);
+ rc = (jboolean)gdk_content_formats_contain_gtype((GdkContentFormats *)arg0, (GType)arg1);
+ GTK4_NATIVE_EXIT(env, that, gdk_1content_1formats_1contain_1gtype_FUNC);
+ return rc;
+}
+#endif
+
#ifndef NO_gdk_1content_1formats_1get_1gtypes
JNIEXPORT jlong JNICALL GTK4_NATIVE(gdk_1content_1formats_1get_1gtypes)
(JNIEnv *env, jclass that, jlong arg0, jlongArray arg1)
@@ -403,6 +415,16 @@ JNIEXPORT jlong JNICALL GTK4_NATIVE(gdk_1content_1formats_1to_1string)
}
#endif
+#ifndef NO_gdk_1content_1formats_1unref
+JNIEXPORT void JNICALL GTK4_NATIVE(gdk_1content_1formats_1unref)
+ (JNIEnv *env, jclass that, jlong arg0)
+{
+ GTK4_NATIVE_ENTER(env, that, gdk_1content_1formats_1unref_FUNC);
+ gdk_content_formats_unref((GdkContentFormats *)arg0);
+ GTK4_NATIVE_EXIT(env, that, gdk_1content_1formats_1unref_FUNC);
+}
+#endif
+
#ifndef NO_gdk_1content_1provider_1get_1value
JNIEXPORT jboolean JNICALL GTK4_NATIVE(gdk_1content_1provider_1get_1value)
(JNIEnv *env, jclass that, jlong arg0, jlong arg1, jlongArray arg2)
@@ -613,6 +635,66 @@ JNIEXPORT void JNICALL GTK4_NATIVE(gdk_1content_1serializer_1set_1task_1data)
}
#endif
+#ifndef NO_gdk_1drop_1finish
+JNIEXPORT void JNICALL GTK4_NATIVE(gdk_1drop_1finish)
+ (JNIEnv *env, jclass that, jlong arg0, jint arg1)
+{
+ GTK4_NATIVE_ENTER(env, that, gdk_1drop_1finish_FUNC);
+ gdk_drop_finish((GdkDrop *)arg0, (GdkDragAction)arg1);
+ GTK4_NATIVE_EXIT(env, that, gdk_1drop_1finish_FUNC);
+}
+#endif
+
+#ifndef NO_gdk_1drop_1get_1actions
+JNIEXPORT jint JNICALL GTK4_NATIVE(gdk_1drop_1get_1actions)
+ (JNIEnv *env, jclass that, jlong arg0)
+{
+ jint rc = 0;
+ GTK4_NATIVE_ENTER(env, that, gdk_1drop_1get_1actions_FUNC);
+ rc = (jint)gdk_drop_get_actions((GdkDrop *)arg0);
+ GTK4_NATIVE_EXIT(env, that, gdk_1drop_1get_1actions_FUNC);
+ return rc;
+}
+#endif
+
+#ifndef NO_gdk_1drop_1get_1formats
+JNIEXPORT jlong JNICALL GTK4_NATIVE(gdk_1drop_1get_1formats)
+ (JNIEnv *env, jclass that, jlong arg0)
+{
+ jlong rc = 0;
+ GTK4_NATIVE_ENTER(env, that, gdk_1drop_1get_1formats_FUNC);
+ rc = (jlong)gdk_drop_get_formats((GdkDrop *)arg0);
+ GTK4_NATIVE_EXIT(env, that, gdk_1drop_1get_1formats_FUNC);
+ return rc;
+}
+#endif
+
+#ifndef NO_gdk_1drop_1read_1value_1async
+JNIEXPORT void JNICALL GTK4_NATIVE(gdk_1drop_1read_1value_1async)
+ (JNIEnv *env, jclass that, jlong arg0, jlong arg1, jint arg2, jlong arg3, jlong arg4, jlong arg5)
+{
+ GTK4_NATIVE_ENTER(env, that, gdk_1drop_1read_1value_1async_FUNC);
+ gdk_drop_read_value_async((GdkDrop *)arg0, (GType)arg1, arg2, (GCancellable *)arg3, (GAsyncReadyCallback)arg4, (gpointer)arg5);
+ GTK4_NATIVE_EXIT(env, that, gdk_1drop_1read_1value_1async_FUNC);
+}
+#endif
+
+#ifndef NO_gdk_1drop_1read_1value_1finish
+JNIEXPORT jlong JNICALL GTK4_NATIVE(gdk_1drop_1read_1value_1finish)
+ (JNIEnv *env, jclass that, jlong arg0, jlong arg1, jlongArray arg2)
+{
+ jlong *lparg2=NULL;
+ jlong rc = 0;
+ GTK4_NATIVE_ENTER(env, that, gdk_1drop_1read_1value_1finish_FUNC);
+ if (arg2) if ((lparg2 = (*env)->GetLongArrayElements(env, arg2, NULL)) == NULL) goto fail;
+ rc = (jlong)gdk_drop_read_value_finish((GdkDrop *)arg0, (GAsyncResult *)arg1, (GError **)lparg2);
+fail:
+ if (arg2 && lparg2) (*env)->ReleaseLongArrayElements(env, arg2, lparg2, 0);
+ GTK4_NATIVE_EXIT(env, that, gdk_1drop_1read_1value_1finish_FUNC);
+ return rc;
+}
+#endif
+
#ifndef NO_gdk_1paintable_1snapshot
JNIEXPORT void JNICALL GTK4_NATIVE(gdk_1paintable_1snapshot)
(JNIEnv *env, jclass that, jlong arg0, jlong arg1, jint arg2, jint arg3)
@@ -2577,6 +2659,16 @@ JNIEXPORT void JNICALL GTK4_NATIVE(gtk_1tree_1view_1column_1cell_1get_1size)
}
#endif
+#ifndef NO_gtk_1tree_1view_1enable_1model_1drag_1dest
+JNIEXPORT void JNICALL GTK4_NATIVE(gtk_1tree_1view_1enable_1model_1drag_1dest)
+ (JNIEnv *env, jclass that, jlong arg0, jlong arg1, jint arg2)
+{
+ GTK4_NATIVE_ENTER(env, that, gtk_1tree_1view_1enable_1model_1drag_1dest_FUNC);
+ gtk_tree_view_enable_model_drag_dest((GtkTreeView *)arg0, (GdkContentFormats *)arg1, (GdkDragAction)arg2);
+ GTK4_NATIVE_EXIT(env, that, gtk_1tree_1view_1enable_1model_1drag_1dest_FUNC);
+}
+#endif
+
#ifndef NO_gtk_1widget_1action_1set_1enabled
JNIEXPORT void JNICALL GTK4_NATIVE(gtk_1widget_1action_1set_1enabled)
(JNIEnv *env, jclass that, jlong arg0, jbyteArray arg1, jboolean arg2)
@@ -2841,6 +2933,16 @@ JNIEXPORT jlong JNICALL GTK4_NATIVE(gtk_1widget_1pick)
}
#endif
+#ifndef NO_gtk_1widget_1remove_1controller
+JNIEXPORT void JNICALL GTK4_NATIVE(gtk_1widget_1remove_1controller)
+ (JNIEnv *env, jclass that, jlong arg0, jlong arg1)
+{
+ GTK4_NATIVE_ENTER(env, that, gtk_1widget_1remove_1controller_FUNC);
+ gtk_widget_remove_controller((GtkWidget *)arg0, (GtkEventController *)arg1);
+ GTK4_NATIVE_EXIT(env, that, gtk_1widget_1remove_1controller_FUNC);
+}
+#endif
+
#ifndef NO_gtk_1widget_1set_1cursor
JNIEXPORT void JNICALL GTK4_NATIVE(gtk_1widget_1set_1cursor)
(JNIEnv *env, jclass that, jlong arg0, jlong arg1)
diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/gtk4_stats.h b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/gtk4_stats.h
index 15d43724ca5..f240d03cae9 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/gtk4_stats.h
+++ b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/gtk4_stats.h
@@ -47,9 +47,11 @@ typedef enum {
gdk_1content_1formats_1builder_1add_1mime_1type_FUNC,
gdk_1content_1formats_1builder_1free_1to_1formats_FUNC,
gdk_1content_1formats_1builder_1new_FUNC,
+ gdk_1content_1formats_1contain_1gtype_FUNC,
gdk_1content_1formats_1get_1gtypes_FUNC,
gdk_1content_1formats_1get_1mime_1types_FUNC,
gdk_1content_1formats_1to_1string_FUNC,
+ gdk_1content_1formats_1unref_FUNC,
gdk_1content_1provider_1get_1value_FUNC,
gdk_1content_1provider_1new_1for_1value_FUNC,
gdk_1content_1provider_1new_1typed_FUNC,
@@ -67,6 +69,11 @@ typedef enum {
gdk_1content_1serializer_1return_1error_FUNC,
gdk_1content_1serializer_1return_1success_FUNC,
gdk_1content_1serializer_1set_1task_1data_FUNC,
+ gdk_1drop_1finish_FUNC,
+ gdk_1drop_1get_1actions_FUNC,
+ gdk_1drop_1get_1formats_FUNC,
+ gdk_1drop_1read_1value_1async_FUNC,
+ gdk_1drop_1read_1value_1finish_FUNC,
gdk_1paintable_1snapshot_FUNC,
gdk_1toplevel_1focus_FUNC,
gdk_1toplevel_1get_1state_FUNC,
@@ -211,6 +218,7 @@ typedef enum {
gtk_1text_1set_1tabs_FUNC,
gtk_1text_1set_1visibility_FUNC,
gtk_1tree_1view_1column_1cell_1get_1size_FUNC,
+ gtk_1tree_1view_1enable_1model_1drag_1dest_FUNC,
gtk_1widget_1action_1set_1enabled_FUNC,
gtk_1widget_1activate_1action_FUNC,
gtk_1widget_1add_1controller_FUNC,
@@ -231,6 +239,7 @@ typedef enum {
gtk_1widget_1measure_FUNC,
gtk_1widget_1paintable_1new_FUNC,
gtk_1widget_1pick_FUNC,
+ gtk_1widget_1remove_1controller_FUNC,
gtk_1widget_1set_1cursor_FUNC,
gtk_1widget_1set_1focusable_FUNC,
gtk_1widget_1set_1overflow_FUNC,
diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/OS.java b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/OS.java
index 59edf503279..a070c4d1329 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/OS.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/OS.java
@@ -383,14 +383,17 @@ public static String getEnvironmentalVariable (String envVarName) {
public static final byte[] delete_text = ascii("delete-text");
public static final byte[] direction_changed = ascii("direction-changed");
public static final byte[] dpi_changed = ascii("notify::scale-factor");
+ public static final byte[] accept = ascii("accept");
public static final byte[] drag_begin = ascii("drag-begin");
public static final byte[] drag_data_delete = ascii("drag-data-delete");
public static final byte[] drag_data_get = ascii("drag-data-get");
public static final byte[] drag_data_received = ascii("drag-data-received");
public static final byte[] drag_drop = ascii("drag-drop");
public static final byte[] drag_end = ascii("drag-end");
+ public static final byte[] drag_enter = ascii("drag-enter");
public static final byte[] drag_leave = ascii("drag-leave");
public static final byte[] drag_motion = ascii("drag-motion");
+ public static final byte[] drop = ascii("drop");
public static final byte[] prepare = ascii("prepare");
public static final byte[] draw = ascii("draw");
public static final byte[] end = ascii("end");
@@ -408,6 +411,7 @@ public static String getEnvironmentalVariable (String envVarName) {
public static final byte[] hide = ascii("hide");
public static final byte[] icon_release = ascii("icon-release");
public static final byte[] insert_text = ascii("insert-text");
+ public static final byte[] items_changed = ascii("items-changed");
public static final byte[] key_press_event = ascii("key-press-event");
public static final byte[] key_release_event = ascii("key-release-event");
public static final byte[] key_pressed = ascii("key-pressed");
diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk4/GTK4.java b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk4/GTK4.java
index df933e27183..3c33dfcfb79 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk4/GTK4.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk4/GTK4.java
@@ -228,6 +228,38 @@ public class GTK4 {
public static final native void gdk_content_formats_builder_add_mime_type(long builder, byte[] mime_type);
/** @param builder cast=(GdkContentFormatsBuilder *) */
public static final native long gdk_content_formats_builder_free_to_formats(long builder);
+ /**
+ * @param formats cast=(GdkContentFormats *)
+ * @param type cast=(GType)
+ */
+ public static final native boolean gdk_content_formats_contain_gtype(long formats, long type);
+ /** @param formats cast=(GdkContentFormats *) */
+ public static final native void gdk_content_formats_unref(long formats);
+
+ /* GdkDrop */
+ /**
+ * @param drop cast=(GdkDrop *)
+ * @param action cast=(GdkDragAction)
+ */
+ public static final native void gdk_drop_finish(long drop, int action);
+ /** @param drop cast=(GdkDrop *) */
+ public static final native int gdk_drop_get_actions(long drop);
+ /** @param drop cast=(GdkDrop *) */
+ public static final native long gdk_drop_get_formats(long drop);
+ /**
+ * @param drop cast=(GdkDrop *)
+ * @param type cast=(GType)
+ * @param cancellable cast=(GCancellable *)
+ * @param callback cast=(GAsyncReadyCallback)
+ * @param user_data cast=(gpointer)
+ */
+ public static final native void gdk_drop_read_value_async(long drop, long type, int io_priority, long cancellable, long callback, long user_data);
+ /**
+ * @param drop cast=(GdkDrop *)
+ * @param result cast=(GAsyncResult *)
+ * @param error cast=(GError **)
+ */
+ public static final native long gdk_drop_read_value_finish(long drop, long result, long[] error);
/* GtkFileChooser */
/**
@@ -705,6 +737,11 @@ public class GTK4 {
* @param controller cast=(GtkEventController *)
*/
public static final native void gtk_widget_add_controller(long widget, long controller);
+ /**
+ * @param widget cast=(GtkWidget *)
+ * @param controller cast=(GtkEventController *)
+ */
+ public static final native void gtk_widget_remove_controller(long widget, long controller);
/** @param widget cast=(GtkWidget *) */
public static final native long gtk_widget_get_first_child(long widget);
/** @param widget cast=(GtkWidget *) */
@@ -863,6 +900,14 @@ public class GTK4 {
/** @param menu_button cast=(GtkMenuButton *) */
public static final native void gtk_menu_button_set_use_underline(long menu_button, boolean use_underline);
+ /* GtkTreeView */
+ /**
+ * @param tree_view cast=(GtkTreeView *)
+ * @param formats cast=(GdkContentFormats *)
+ * @param actions cast=(GdkDragAction)
+ */
+ public static final native void gtk_tree_view_enable_model_drag_dest(long tree_view, long formats, int actions);
+
/* GtkTreeViewColumn */
/**
* @param tree_column cast=(GtkTreeViewColumn *)
diff --git a/bundles/org.eclipse.swt/Eclipse SWT Program/cocoa/org/eclipse/swt/program/Program.java b/bundles/org.eclipse.swt/Eclipse SWT Program/cocoa/org/eclipse/swt/program/Program.java
index 465abcc1180..786067fecee 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT Program/cocoa/org/eclipse/swt/program/Program.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT Program/cocoa/org/eclipse/swt/program/Program.java
@@ -153,20 +153,17 @@ private static NSURL findAppURLForExtension(NSString ext) {
// On macOS 12.0+, use the content type-based API which works reliably
// for all file types including third-party ones.
if (OS.VERSION >= OS.VERSION(12, 0, 0)) {
- long UTTypeClass = OS.objc_getClass("UTType");
- if (UTTypeClass != 0) {
- long utType = OS.objc_msgSend(UTTypeClass, OS.sel_typeWithFilenameExtension_, ext.id);
- if (utType != 0) {
- NSURL appURL = workspace.urlForApplicationToOpenContentType(utType);
- if (appURL != null) {
- return appURL;
- }
+ UTType utType = UTType.typeWithFilenameExtension(ext);
+ if (utType != null) {
+ NSURL appURL = workspace.URLForApplicationToOpenContentType(utType);
+ if (appURL != null) {
+ return appURL;
}
}
}
// Fallback: URL-based lookup (available since macOS 10.6, deprecated in macOS 12.0)
NSURL fileURL = NSURL.fileURLWithPath(NSString.stringWith("/tmp/dummy." + ext.getString()));
- return workspace.urlForApplicationToOpenURL(fileURL);
+ return workspace.URLForApplicationToOpenURL(fileURL);
}
static Program getProgram(NSBundle bundle) {
diff --git a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java
index 6b4cb998c86..47b20458042 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java
@@ -14,16 +14,19 @@
package org.eclipse.swt.graphics;
import static org.junit.Assert.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.*;
+import java.util.*;
import java.util.concurrent.*;
+import java.util.stream.*;
import org.eclipse.swt.*;
import org.eclipse.swt.internal.*;
import org.eclipse.swt.widgets.*;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
@ExtendWith(PlatformSpecificExecutionExtension.class)
@ExtendWith(WithMonitorSpecificScalingExtension.class)
@@ -142,4 +145,69 @@ private static int renderTextAndCountNonWhitePixels(Image target, Font font, Str
}
return count;
}
+
+ /**
+ * Regression test for the size calculation in scaling/cropping GC.drawImage()
+ * operations with asymmetric source dimensions (smaller height than width) at
+ * fractional zoom levels.
+ *
+ * At fractional zoom levels the effective X and Y scale factors diverge because
+ * each axis is rounded independently (e.g. at 125%:
+ * scaleFactorX = 625/500 = 1.25 but
+ * scaleFactorY = 24/19 ≈ 1.263).
+ */
+ @ParameterizedTest
+ @MethodSource("zoomAndHeightArguments")
+ public void drawImage_asymmetricDimensionsAtFractionalZoom(int zoom, int height) {
+ Display display = Display.getDefault();
+
+ int logicalWidth = 500;
+ int logicalHeight = height;
+
+ PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF);
+ ImageData srcData = new ImageData(logicalWidth, logicalHeight, 32, palette);
+ for (int y = 0; y < logicalHeight; y++) {
+ for (int x = 0; x < logicalWidth; x++) {
+ // left half red, right half blue – makes wrong-rectangle errors visible
+ srcData.setPixel(x, y, x < logicalWidth / 2 ? 0xFF0000 : 0x0000FF);
+ }
+ }
+ Image srcImage = new Image(display, srcData);
+
+ int previousZoom = DPIUtil.getDeviceZoom();
+ try {
+ DPIUtil.setDeviceZoom(zoom);
+
+ Image referenceImage = new Image(display, logicalWidth + 5, logicalHeight + 5);
+ GC referenceGC = new GC(referenceImage);
+ referenceGC.drawImage(srcImage, 0, 0);
+ referenceGC.dispose();
+
+ Image testImageScaled = new Image(display, logicalWidth + 5, logicalHeight + 5);
+ GC testGC = new GC(testImageScaled);
+ testGC.drawImage(srcImage, 0, 0, logicalWidth, logicalHeight);
+ testGC.dispose();
+ assertArrayEquals(referenceImage.getImageData(zoom).data, testImageScaled.getImageData(zoom).data);
+ testImageScaled.dispose();
+
+ Image testImageScaledCropped = new Image(display, logicalWidth + 5, logicalHeight + 5);
+ testGC = new GC(testImageScaledCropped);
+ testGC.drawImage(srcImage, 0, 0, logicalWidth, logicalHeight, 0, 0, logicalWidth, logicalHeight);
+ testGC.dispose();
+ assertArrayEquals(referenceImage.getImageData(zoom).data, testImageScaledCropped.getImageData(zoom).data);
+ testImageScaledCropped.dispose();
+
+ referenceImage.dispose();
+ } finally {
+ DPIUtil.setDeviceZoom(previousZoom);
+ srcImage.dispose();
+ }
+ }
+
+ private static Stream zoomAndHeightArguments() {
+ int[] zooms = { 25, 50, 75, 100, 125, 150, 175, 200 };
+ int[] heights = IntStream.rangeClosed(4, 20).toArray();
+ return Arrays.stream(zooms).boxed()
+ .flatMap(zoom -> Arrays.stream(heights).mapToObj(height -> Arguments.of(zoom, height)));
+ }
}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/ImagesWin32Tests.java b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/ImagesWin32Tests.java
index 6b60c2a20df..33f7e3fac26 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/ImagesWin32Tests.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/ImagesWin32Tests.java
@@ -13,7 +13,8 @@
*******************************************************************************/
package org.eclipse.swt.graphics;
-import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import org.eclipse.swt.*;
import org.eclipse.swt.internal.*;
@@ -30,9 +31,219 @@ public void testImageIconTypeShouldNotChangeAfterCallingGetHandleForDifferentZoo
Image icon = Display.getDefault().getSystemImage(SWT.ICON_ERROR);
try {
Image.win32_getHandle(icon, 200);
- assertEquals("Image type should stay to SWT.ICON", SWT.ICON, icon.type);
+ assertEquals(SWT.ICON, icon.type, "Image type should stay to SWT.ICON");
} finally {
icon.dispose();
}
}
+
+ /**
+ * Tests that a GC.drawImage() handle is reused across consecutive calls at
+ * different pixel sizes when the image only provides 100% zoom data. Because
+ * every zoom request falls back to the same 100% data, the effective (nearest
+ * available) zoom is always 100%, and a freshly allocated handle should not be
+ * required for each different draw size.
+ *
+ * See https://github.com/eclipse-platform/eclipse.platform.swt/issues/3419
+ */
+ @Test
+ public void testDrawingHandleIsReusedForSingleZoomImageAtDifferentSizes() {
+ PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF);
+ ImageData imageData = new ImageData(10, 10, 24, palette);
+ // Provider only has 100% data; returns null for every other zoom
+ Image image = new Image(Display.getDefault(), (ImageDataProvider) zoom -> zoom == 100 ? imageData : null);
+ long[] firstHandle = {0};
+ long[] secondHandle = {0};
+ try {
+ // 20x20 pixels → 200% zoom equivalent for a 10x10 base image
+ image.executeOnImageHandleAtBestFittingSize(h -> firstHandle[0] = h.handle(), 20, 20);
+ // 30x30 pixels → 300% zoom equivalent; provider still falls back to 100%,
+ // so the nearest available zoom is still 100% and the handle must be reused
+ image.executeOnImageHandleAtBestFittingSize(h -> secondHandle[0] = h.handle(), 30, 30);
+ assertNotEquals(0L, firstHandle[0], "First handle should be non-zero");
+ assertEquals(firstHandle[0], secondHandle[0],
+ "Consecutive GC.drawImage() calls at different sizes should reuse the same "
+ + "handle when the nearest available zoom is the same (100% in this case)");
+ } finally {
+ image.dispose();
+ }
+ }
+
+ /**
+ * Tests that a GC.drawImage() handle is reused across consecutive calls at
+ * different pixel sizes when the image provides data at 100% and 200% zoom.
+ * Sizes that both map to the 200% nearest available zoom (e.g. 200% and 250%)
+ * should share the same underlying handle without re-allocating it.
+ *
+ * See https://github.com/eclipse-platform/eclipse.platform.swt/issues/3419
+ */
+ @Test
+ public void testDrawingHandleIsReusedForTwoZoomImageAtSizesWithSameNearestZoom() {
+ PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF);
+ ImageData imageData100 = new ImageData(10, 10, 24, palette);
+ ImageData imageData200 = new ImageData(20, 20, 24, palette);
+ // Provider has explicit data at 100% and 200%; returns null for anything else
+ Image image = new Image(Display.getDefault(), (ImageDataProvider) zoom -> zoom == 100 ? imageData100 : zoom == 200 ? imageData200 : null);
+ long[] firstHandle = {0};
+ long[] secondHandle = {0};
+ try {
+ // 20x20 pixels → exactly 200% zoom for the 10x10 base image; uses 200% data
+ image.executeOnImageHandleAtBestFittingSize(h -> firstHandle[0] = h.handle(), 20, 20);
+ // 25x25 pixels → 250% zoom equivalent; nearest available is 200%, so the
+ // previously cached 200% handle should be reused
+ image.executeOnImageHandleAtBestFittingSize(h -> secondHandle[0] = h.handle(), 25, 25);
+ assertNotEquals(0L, firstHandle[0], "First handle should be non-zero");
+ assertEquals(firstHandle[0], secondHandle[0],
+ "Consecutive GC.drawImage() calls at different sizes should reuse the same "
+ + "handle when the nearest available zoom is the same (200% in this case)");
+ } finally {
+ image.dispose();
+ }
+ }
+
+ /**
+ * Tests that GC.drawImage() handles differ when consecutive calls at different
+ * pixel sizes land in different nearest-available-zoom regions for an image
+ * that provides distinct data at 100% and 200%. A size mapping to 100% and a
+ * size mapping to 200% must not share the same native handle, as they would
+ * represent different pixel content.
+ *
+ * See https://github.com/eclipse-platform/eclipse.platform.swt/issues/3419
+ */
+ @Test
+ public void testHandlesAreDifferentForTwoZoomImageAtDifferentNearestZooms() {
+ PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF);
+ ImageData imageData100 = new ImageData(10, 10, 24, palette);
+ ImageData imageData200 = new ImageData(20, 20, 24, palette);
+ // Provider has explicit data at 100% and 200%; returns null for anything else
+ Image image = new Image(Display.getDefault(), (ImageDataProvider) zoom -> zoom == 100 ? imageData100 : zoom == 200 ? imageData200 : null);
+ long[] handle100Zone = {0};
+ long[] handle200Zone = {0};
+ try {
+ // 10x10 pixels → 100% zoom for the 10x10 base image; nearest available is 100%
+ image.executeOnImageHandleAtBestFittingSize(h -> handle100Zone[0] = h.handle(), 10, 10);
+ // 20x20 pixels → 200% zoom; nearest available is 200% → must differ from the
+ // 100% handle since the underlying pixel data is different
+ image.executeOnImageHandleAtBestFittingSize(h -> handle200Zone[0] = h.handle(), 20, 20);
+ assertNotEquals(0L, handle100Zone[0], "First handle should be non-zero");
+ assertNotEquals(handle100Zone[0], handle200Zone[0],
+ "GC.drawImage() calls where the nearest available zoom differs must not "
+ + "reuse the same handle (100% data vs 200% data)");
+ } finally {
+ image.dispose();
+ }
+ }
+
+ /**
+ * Tests that a GC.drawImage() handle at the exact imageZoom is returned in
+ * preference to a handle at nearestAvailableZoom when both are present in the
+ * image's handle manager.
+ *
+ * Explicitly creating a persistent handle at 200% via
+ * {@link Image#win32_getHandle(Image, int)} places it in the handle manager.
+ * When GC.drawImage() then targets a pixel size that maps to imageZoom=200
+ * (while nearestAvailableZoom stays 100% because the provider only has 100%
+ * data), the 200% handle must be found first via the imageZoom lookup and
+ * returned instead of the 100% one.
+ *
+ * See https://github.com/eclipse-platform/eclipse.platform.swt/issues/3419
+ */
+ @Test
+ public void testDrawImagePrefersExistingHandleAtExactImageZoom() {
+ PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF);
+ ImageData imageData = new ImageData(10, 10, 24, palette);
+ Image image = new Image(Display.getDefault(), (ImageDataProvider) zoom -> zoom == 100 ? imageData : null);
+ try {
+ long handle100 = Image.win32_getHandle(image, 100);
+ long handle200 = Image.win32_getHandle(image, 200);
+ assertNotEquals(0L, handle100, "100% handle should be non-zero");
+ assertNotEquals(0L, handle200, "200% handle should be non-zero");
+ assertNotEquals(handle100, handle200, "Handles for different zooms should be distinct native objects");
+ long[] drawHandle = {0};
+ // 20x20 pixels → imageZoom=200 for a 10x10 base; nearestAvailableZoom=100
+ // The 200% handle already in the handle manager must be found and preferred
+ image.executeOnImageHandleAtBestFittingSize(h -> drawHandle[0] = h.handle(), 20, 20);
+ assertEquals(handle200, drawHandle[0],
+ "GC.drawImage() should prefer the existing handle at imageZoom=200 "
+ + "over the nearestAvailableZoom=100 handle");
+ } finally {
+ image.dispose();
+ }
+ }
+
+ /**
+ * Tests that when the pixel size requested by GC.drawImage() maps to a zoom
+ * equal to a current monitor's zoom, a persistent handle is created for that
+ * zoom level and is subsequently reusable via
+ * {@link Image#win32_getHandle(Image, int)}.
+ *
+ * A shell is created so that {@code Display.getShells()} is non-empty and
+ * the monitor zoom is visible to the handle-selection logic. Drawing at a
+ * pixel size whose imageZoom equals the shell's zoom triggers the
+ * monitor-zoom persistence path: the handle is stored in the image's handle
+ * manager and can be retrieved without allocating a second native object.
+ * On 100%-DPI machines the new {@code imageZoom == monitorZoom} branch
+ * overlaps with the existing {@code nearestAvailableZoom == 100} fallback;
+ * on HiDPI machines the new branch is exercised in isolation.
+ *
+ * See https://github.com/eclipse-platform/eclipse.platform.swt/issues/3419
+ */
+ @Test
+ public void testDrawImageCreatesAndReusesPersistentHandleForMonitorZoomImageZoom() {
+ PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF);
+ ImageData imageData = new ImageData(10, 10, 24, palette);
+ Image image = new Image(Display.getDefault(), (ImageDataProvider) zoom -> zoom == 100 ? imageData : null);
+ Shell shell = new Shell(Display.getDefault());
+ try {
+ int shellZoom = shell.getZoom();
+ int pixelSize = 10 * shellZoom / 100;
+ long[] drawHandle = {0};
+ // Drawing at shellZoom's pixel size → imageZoom == shellZoom == monitorZoom
+ // A persistent handle must be created and stored in the handle manager
+ image.executeOnImageHandleAtBestFittingSize(h -> drawHandle[0] = h.handle(), pixelSize, pixelSize);
+ // If persisted, win32_getHandle returns the same native handle without a new allocation
+ long persistedHandle = Image.win32_getHandle(image, shellZoom);
+ assertNotEquals(0L, drawHandle[0], "Draw handle should be non-zero");
+ assertEquals(persistedHandle, drawHandle[0],
+ "GC.drawImage() at the monitor zoom should create a persistent handle "
+ + "so that win32_getHandle returns the same native object");
+ } finally {
+ image.dispose();
+ shell.dispose();
+ }
+ }
+
+ /**
+ * Tests that a persistent native handle already created via
+ * {@link Image#win32_getHandle(Image, int)} is found and reused by
+ * GC.drawImage() when the nearest available zoom for the requested draw size
+ * maps to the same zoom. This verifies that the
+ * {@code imageHandleManager.get(nearestAvailableZoom)} lookup is effective and
+ * avoids redundant handle allocation.
+ *
+ * See https://github.com/eclipse-platform/eclipse.platform.swt/issues/3419
+ */
+ @Test
+ public void testDrawImageReusesExistingPersistentHandleForNearestAvailableZoom() {
+ PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF);
+ ImageData imageData = new ImageData(10, 10, 24, palette);
+ // Provider only has 100% data; every zoom falls back to 100%
+ Image image = new Image(Display.getDefault(), (ImageDataProvider) zoom -> zoom == 100 ? imageData : null);
+ try {
+ // Force creation of a persistent 100% handle (as would happen via GC.drawImage
+ // on a 100% zoom canvas or via Image.getImageData())
+ long persistentHandle = Image.win32_getHandle(image, 100);
+ assertNotEquals(0L, persistentHandle, "Persistent handle should be non-zero");
+ long[] drawHandle = {0};
+ // 20x20 pixels → 200% zoom equivalent for the 10x10 base image; nearest
+ // available is still 100%, so the already-cached persistent handle must be
+ // returned without allocating a new one
+ image.executeOnImageHandleAtBestFittingSize(h -> drawHandle[0] = h.handle(), 20, 20);
+ assertEquals(persistentHandle, drawHandle[0],
+ "GC.drawImage() should reuse the existing persistent handle when the "
+ + "nearest available zoom matches the cached handle's zoom (100% here)");
+ } finally {
+ image.dispose();
+ }
+ }
}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/ToolBarWin32Tests.java b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/ToolBarWin32Tests.java
new file mode 100644
index 00000000000..c730c22d488
--- /dev/null
+++ b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/ToolBarWin32Tests.java
@@ -0,0 +1,191 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Vector Informatik GmbH and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *******************************************************************************/
+package org.eclipse.swt.widgets;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.*;
+import java.util.function.*;
+
+import org.eclipse.swt.*;
+import org.eclipse.swt.graphics.*;
+import org.eclipse.swt.internal.*;
+import org.eclipse.swt.layout.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.extension.*;
+
+/**
+ * Windows-specific tests for {@link ToolBar}'s image handling across monitor
+ * zoom changes.
+ */
+@ExtendWith(PlatformSpecificExecutionExtension.class)
+class ToolBarWin32Tests {
+
+ private static final int TIMEOUT_MILLIS = 5000;
+
+ private record ToolItemWithExpectedColor(ToolItem toolItem, RGB expectedColor) {
+ }
+
+ /**
+ * Regression test for a tool bar item rendering the wrong or a blank icon after
+ * a monitor zoom (DPI) change, as reported in issue
+ * #3466.
+ *
+ * The bug is triggered by clearing one item's image (which leaves the image
+ * list in a state that a later zoom change mishandles); neither disposing an
+ * image nor multiple monitors are required. The test observes the actual
+ * rendered result via public API only: each item gets a distinctly colored
+ * icon, and after the zoom change the dominant color under each item must still
+ * match that item's own icon.
+ */
+ @Test
+ void testIconsRenderedCorrectlyAfterZoomChangeWithImageListHole() {
+ Display display = new Display();
+ RGB[] colors = { new RGB(220, 40, 40), new RGB(40, 180, 40), new RGB(40, 40, 220), new RGB(230, 200, 30) };
+ Image[] icons = new Image[colors.length];
+ for (int i = 0; i < colors.length; i++) {
+ icons[i] = solidIcon(display, 16, colors[i]);
+ }
+ try {
+ Shell shell = new Shell(display);
+ shell.setLayout(new FillLayout());
+ ToolBar bar = new ToolBar(shell, SWT.FLAT);
+ ToolItem[] items = new ToolItem[colors.length];
+ for (int i = 0; i < colors.length; i++) {
+ items[i] = new ToolItem(bar, SWT.PUSH);
+ items[i].setImage(icons[i]);
+ }
+ shell.setSize(500, 90);
+ shell.open();
+
+ // Punch a hole below the other items: clear the first item's image.
+ items[0].setImage(null);
+
+ int zoom = bar.getAutoscalingZoom();
+ DPITestUtil.changeDPIZoom(bar.getShell(), zoom * 2);
+
+ // Only the first item is expected to rendered blank
+ Set itemsToCheck = new HashSet<>();
+ for (int i = 1; i < items.length; i++) {
+ itemsToCheck.add(new ToolItemWithExpectedColor(items[i], colors[i]));
+ }
+ // Dispatch events until every item renders its own icon color, or fail on
+ // timeout. With the bug an item permanently renders another item's icon or
+ // a blank one, so the condition is never met and the timeout triggers.
+ assertTrue(waitUntilIconsRenderOwnColor(display, () -> iconsRenderOwnColor(bar, itemsToCheck, colors),
+ TIMEOUT_MILLIS), "every tool item must render its own icon color after a zoom change");
+ } finally {
+ for (Image icon : icons)
+ icon.dispose();
+ display.dispose();
+ }
+ }
+
+ private static boolean waitUntilIconsRenderOwnColor(Display display, BooleanSupplier condition,
+ long timeoutMillis) {
+ long deadline = System.currentTimeMillis() + timeoutMillis;
+ while (System.currentTimeMillis() < deadline) {
+ if (condition.getAsBoolean()) {
+ return true;
+ }
+ if (!display.readAndDispatch()) {
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ }
+ return condition.getAsBoolean();
+ }
+
+ private static boolean iconsRenderOwnColor(ToolBar bar, Set items, RGB[] candidateColors) {
+ Image snapshot = renderToolBar(bar);
+ try {
+ for (ToolItemWithExpectedColor item : items) {
+ if (!item.expectedColor().equals(dominantIconColor(snapshot, item.toolItem().getBounds(), candidateColors))) {
+ return false;
+ }
+ }
+ return true;
+ } finally {
+ snapshot.dispose();
+ }
+ }
+
+ private static Image solidIcon(Display display, int size, RGB rgb) {
+ Image image = new Image(display, size, size);
+ GC gc = new GC(image);
+ Color color = new Color(display, rgb);
+ gc.setBackground(color);
+ gc.fillRectangle(0, 0, size, size);
+ gc.dispose();
+ return image;
+ }
+
+ private static Image renderToolBar(ToolBar bar) {
+ Point size = bar.getSize();
+ Image snapshot = new Image(bar.getDisplay(), Math.max(1, size.x), Math.max(1, size.y));
+ GC gc = new GC(snapshot);
+ bar.print(gc);
+ gc.dispose();
+ return snapshot;
+ }
+
+ /**
+ * Returns which of the given candidate icon colors dominates the area of an
+ * item. Each sufficiently saturated pixel is classified to its nearest
+ * candidate color; the candidate matching the most pixels wins. Classifying to
+ * a fixed palette (rather than comparing exact RGB values) makes the result
+ * deterministic despite anti-aliasing, DPI interpolation and theming.
+ */
+ private static RGB dominantIconColor(Image snapshot, Rectangle bounds, RGB[] candidates) {
+ ImageData data = snapshot.getImageData();
+ PaletteData palette = data.palette;
+ int[] votes = new int[candidates.length];
+ int x0 = Math.max(0, bounds.x), y0 = Math.max(0, bounds.y);
+ int x1 = Math.min(data.width, bounds.x + bounds.width);
+ int y1 = Math.min(data.height, bounds.y + bounds.height);
+ for (int y = y0; y < y1; y++) {
+ for (int x = x0; x < x1; x++) {
+ RGB rgb = palette.getRGB(data.getPixel(x, y));
+ int max = Math.max(rgb.red, Math.max(rgb.green, rgb.blue));
+ int min = Math.min(rgb.red, Math.min(rgb.green, rgb.blue));
+ if (max - min < 60) {
+ continue; // ignore low-saturation background/borders
+ }
+ int best = -1, bestDist = Integer.MAX_VALUE;
+ for (int c = 0; c < candidates.length; c++) {
+ int dr = rgb.red - candidates[c].red;
+ int dg = rgb.green - candidates[c].green;
+ int db = rgb.blue - candidates[c].blue;
+ int dist = dr * dr + dg * dg + db * db;
+ if (dist < bestDist) {
+ bestDist = dist;
+ best = c;
+ }
+ }
+ if (best >= 0)
+ votes[best]++;
+ }
+ }
+ int winner = -1, most = 0;
+ for (int c = 0; c < candidates.length; c++) {
+ if (votes[c] > most) {
+ most = votes[c];
+ winner = c;
+ }
+ }
+ return winner < 0 ? null : candidates[winner];
+ }
+}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Combo.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Combo.java
index 0597becb231..76e216736d1 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Combo.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Combo.java
@@ -1445,9 +1445,9 @@ boolean sendKeyEvent (NSEvent nsEvent, int type) {
int stateMask = 0;
long modifierFlags = nsEvent.modifierFlags();
if ((modifierFlags & OS.NSAlternateKeyMask) != 0) stateMask |= SWT.ALT;
- if ((modifierFlags & OS.NSShiftKeyMask) != 0) stateMask |= SWT.SHIFT;
- if ((modifierFlags & OS.NSControlKeyMask) != 0) stateMask |= SWT.CONTROL;
- if ((modifierFlags & OS.NSCommandKeyMask) != 0) stateMask |= SWT.COMMAND;
+ if ((modifierFlags & OS.NSEventModifierFlagShift) != 0) stateMask |= SWT.SHIFT;
+ if ((modifierFlags & OS.NSEventModifierFlagControl) != 0) stateMask |= SWT.CONTROL;
+ if ((modifierFlags & OS.NSEventModifierFlagCommand) != 0) stateMask |= SWT.COMMAND;
if (type != SWT.KeyDown) return result;
short keyCode = nsEvent.keyCode ();
if (stateMask == SWT.COMMAND) {
@@ -1484,7 +1484,7 @@ boolean sendTrackingKeyEvent (NSEvent nsEvent, int type) {
* queue.
*/
long modifiers = nsEvent.modifierFlags();
- if ((modifiers & OS.NSShiftKeyMask) == 0) {
+ if ((modifiers & OS.NSEventModifierFlagShift) == 0) {
short keyCode = nsEvent.keyCode ();
switch (keyCode) {
case 125: /* Arrow Down */
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Control.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Control.java
index 8c2238e83cf..f28f96b24da 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Control.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Control.java
@@ -1044,7 +1044,7 @@ void doCommandBySelector (long id, long sel, long selector) {
* is down, because we likely triggered the current key sequence via flagsChanged.
*/
long modifiers = nsEvent.modifierFlags();
- if (s.keyInputHappened == false || (modifiers & OS.NSCommandKeyMask) != 0) {
+ if (s.keyInputHappened == false || (modifiers & OS.NSEventModifierFlagCommand) != 0) {
s.keyInputHappened = true;
boolean [] consume = new boolean [1];
if (translateTraversal (nsEvent.keyCode (), nsEvent, consume)) return;
@@ -1363,9 +1363,9 @@ void flagsChanged (long id, long sel, long theEvent) {
int keyCode = Display.translateKey (nsEvent.keyCode ());
switch (keyCode) {
case SWT.ALT: mask = OS.NSAlternateKeyMask; break;
- case SWT.CONTROL: mask = OS.NSControlKeyMask; break;
- case SWT.COMMAND: mask = OS.NSCommandKeyMask; break;
- case SWT.SHIFT: mask = OS.NSShiftKeyMask; break;
+ case SWT.CONTROL: mask = OS.NSEventModifierFlagControl; break;
+ case SWT.COMMAND: mask = OS.NSEventModifierFlagCommand; break;
+ case SWT.SHIFT: mask = OS.NSEventModifierFlagShift; break;
case SWT.CAPS_LOCK:
Event event = new Event();
event.keyCode = keyCode;
@@ -2558,7 +2558,7 @@ boolean mouseEvent (long id, long sel, long theEvent, int type) {
switch (nsType) {
case OS.NSLeftMouseDown:
- if (nsEvent.clickCount() == 1 && (nsEvent.modifierFlags() & OS.NSControlKeyMask) == 0 && (state & DRAG_DETECT) != 0 && hooks (SWT.DragDetect)) {
+ if (nsEvent.clickCount() == 1 && (nsEvent.modifierFlags() & OS.NSEventModifierFlagControl) == 0 && (state & DRAG_DETECT) != 0 && hooks (SWT.DragDetect)) {
consume = new boolean[1];
NSPoint location = view.convertPoint_fromView_(nsEvent.locationInWindow(), null);
if (!view.isFlipped ()) {
@@ -4783,7 +4783,7 @@ boolean translateTraversal (int key, NSEvent theEvent, boolean [] consume) {
}
case 48: /* Tab */ {
long modifiers = theEvent.modifierFlags ();
- boolean next = (modifiers & OS.NSShiftKeyMask) == 0;
+ boolean next = (modifiers & OS.NSEventModifierFlagShift) == 0;
detail = next ? SWT.TRAVERSE_TAB_NEXT : SWT.TRAVERSE_TAB_PREVIOUS;
break;
}
@@ -4799,7 +4799,7 @@ boolean translateTraversal (int key, NSEvent theEvent, boolean [] consume) {
case 121: /* Page down */ {
all = true;
long modifiers = theEvent.modifierFlags ();
- if ((modifiers & OS.NSControlKeyMask) == 0) return false;
+ if ((modifiers & OS.NSEventModifierFlagControl) == 0) return false;
detail = key == 121 /* Page down */ ? SWT.TRAVERSE_PAGE_NEXT : SWT.TRAVERSE_PAGE_PREVIOUS;
break;
}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Display.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Display.java
index 25f78cbc179..c2524f8e8c9 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Display.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Display.java
@@ -1103,7 +1103,7 @@ void createMainMenu () {
title = NSString.stringWith(SWT.getMessage("SWT_HideOthers"));
menuItem = appleMenu.addItemWithTitle(title, OS.sel_hideOtherApplications_, NSString.stringWith("h"));
- menuItem.setKeyEquivalentModifierMask(OS.NSCommandKeyMask | OS.NSAlternateKeyMask);
+ menuItem.setKeyEquivalentModifierMask(OS.NSEventModifierFlagCommand | OS.NSAlternateKeyMask);
menuItem.setTarget(applicationDelegate);
title = NSString.stringWith(SWT.getMessage("SWT_ShowAll"));
@@ -3924,9 +3924,9 @@ boolean performKeyEquivalent(NSWindow window, NSEvent nsEvent) {
long selector = 0;
long modifierFlags = nsEvent.modifierFlags();
if ((modifierFlags & OS.NSAlternateKeyMask) != 0) stateMask |= SWT.ALT;
- if ((modifierFlags & OS.NSShiftKeyMask) != 0) stateMask |= SWT.SHIFT;
- if ((modifierFlags & OS.NSControlKeyMask) != 0) stateMask |= SWT.CONTROL;
- if ((modifierFlags & OS.NSCommandKeyMask) != 0) stateMask |= SWT.COMMAND;
+ if ((modifierFlags & OS.NSEventModifierFlagShift) != 0) stateMask |= SWT.SHIFT;
+ if ((modifierFlags & OS.NSEventModifierFlagControl) != 0) stateMask |= SWT.CONTROL;
+ if ((modifierFlags & OS.NSEventModifierFlagCommand) != 0) stateMask |= SWT.COMMAND;
if (stateMask == SWT.COMMAND) {
short keyCode = nsEvent.keyCode ();
switch (keyCode) {
@@ -5679,7 +5679,7 @@ void applicationSendEvent (long id, long sel, long event) {
* Feature in Cocoa. The help key triggers context-sensitive help but doesn't get forwarded to the window as a key event.
* If the event is destined for the key window, is the help key, and is an NSKeyDown, send it directly to the window first.
*/
- if (window != null && window.isKeyWindow() && nsEvent.type() == OS.NSKeyDown && (nsEvent.modifierFlags() & OS.NSHelpKeyMask) != 0) {
+ if (window != null && window.isKeyWindow() && nsEvent.type() == OS.NSKeyDown && (nsEvent.modifierFlags() & OS.NSEventModifierFlagHelp) != 0) {
window.sendEvent(nsEvent);
}
@@ -5687,7 +5687,7 @@ void applicationSendEvent (long id, long sel, long event) {
* Feature in Cocoa. NSKeyUp events are not delivered to the window if the command key is down.
* If the event is destined for the key window, and it's a key up and the command key is down, send it directly to the window.
*/
- if (window != null && window.isKeyWindow() && nsEvent.type() == OS.NSKeyUp && (nsEvent.modifierFlags() & OS.NSCommandKeyMask) != 0) {
+ if (window != null && window.isKeyWindow() && nsEvent.type() == OS.NSKeyUp && (nsEvent.modifierFlags() & OS.NSEventModifierFlagCommand) != 0) {
window.sendEvent(nsEvent);
} else {
objc_super super_struct = new objc_super ();
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Link.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Link.java
index 6d075f0136a..c1f1fd465a7 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Link.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Link.java
@@ -824,7 +824,7 @@ int traversalCode (int key, NSEvent theEvent) {
int bits = super.traversalCode (key, theEvent);
if (key == 48 /* Tab */ && theEvent != null) {
long modifierFlags = theEvent.modifierFlags();
- boolean next = (modifierFlags & OS.NSShiftKeyMask) == 0;
+ boolean next = (modifierFlags & OS.NSEventModifierFlagShift) == 0;
if (next && focusIndex < offsets.length - 1) {
return bits & ~ SWT.TRAVERSE_TAB_NEXT;
}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Menu.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Menu.java
index 2bdf5da08c8..9c5bbfab320 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Menu.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Menu.java
@@ -389,7 +389,7 @@ void createItem (MenuItem item, int index) {
long keyEquiv = 0;
if (keyEquivString != null) {
keyEquiv = keyEquivString.characterAtIndex(0);
- if ((keyMask & OS.NSCommandKeyMask) != 0) keyEquiv |= SWT.COMMAND;
+ if ((keyMask & OS.NSEventModifierFlagCommand) != 0) keyEquiv |= SWT.COMMAND;
if ((keyMask & OS.NSAlternateKeyMask) != 0) keyEquiv |= SWT.ALT;
item.accelerator = (int) keyEquiv;
}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/MenuItem.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/MenuItem.java
index 512ef0ab5ca..5ddd1191c6d 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/MenuItem.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/MenuItem.java
@@ -623,9 +623,9 @@ public void setAccelerator (int accelerator) {
nsItem.setKeyEquivalent (nsstring.lowercaseString());
nsstring.release();
int mask = 0;
- if ((accelerator & SWT.SHIFT) != 0) mask |= OS.NSShiftKeyMask;
- if ((accelerator & SWT.CONTROL) != 0) mask |= OS.NSControlKeyMask;
- if ((accelerator & SWT.COMMAND) != 0) mask |= OS.NSCommandKeyMask;
+ if ((accelerator & SWT.SHIFT) != 0) mask |= OS.NSEventModifierFlagShift;
+ if ((accelerator & SWT.CONTROL) != 0) mask |= OS.NSEventModifierFlagControl;
+ if ((accelerator & SWT.COMMAND) != 0) mask |= OS.NSEventModifierFlagCommand;
if ((accelerator & SWT.ALT) != 0) mask |= OS.NSAlternateKeyMask;
nsItem.setKeyEquivalentModifierMask (mask);
}
@@ -951,10 +951,10 @@ boolean updateAccelerator (boolean show) {
if (i < buffer.length && buffer [i] == '\t') {
for (j = i + 1; j < buffer.length; j++) {
switch (buffer [j]) {
- case '\u2303': mask |= OS.NSControlKeyMask; i++; break;
+ case '\u2303': mask |= OS.NSEventModifierFlagControl; i++; break;
case '\u2325': mask |= OS.NSAlternateKeyMask; i++; break;
- case '\u21E7': mask |= OS.NSShiftKeyMask; i++; break;
- case '\u2318': mask |= OS.NSCommandKeyMask; i++; break;
+ case '\u21E7': mask |= OS.NSEventModifierFlagShift; i++; break;
+ case '\u2318': mask |= OS.NSEventModifierFlagCommand; i++; break;
default:
j = buffer.length;
break;
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/MessageBox.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/MessageBox.java
index 53684bcb4d2..2358dc0186b 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/MessageBox.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/MessageBox.java
@@ -151,16 +151,16 @@ public String getMessage () {
*/
public int open () {
NSAlert alert = (NSAlert) new NSAlert().alloc().init();
- int alertType = OS.NSInformationalAlertStyle;
+ int alertType = OS.NSAlertStyleInformational;
if ((style & SWT.ICON_ERROR) != 0) {
- alertType = OS.NSCriticalAlertStyle;
+ alertType = OS.NSAlertStyleCritical;
}
if (((style & SWT.ICON_INFORMATION) != 0) || ((style & SWT.ICON_WORKING) != 0) || ((style & SWT.ICON_QUESTION) != 0)) {
- alertType = OS.NSInformationalAlertStyle;
+ alertType = OS.NSAlertStyleInformational;
alert.setIcon(NSImage.imageNamed(OS.NSImageNameInfo));
}
if ((style & SWT.ICON_WARNING) != 0) {
- alertType = OS.NSWarningAlertStyle;
+ alertType = OS.NSAlertStyleWarning;
alert.setIcon(NSImage.imageNamed(OS.NSImageNameCaution));
}
alert.setAlertStyle(alertType);
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/ProgressBar.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/ProgressBar.java
index eac59b1a91f..a01615d5b55 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/ProgressBar.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/ProgressBar.java
@@ -87,7 +87,11 @@ static int checkStyle (int style) {
@Override
public Point computeSize (int wHint, int hHint, boolean changed) {
checkWidget();
- int size = OS.NSProgressIndicatorPreferredThickness;
+ NSProgressIndicator widget = (NSProgressIndicator)view;
+ NSRect oldFrame = widget.frame();
+ widget.sizeToFit();
+ int size = (int)widget.frame().height;
+ widget.setFrame(oldFrame);
int width = 0, height = 0;
if ((style & SWT.HORIZONTAL) != 0) {
height = size;
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Sash.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Sash.java
index e9792688bc5..200a9ec1e98 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Sash.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Sash.java
@@ -305,7 +305,7 @@ boolean sendKeyEvent(NSEvent nsEvent, int type) {
int xChange = 0, yChange = 0;
int stepSize = PAGE_INCREMENT;
long modifiers = nsEvent.modifierFlags();
- if ((modifiers & OS.NSControlKeyMask) != 0) stepSize = INCREMENT;
+ if ((modifiers & OS.NSEventModifierFlagControl) != 0) stepSize = INCREMENT;
if ((style & SWT.VERTICAL) != 0) {
if (keyCode == 126 || keyCode == 125) break;
xChange = keyCode == 123 ? -stepSize : stepSize;
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/ScrollBar.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/ScrollBar.java
index ea25420fec6..8e29470b678 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/ScrollBar.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/ScrollBar.java
@@ -481,18 +481,10 @@ void sendSelection () {
int hitPart = (int)((NSScroller)view).testPart(point);
Event event = new Event();
switch (hitPart) {
- case OS.NSScrollerDecrementLine:
- value -= increment;
- event.detail = SWT.ARROW_UP;
- break;
case OS.NSScrollerDecrementPage:
value -= pageIncrement;
event.detail = SWT.PAGE_UP;
break;
- case OS.NSScrollerIncrementLine:
- value += increment;
- event.detail = SWT.ARROW_DOWN;
- break;
case OS.NSScrollerIncrementPage:
value += pageIncrement;
event.detail = SWT.PAGE_DOWN;
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Shell.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Shell.java
index 9ef9aa678ea..0029ba4b3bb 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Shell.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Shell.java
@@ -2515,7 +2515,7 @@ void windowSendEvent (long id, long sel, long event) {
* swallowed to handle native traversal. If we find that, force the key event to
* the first responder.
*/
- if ((nsEvent.modifierFlags() & OS.NSControlKeyMask) != 0) {
+ if ((nsEvent.modifierFlags() & OS.NSEventModifierFlagControl) != 0) {
NSString chars = nsEvent.characters();
if (chars != null && chars.length() == 1) {
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Slider.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Slider.java
index 43e73d710fb..a6490a86622 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Slider.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Slider.java
@@ -333,18 +333,10 @@ void sendSelection () {
int hitPart = (int)((NSScroller)view).hitPart();
int value = getSelection ();
switch (hitPart) {
- case OS.NSScrollerDecrementLine:
- event.detail = SWT.ARROW_UP;
- value -= increment;
- break;
case OS.NSScrollerDecrementPage:
value -= pageIncrement;
event.detail = SWT.PAGE_UP;
break;
- case OS.NSScrollerIncrementLine:
- value += increment;
- event.detail = SWT.ARROW_DOWN;
- break;
case OS.NSScrollerIncrementPage:
value += pageIncrement;
event.detail = SWT.PAGE_DOWN;
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Table.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Table.java
index 49549fcf9ca..5d6825ec32a 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Table.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Table.java
@@ -258,7 +258,7 @@ boolean canDragRowsWithIndexes_atPoint(long id, long sel, long rowIndexes, NSPoi
boolean drag = (state & DRAG_DETECT) != 0 && hooks (SWT.DragDetect);
if (drag) {
- if (!widget.isRowSelected(row) && (modifiers & (OS.NSCommandKeyMask | OS.NSShiftKeyMask | OS.NSAlternateKeyMask)) == 0) {
+ if (!widget.isRowSelected(row) && (modifiers & (OS.NSEventModifierFlagCommand | OS.NSEventModifierFlagShift | OS.NSAlternateKeyMask)) == 0) {
NSIndexSet set = (NSIndexSet)new NSIndexSet().alloc();
set = set.initWithIndex(row);
widget.selectRowIndexes (set, false);
@@ -2080,7 +2080,7 @@ void mouseDown (long id, long sel, long theEvent) {
// which is interpreted as a single click that clears the selection. Fix is to ignore control-click if the
// view has a context menu.
NSEvent event = new NSEvent(theEvent);
- if ((event.modifierFlags() & OS.NSControlKeyMask) != 0) return;
+ if ((event.modifierFlags() & OS.NSEventModifierFlagControl) != 0) return;
}
super.mouseDown(id, sel, theEvent);
}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Text.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Text.java
index ee965adcccd..17fd2df1630 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Text.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Text.java
@@ -1714,7 +1714,7 @@ boolean sendKeyEvent (NSEvent nsEvent, int type) {
if (!result) return result;
if (type != SWT.KeyDown) return result;
long modifierFlags = nsEvent.modifierFlags();
- if ((modifierFlags & OS.NSCommandKeyMask) != 0) {
+ if ((modifierFlags & OS.NSEventModifierFlagCommand) != 0) {
short keyCode = nsEvent.keyCode ();
switch (keyCode) {
case 7: /* X */
@@ -2460,8 +2460,8 @@ int traversalCode (int key, NSEvent theEvent) {
bits &= ~SWT.TRAVERSE_RETURN;
if (key == 48 /* Tab */ && theEvent != null) {
long modifiers = theEvent.modifierFlags ();
- boolean next = (modifiers & OS.NSShiftKeyMask) == 0;
- if (next && (modifiers & OS.NSControlKeyMask) == 0) {
+ boolean next = (modifiers & OS.NSEventModifierFlagShift) == 0;
+ if (next && (modifiers & OS.NSEventModifierFlagControl) == 0) {
bits &= ~(SWT.TRAVERSE_TAB_NEXT | SWT.TRAVERSE_TAB_PREVIOUS);
}
}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Tracker.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Tracker.java
index aa8e2507e93..374a5f3c93d 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Tracker.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Tracker.java
@@ -559,9 +559,9 @@ void key (NSEvent nsEvent) {
int mask = 0;
switch (keyCode) {
case SWT.ALT: mask = OS.NSAlternateKeyMask; break;
- case SWT.CONTROL: mask = OS.NSControlKeyMask; break;
- case SWT.COMMAND: mask = OS.NSCommandKeyMask; break;
- case SWT.SHIFT: mask = OS.NSShiftKeyMask; break;
+ case SWT.CONTROL: mask = OS.NSEventModifierFlagControl; break;
+ case SWT.COMMAND: mask = OS.NSEventModifierFlagCommand; break;
+ case SWT.SHIFT: mask = OS.NSEventModifierFlagShift; break;
case SWT.CAPS_LOCK:
Event event = new Event();
event.keyCode = keyCode;
@@ -583,7 +583,7 @@ void key (NSEvent nsEvent) {
}
}
- int stepSize = (modifierFlags & OS.NSControlKeyMask) != 0 ? STEPSIZE_SMALL : STEPSIZE_LARGE;
+ int stepSize = (modifierFlags & OS.NSEventModifierFlagControl) != 0 ? STEPSIZE_SMALL : STEPSIZE_LARGE;
int xChange = 0, yChange = 0;
switch (nsKeyCode) {
case 53: /* Esc */
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/TrayItem.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/TrayItem.java
index 00b271bfdf7..328e6139996 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/TrayItem.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/TrayItem.java
@@ -510,7 +510,7 @@ boolean shouldShowMenu (NSEvent event) {
if (!(hooks(SWT.Selection) || hooks(SWT.DefaultSelection))) {
return true;
}
- if ((event.modifierFlags() & OS.NSDeviceIndependentModifierFlagsMask) == OS.NSControlKeyMask) {
+ if ((event.modifierFlags() & OS.NSDeviceIndependentModifierFlagsMask) == OS.NSEventModifierFlagControl) {
return true;
}
return false;
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Tree.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Tree.java
index 0a92f0beb2f..4195ef69532 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Tree.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Tree.java
@@ -312,7 +312,7 @@ boolean canDragRowsWithIndexes_atPoint(long id, long sel, long rowIndexes, NSPoi
boolean drag = (state & DRAG_DETECT) != 0 && hooks (SWT.DragDetect);
if (drag) {
- if (!widget.isRowSelected(row) && (modifiers & (OS.NSCommandKeyMask | OS.NSShiftKeyMask | OS.NSAlternateKeyMask | OS.NSControlKeyMask)) == 0) {
+ if (!widget.isRowSelected(row) && (modifiers & (OS.NSEventModifierFlagCommand | OS.NSEventModifierFlagShift | OS.NSAlternateKeyMask | OS.NSEventModifierFlagControl)) == 0) {
NSIndexSet set = (NSIndexSet)new NSIndexSet().alloc();
set = set.initWithIndex(row);
widget.selectRowIndexes (set, false);
@@ -2133,7 +2133,7 @@ void mouseDown (long id, long sel, long theEvent) {
// it from menuForEvent:. This has the side effect, however, of sending control-click to the NSTableView,
// which is interpreted as a single click that clears the selection. Fix is to ignore control-click,
NSEvent event = new NSEvent(theEvent);
- if ((event.modifierFlags() & OS.NSControlKeyMask) != 0) return;
+ if ((event.modifierFlags() & OS.NSEventModifierFlagControl) != 0) return;
}
super.mouseDown(id, sel, theEvent);
}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Widget.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Widget.java
index 9e3b018b5f1..3804a64ecf6 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Widget.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/widgets/Widget.java
@@ -1879,9 +1879,9 @@ boolean setInputState (Event event, NSEvent nsEvent, int type) {
}
long modifierFlags = nsEvent.modifierFlags();
if ((modifierFlags & OS.NSAlternateKeyMask) != 0) event.stateMask |= SWT.ALT;
- if ((modifierFlags & OS.NSShiftKeyMask) != 0) event.stateMask |= SWT.SHIFT;
- if ((modifierFlags & OS.NSControlKeyMask) != 0) event.stateMask |= SWT.CONTROL;
- if ((modifierFlags & OS.NSCommandKeyMask) != 0) event.stateMask |= SWT.COMMAND;
+ if ((modifierFlags & OS.NSEventModifierFlagShift) != 0) event.stateMask |= SWT.SHIFT;
+ if ((modifierFlags & OS.NSEventModifierFlagControl) != 0) event.stateMask |= SWT.CONTROL;
+ if ((modifierFlags & OS.NSEventModifierFlagCommand) != 0) event.stateMask |= SWT.COMMAND;
long state = NSEvent.pressedMouseButtons();
if ((state & 0x1) != 0) event.stateMask |= SWT.BUTTON1;
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/library/make_common.mak b/bundles/org.eclipse.swt/Eclipse SWT/common/library/make_common.mak
index d66419e442d..672f294dafe 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/common/library/make_common.mak
+++ b/bundles/org.eclipse.swt/Eclipse SWT/common/library/make_common.mak
@@ -14,4 +14,4 @@
maj_ver=4
min_ver=974
-rev=7
+rev=10
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/widgets/Item.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/widgets/Item.java
index fd33f71ca76..c3e16f24878 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/widgets/Item.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/widgets/Item.java
@@ -230,7 +230,6 @@ private void handleDPIChange(Event event) {
// Refresh the image
Image image = getImage();
if (image != null) {
- setImage(null);
setImage(image);
}
}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Composite.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Composite.java
index e6b18fbacfb..23e74f61c62 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Composite.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Composite.java
@@ -1396,7 +1396,20 @@ void moveAbove (long child, long sibling) {
long parentHandle = parentingHandle ();
if (GTK.GTK4) {
if (sibling == 0) {
- GTK4.gtk_widget_insert_after(child, parentHandle, 0L);
+ /*
+ * True raise-to-top-of-paint-order, scoped to Shell-direct
+ * children (e.g. an overlay pane raised via moveAbove(null)).
+ * GTK4 has no per-widget GdkWindow, so this same native sibling
+ * list also backs Composite._getChildren(); other code already
+ * relies on today's insert_after(..., NULL) behaviour to keep
+ * getChildren() stable for non-Shell parents, so this is not
+ * changed generally.
+ */
+ if (this instanceof Shell) {
+ GTK4.gtk_widget_insert_before(child, parentHandle, 0L);
+ } else {
+ GTK4.gtk_widget_insert_after(child, parentHandle, 0L);
+ }
} else {
GTK4.gtk_widget_insert_before(child, parentHandle, sibling);
}
@@ -1409,13 +1422,27 @@ void moveAbove (long child, long sibling) {
void moveBelow (long child, long sibling) {
if (child == sibling) return;
long parentHandle = parentingHandle ();
- if (sibling == 0 && parentHandle == fixedHandle) {
+ /*
+ * GTK3-only: when sibling == 0 (move to the bottom of the z-order) the
+ * child would be stacked behind the scrolled content's GdkWindow, hiding
+ * overlay controls such as Table/Tree editors. Re-place it just above the
+ * scrolled content instead.
+ *
+ * Not needed on GTK4: widgets have no per-widget GdkWindow, so paint order
+ * is simply the parent's child-list order (first = bottom, last = top).
+ */
+ if (!GTK.GTK4 && sibling == 0 && parentHandle == fixedHandle) {
moveAbove (child, scrolledHandle != 0 ? scrolledHandle : handle);
return;
}
if (GTK.GTK4) {
+ /* Mirror image of moveAbove - see the comment there. */
if (sibling == 0) {
- GTK4.gtk_widget_insert_before(child, parentHandle, 0L);
+ if (this instanceof Shell) {
+ GTK4.gtk_widget_insert_after(child, parentHandle, 0L);
+ } else {
+ GTK4.gtk_widget_insert_before(child, parentHandle, 0L);
+ }
} else {
GTK4.gtk_widget_insert_after(child, parentHandle, sibling);
}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Control.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Control.java
index c78a643e29c..59ac7ed8968 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Control.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Control.java
@@ -815,7 +815,13 @@ void createWidget(int index) {
checkBuffered();
showWidget();
setInitialBounds();
- setZOrder(null, false, false);
+ if (GTK.GTK4 && !(this instanceof Shell)) {
+ /* moveBelow(null) now sinks Shell-direct children to the back
+ * (see Composite.moveAbove) - guard against undoing that. */
+ setZOrder(null, false, false, !(parent instanceof Shell));
+ } else {
+ setZOrder(null, false, false);
+ }
if (!GTK.GTK4) setRelations();
checkMirrored();
checkBorder();
@@ -2501,7 +2507,9 @@ public void removePaintListener(PaintListener listener) {
void removeRelation () {
if (!isDescribedByLabel ()) return; /* there will not be any */
if (labelRelation != null) {
- _getAccessible().removeRelation (ACC.RELATION_LABELLED_BY, labelRelation._getAccessible());
+ if (accessible != null && labelRelation.accessible != null) {
+ accessible.removeRelation (ACC.RELATION_LABELLED_BY, labelRelation.accessible);
+ }
labelRelation = null;
}
}
@@ -4831,10 +4839,10 @@ void destroyWidget() {
// GTK windows don't have a parent, so destroy it now
GTK4.gtk_window_destroy(currHandle);
} else if (parent != null) {
- if (fixedHandle != 0) {
- // Remove widget from hierarchy by removing it from parent container
- OS.swt_fixed_remove(parent.parentingHandle(), fixedHandle);
- }
+ /* Use currHandle, not fixedHandle alone - widgets without a
+ * separate fixedHandle wrapper were otherwise never actually
+ * unparented here, leaving them alive and rendered natively. */
+ OS.swt_fixed_remove(parent.parentingHandle(), currHandle);
} else {
assert false : "widgets must have a parent or be a GtkWindow";
}
@@ -5923,7 +5931,12 @@ public boolean setParent (Composite parent) {
allocation.height = height;
gtk_widget_size_allocate(topHandle, allocation, -1);
this.parent = parent;
- setZOrder (null, false, true);
+ if (GTK.GTK4 && !(this instanceof Shell)) {
+ /* See createWidget() re: guarding fixChildren on GTK4. */
+ setZOrder (null, false, true, !(parent instanceof Shell));
+ } else {
+ setZOrder (null, false, true);
+ }
reskin (SWT.ALL);
// restore focus to the last Control that had it, if focus is now gone
if (focusControlBeforeReparent != null && !focusControlBeforeReparent.isDisposed() && display.getFocusControl() == null) {
@@ -6153,6 +6166,20 @@ public void setVisible (boolean visible) {
if (enableWindow != 0) GDK.gdk_window_show_unraised(enableWindow);
}
gtk_widget_show (topHandle);
+ /*
+ * On GTK4, a Composite laid out while it (or an ancestor) is hidden can end
+ * up with its children sized against a stale 0x0 client area, because
+ * gtk_widget_hide() resets allocations to 0x0 (issue #3330) and setBounds on a
+ * hidden widget shows/allocates/re-hides it. The size given while hidden is
+ * still stored in the parent's swt_fixed child list, so re-running the parent's
+ * size allocation re-applies this control's real size (restoring its client
+ * area), after which a re-layout of its own subtree lets the children pick up
+ * the now-correct client area. See issue #3450.
+ */
+ if (GTK.GTK4 && this instanceof Composite composite && composite.layout != null) {
+ parent.forceResize ();
+ composite.layout (true, true);
+ }
}
} else {
/*
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java
index ab7d2e476e3..4a1bc89cce3 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java
@@ -135,12 +135,14 @@ public class Display extends Device implements Executor {
long changeValueProc;
long snapshotDrawProc, keyPressReleaseProc, focusProc, windowActiveProc, enterMotionProc, leaveProc,
scrollProc, resizeProc, layoutProc, activateProc, gesturePressReleaseProc;
+ long menuItemsChangedProc;
long notifyProc;
long computeSizeProc;
Callback windowCallback2, windowCallback3, windowCallback4, windowCallback5, windowCallback6;
Callback changeValue;
Callback snapshotDraw, keyPressReleaseCallback, focusCallback, windowActiveCallback, enterMotionCallback, computeSizeCallback,
scrollCallback, leaveCallback, resizeCallback, layoutCallback, activateCallback, gesturePressReleaseCallback;
+ Callback menuItemsChangedCallback;
Callback notifyCallback;
EventTable eventTable, filterTable;
static String APP_NAME = "SWT"; //$NON-NLS-1$
@@ -3633,6 +3635,10 @@ void initializeCallbacks () {
activateCallback = new Callback(this, "activateProc", void.class, new Type[] {long.class, long.class, long.class}); //$NON-NLS-1$
activateProc = activateCallback.getAddress();
+ menuItemsChangedCallback = new Callback(this, "menuItemsChangedProc", void.class, new Type[] {
+ long.class, int.class, int.class, int.class, long.class}); //$NON-NLS-1$
+ menuItemsChangedProc = menuItemsChangedCallback.getAddress();
+
computeSizeCallback = new Callback(this, "computeSizeProc", void.class, new Type[] {long.class, long.class, long.class}); //$NON-NLS-1$
computeSizeProc = computeSizeCallback.getAddress();
}
@@ -4671,6 +4677,10 @@ void releaseDisplay () {
activateCallback = null;
activateProc = 0;
+ menuItemsChangedCallback.dispose();
+ menuItemsChangedCallback = null;
+ menuItemsChangedProc = 0;
+
computeSizeCallback.dispose();
computeSizeCallback = null;
computeSizeProc = 0;
@@ -6145,6 +6155,11 @@ void activateProc(long action, long parameter, long user_data) {
widget.gtk_activate(user_data);
}
+void menuItemsChangedProc(long model, int position, int removed, int added, long user_data) {
+ Widget widget = getWidget(user_data);
+ if (widget instanceof Menu menu) menu.modelItemsChanged();
+}
+
void resizeProc(long handle, int width, int height) {
Widget widget = getWidget(handle);
if (widget != null) widget.gtk_size_allocate(handle, 0);
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Menu.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Menu.java
index 8f5c2159541..19467e4b2d3 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Menu.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Menu.java
@@ -261,6 +261,88 @@ boolean ableToSetLocation() {
return hasLocation;
}
+/*
+ * GtkPopoverMenu inserts section-separator widgets from a GLib idle callback
+ * (gtk_menu_section_box_handle_sync_separators). Calling gtk_popover_popup()
+ * synchronously would measure the menu before those widgets exist, undersizing
+ * it into a scrollbar. Deferring via asyncExec lets that idle run first.
+ */
+void popupGtk4Popover(boolean hasPointingTo, int pointX, int pointY) {
+ if (isDisposed()) return;
+ display.asyncExec(() -> {
+ if (isDisposed()) return;
+ if (hasPointingTo) {
+ GdkRectangle popoverPosition = new GdkRectangle();
+ popoverPosition.x = pointX;
+ popoverPosition.y = pointY;
+ popoverPosition.width = popoverPosition.height = 1;
+ GTK.gtk_popover_set_pointing_to(handle, popoverPosition);
+ // Hide during the fit (scheduleGtk4PopoverFit) so the menu appears
+ // directly at its final location without a visible jump.
+ GTK.gtk_widget_set_opacity(handle, 0.0);
+ GTK.gtk_popover_popup(handle);
+ scheduleGtk4PopoverFit(pointX, pointY, 0);
+ } else {
+ GTK.gtk_popover_popup(handle);
+ }
+ });
+}
+
+/*
+ * Keep a bottom-of-screen popup menu from being shrunk into a scrolled view.
+ *
+ * GtkPopover's layout carries GDK_ANCHOR_RESIZE_Y (a GtkPopoverMenu can always
+ * shrink), so when the menu fits neither below the anchor nor flipped above it,
+ * the compositor shrinks it and shows a scrollbar. Wayland has no global window
+ * coordinates, so we cannot compute the overflow directly; instead we compare
+ * the popover's allocated height to its natural height and shift the anchor up
+ * by the shortfall, which yields exactly that much more room below. Repeated
+ * until it fits (usually 2-3 passes; the bound is just a safety cap). The
+ * popover stays hidden until settled.
+ */
+static final int GTK4_POPOVER_FIT_MAX_ITERATIONS = 6;
+
+void scheduleGtk4PopoverFit(int pointX, int pointY, int iteration) {
+ display.asyncExec(() -> {
+ if (isDisposed()) return;
+ if (!GTK.gtk_widget_get_mapped(handle)) {
+ // Menu was dismissed before it settled; make sure it is not left
+ // permanently transparent for a future show.
+ GTK.gtk_widget_set_opacity(handle, 1.0);
+ return;
+ }
+ if (iteration >= GTK4_POPOVER_FIT_MAX_ITERATIONS) {
+ revealGtk4Popover();
+ return;
+ }
+ int[] natHeight = new int[1];
+ GTK4.gtk_widget_measure(handle, GTK.GTK_ORIENTATION_VERTICAL, -1, null, natHeight, null, null);
+ int allocated = GTK4.gtk_widget_get_height(handle);
+ if (natHeight[0] <= 0 || allocated <= 0) {
+ // Not measured/allocated yet; wait another turn.
+ scheduleGtk4PopoverFit(pointX, pointY, iteration + 1);
+ return;
+ }
+ int shortfall = natHeight[0] - allocated;
+ if (shortfall <= 0) {
+ revealGtk4Popover();
+ return;
+ }
+ int newPointY = pointY - shortfall;
+ GdkRectangle popoverPosition = new GdkRectangle();
+ popoverPosition.x = pointX;
+ popoverPosition.y = newPointY;
+ popoverPosition.width = popoverPosition.height = 1;
+ GTK.gtk_popover_set_pointing_to(handle, popoverPosition);
+ scheduleGtk4PopoverFit(pointX, newPointY, iteration + 1);
+ });
+}
+
+void revealGtk4Popover() {
+ if (isDisposed()) return;
+ GTK.gtk_widget_set_opacity(handle, 1.0);
+}
+
void _setVisible (boolean visible) {
if (visible == GTK.gtk_widget_get_mapped (handle)) return;
if (visible) {
@@ -307,7 +389,6 @@ void _setVisible (boolean visible) {
long eventPtr = 0;
if (ableToSetLocation()) {
if (GTK.GTK4) {
- GdkRectangle popoverPosition = new GdkRectangle();
/*
* gtk_popover_set_pointing_to expects coordinates in the coordinate
* space of the popover's current parent widget.
@@ -323,24 +404,16 @@ void _setVisible (boolean visible) {
* relative to that control - do not apply the shell to
* parent.handle translation.
*/
+ int pointX = x, pointY = y;
long currentParent = GTK.gtk_widget_get_parent(handle);
if (currentParent == parent.handle) {
double[] relX = new double[1], relY = new double[1];
if (GTK4.gtk_widget_translate_coordinates(parent.getShell().topHandle(), parent.handle, x, y, relX, relY)) {
- popoverPosition.x = (int) relX[0];
- popoverPosition.y = (int) relY[0];
- } else {
- popoverPosition.x = x;
- popoverPosition.y = y;
+ pointX = (int) relX[0];
+ pointY = (int) relY[0];
}
- } else {
- popoverPosition.x = x;
- popoverPosition.y = y;
}
- popoverPosition.width = popoverPosition.height = 1;
- GTK.gtk_popover_set_pointing_to(handle, popoverPosition);
-
- GTK.gtk_popover_popup(handle);
+ popupGtk4Popover(true, pointX, pointY);
} else {
// Create the GdkEvent manually as we need to control
// certain fields like the event window
@@ -384,7 +457,7 @@ void _setVisible (boolean visible) {
}
} else {
if (GTK.GTK4) {
- GTK.gtk_popover_popup(handle);
+ popupGtk4Popover(false, 0, 0);
} else {
/*
* GTK Feature: gtk_menu_popup is deprecated as of GTK3.22 and the new method gtk_menu_popup_at_pointer
@@ -871,6 +944,86 @@ long gtk_map (long widget) {
return super.gtk_map(widget);
}
+/**
+ * Re-runs the CASCADE submenu SHOW/HIDE wiring after the menu structure changed
+ * while already mapped. The initial wiring at
+ * {@link #gtk_map}/{@link #gtk_show} only covers submenus that existed then;
+ * ones attached, replaced or rebuilt later would otherwise never get their
+ * {@link SWT#Show} event, making lazily-populated submenus appear empty.
+ */
+private void reconnectDropDownMenuSignalsIfMapped() {
+ if (!GTK.GTK4) return;
+ if ((style & SWT.BAR) != 0) {
+ if (handle != 0 && GTK.gtk_widget_get_mapped(handle)) {
+ connectDropDownMenuSignals();
+ }
+ } else if ((style & SWT.POP_UP) != 0) {
+ if (handle != 0 && GTK.gtk_widget_get_mapped(handle)) {
+ connectCascadeSubMenuSignals(this, handle);
+ }
+ } else if ((style & SWT.DROP_DOWN) != 0) {
+ if (popoverHandle != 0 && GTK.gtk_widget_get_mapped(popoverHandle)) {
+ connectCascadeSubMenuSignals(this, popoverHandle);
+ }
+ }
+}
+
+/**
+ * Hooks this menu's {@code items-changed} handler on the given {@code GMenu}
+ * model (no-op for {@code 0}), routing back via {@link #handle}. Connected in the
+ * signal's after phase so it runs once GTK's {@code GtkMenuTracker} has
+ * already (re)built the nested GtkPopoverMenu widgets, letting re-wiring run
+ * synchronously.
+ */
+void hookItemsChanged(long model) {
+ if (GTK.GTK4 && model != 0) {
+ long closure = OS.g_cclosure_new(display.menuItemsChangedProc, handle, 0);
+ OS.g_signal_connect_closure(model, OS.items_changed, closure, true);
+ }
+}
+
+/**
+ * Called when this menu's {@code GMenuModel} emitted {@code items-changed} (its
+ * content was modified after creation). Re-runs the CASCADE submenu wiring from
+ * the nearest mapped ancestor so any nested GtkPopoverMenu GTK (re)built gets its
+ * {@link SWT#Show} routed to the SWT DROP_DOWN submenu. Runs synchronously (see
+ * {@link #hookItemsChanged(long)}); a cheap no-op if no ancestor is mapped.
+ */
+void modelItemsChanged() {
+ if (!GTK.GTK4 || isDisposed()) return;
+ Menu root = this;
+ while (root.cascade != null && root.cascade.parent != null && !root.cascade.parent.isDisposed()) {
+ root = root.cascade.parent;
+ }
+ root.reconnectDropDownMenuSignalsIfMapped();
+}
+
+/**
+ * Wires the SHOW/HIDE signals of the given DROP_DOWN {@code submenu} to the
+ * discovered {@code popover} GtkPopoverMenu widget, refreshing the SWT-side
+ * cache. If the submenu was previously wired to a now-stale popover (e.g. GTK
+ * rebuilt the widget after a model change), the stale handle is released first.
+ */
+private void wireSubMenuPopover(Menu submenu, long popover) {
+ if (submenu.popoverHandle == popover) return;
+ if (submenu.popoverHandle != 0) {
+ /*
+ * Release the stale popover we previously cached (mirrors deregister()). Its
+ * SHOW/HIDE closures are deliberately left connected: GTK normally destroys the
+ * widget along with the model change, and should it survive, removeWidget() above
+ * means its handlers no longer resolve to a widget and simply no-op.
+ */
+ display.removeWidget(submenu.popoverHandle);
+ OS.g_object_unref(submenu.popoverHandle);
+ submenu.popoverHandle = 0;
+ }
+ OS.g_object_ref(popover);
+ submenu.popoverHandle = popover;
+ display.addWidget(popover, submenu);
+ OS.g_signal_connect_closure_by_id(popover, display.signalIds[SHOW], 0, display.getClosure(SHOW), false);
+ OS.g_signal_connect_closure_by_id(popover, display.signalIds[HIDE], 0, display.getClosure(HIDE), false);
+}
+
private void connectDropDownMenuSignals() {
if (items == null) return;
long barItem = GTK4.gtk_widget_get_first_child(handle);
@@ -878,17 +1031,13 @@ private void connectDropDownMenuSignals() {
for (MenuItem menuItem : items) {
if (barItem == 0) break;
if ((menuItem.style & SWT.SEPARATOR) != 0) continue;
- if (menuItem.menu != null && menuItem.menu.popoverHandle == 0) {
+ if (menuItem.menu != null) {
long popover = findGtkPopoverMenuChild(barItem);
- if (popover != 0) {
- OS.g_object_ref(popover);
- menuItem.menu.popoverHandle = popover;
- display.addWidget(popover, menuItem.menu);
- OS.g_signal_connect_closure_by_id(popover, display.signalIds[SHOW], 0, display.getClosure(SHOW), false);
- OS.g_signal_connect_closure_by_id(popover, display.signalIds[HIDE], 0, display.getClosure(HIDE), false);
- /*
- * Also connect SHOW/HIDE signals for nested CASCADE submenus.
- */
+ /* Re-wire when the discovered popover differs from the cache (initial or GTK rebuilt it). */
+ if (popover != 0 && menuItem.menu.popoverHandle != popover) {
+ wireSubMenuPopover(menuItem.menu, popover);
+ }
+ if (menuItem.menu.popoverHandle != 0) {
connectCascadeSubMenuSignals(menuItem.menu);
}
}
@@ -900,30 +1049,33 @@ private void connectCascadeSubMenuSignals(Menu menu) {
connectCascadeSubMenuSignals(menu, menu.popoverHandle);
}
+/**
+ * Connects SHOW/HIDE signals for the CASCADE submenus of the given menu. A
+ * submenu whose GtkPopoverMenu is not (yet) present is simply skipped; the next
+ * pass triggered by MAP, SHOW or "items-changed" picks it up.
+ */
private void connectCascadeSubMenuSignals(Menu menu, long parentPopoverHandle) {
if (menu == null || parentPopoverHandle == 0 || menu.items == null) return;
for (MenuItem item : menu.items) {
if ((item.style & SWT.CASCADE) != 0 && item.menu != null) {
- /*
- * item.menu is the CASCADE submenu (always SWT.DROP_DOWN style).
- * Its popoverHandle is 0 until we find and register its GtkPopoverMenu.
- * Skip if already connected (popoverHandle != 0).
- */
- if (item.menu.popoverHandle != 0) continue;
+ /* Re-discover every pass: a rebuild can make GTK replace the widget,
+ * leaving a stale handle whose SHOW never fires (submenu appears empty). */
long nestedPopover = findNestedPopoverForModel(parentPopoverHandle, item.menu.modelHandle);
if (nestedPopover != 0) {
- OS.g_object_ref(nestedPopover);
- item.menu.popoverHandle = nestedPopover;
- display.addWidget(nestedPopover, item.menu);
- OS.g_signal_connect_closure_by_id(nestedPopover, display.signalIds[SHOW], 0, display.getClosure(SHOW), false);
- OS.g_signal_connect_closure_by_id(nestedPopover, display.signalIds[HIDE], 0, display.getClosure(HIDE), false);
- // Recurse to handle further nested CASCADE submenus
+ if (item.menu.popoverHandle != nestedPopover) {
+ wireSubMenuPopover(item.menu, nestedPopover);
+ }
connectCascadeSubMenuSignals(item.menu);
}
}
}
}
+/**
+ * Recursively searches the widget subtree rooted at {@code parentWidget} for the
+ * nested GtkPopoverMenu whose GMenuModel is {@code targetModel}, returning its
+ * handle or {@code 0} if it is not (yet) present.
+ */
private long findNestedPopoverForModel(long parentWidget, long targetModel) {
if (parentWidget == 0 || targetModel == 0) return 0;
long child = GTK4.gtk_widget_get_first_child(parentWidget);
@@ -983,6 +1135,10 @@ long gtk_show (long widget) {
return 0;
}
sendEvent (SWT.Show);
+ /* Wire cascade submenu SHOW/HIDE signals once the DROP_DOWN popover is shown. */
+ if (GTK.GTK4 && (style & SWT.DROP_DOWN) != 0 && popoverHandle != 0) {
+ connectCascadeSubMenuSignals(this, popoverHandle);
+ }
if (OS.ubuntu_menu_proxy_get() != 0) {
MenuItem[] items = getItems();
for (int i=0; idetailed action name: actionId qualified with the prefix the
+ * action group was inserted under (see Menu#createHandle), plus a target for
+ * SWT.RADIO. Use this where an action is referenced by name, such as
+ * g_menu_item_new() or gtk_named_action_new(), and never with the GActionMap
+ * API.
+ */
String actionName;
/**
@@ -264,38 +277,63 @@ void createHandle(int index) {
case SWT.SEPARATOR:
modelHandle = OS.g_menu_new();
handle = OS.g_menu_item_new_section(null, modelHandle);
+ /*
+ * A separator starts a new section GMenu; observe it for
+ * "items-changed" too so submenus added into this section later get
+ * wired (see Menu#hookItemsChanged and issue #3451).
+ */
+ parent.hookItemsChanged(modelHandle);
break;
case SWT.RADIO:
long stringVariantType = OS.g_variant_type_new(OS.G_VARIANT_TYPE_STRING);
+ actionId = String.valueOf(this.hashCode());
actionHandle = OS.g_simple_action_new_stateful(
- Converter.javaStringToCString(String.valueOf(this.hashCode())),
+ Converter.javaStringToCString(actionId),
stringVariantType,
OS.g_variant_new_string(Converter.javaStringToCString("untoggled")));
OS.g_action_map_add_action(parent.actionGroup, actionHandle);
- actionName = String.valueOf(parent.hashCode()) + "." + String.valueOf(this.hashCode()) + "::toggled";
+ actionName = String.valueOf(parent.hashCode()) + "." + actionId + "::toggled";
handle = OS.g_menu_item_new(null, Converter.javaStringToCString(actionName));
OS.g_variant_type_free(stringVariantType);
break;
case SWT.CHECK:
long boolVariantType = OS.g_variant_type_new(OS.G_VARIANT_TYPE_BOOLEAN);
+ actionId = String.valueOf(this.hashCode());
actionHandle = OS.g_simple_action_new_stateful(
- Converter.javaStringToCString(String.valueOf(this.hashCode())),
+ Converter.javaStringToCString(actionId),
0,
OS.g_variant_new_boolean(false));
OS.g_action_map_add_action(parent.actionGroup, actionHandle);
- actionName = String.valueOf(parent.hashCode()) + "." + String.valueOf(this.hashCode());
+ actionName = String.valueOf(parent.hashCode()) + "." + actionId;
handle = OS.g_menu_item_new(null, Converter.javaStringToCString(actionName));
OS.g_variant_type_free(boolVariantType);
break;
case SWT.CASCADE:
modelHandle = OS.g_menu_new();
- handle = OS.g_menu_item_new_submenu(Converter.javaStringToCString(""), modelHandle);
+ /*
+ * Give the CASCADE item an action so it can be enabled/disabled like
+ * on the other platforms and like GTK3. A plain submenu item created
+ * via g_menu_item_new_submenu has no action, so its GtkModelButton is
+ * always sensitive: setEnabled(false) would be a no-op and the (possibly
+ * empty) submenu could still be opened. Attaching a SimpleAction makes
+ * the item follow the action's enabled state. While the action is
+ * enabled, activating the item still navigates into the submenu (the
+ * action is not triggered); while disabled, the item is insensitive and
+ * the submenu cannot be opened.
+ */
+ actionId = String.valueOf(this.hashCode());
+ actionHandle = OS.g_simple_action_new(Converter.javaStringToCString(actionId), 0);
+ OS.g_action_map_add_action(parent.actionGroup, actionHandle);
+ actionName = String.valueOf(parent.hashCode()) + "." + actionId;
+ handle = OS.g_menu_item_new(Converter.javaStringToCString(""), Converter.javaStringToCString(actionName));
+ OS.g_menu_item_set_submenu(handle, modelHandle);
break;
case SWT.PUSH:
default:
- actionHandle = OS.g_simple_action_new(Converter.javaStringToCString(String.valueOf(this.hashCode())), 0);
+ actionId = String.valueOf(this.hashCode());
+ actionHandle = OS.g_simple_action_new(Converter.javaStringToCString(actionId), 0);
OS.g_action_map_add_action(parent.actionGroup, actionHandle);
- actionName = String.valueOf(parent.hashCode()) + "." + String.valueOf(this.hashCode());
+ actionName = String.valueOf(parent.hashCode()) + "." + actionId;
handle = OS.g_menu_item_new(null, Converter.javaStringToCString(actionName));
break;
}
@@ -517,10 +555,6 @@ public boolean getEnabled () {
checkWidget();
if (GTK.GTK4) {
- if ((style & SWT.CASCADE) != 0) {
- return true;
- }
-
return OS.g_action_get_enabled(actionHandle);
} else {
return GTK.gtk_widget_get_sensitive(handle);
@@ -653,6 +687,10 @@ long gtk_activate (long widget) {
}
}
+ if (GTK.GTK4 && (style & SWT.CHECK) != 0) {
+ OS.g_simple_action_set_state(actionHandle, OS.g_variant_new_boolean(!getSelection()));
+ }
+
sendSelectionEvent (SWT.Selection);
return 0;
}
@@ -684,7 +722,7 @@ void hookEvents() {
super.hookEvents();
if (GTK.GTK4) {
- if ((style & SWT.PUSH) != 0 || (style & SWT.RADIO) != 0) {
+ if ((style & SWT.PUSH) != 0 || (style & SWT.RADIO) != 0 || (style & SWT.CHECK) != 0) {
OS.g_signal_connect(actionHandle, OS.activate, display.activateProc, handle);
}
} else {
@@ -739,7 +777,12 @@ void releaseWidget() {
super.releaseWidget();
if (GTK.GTK4) {
- if (parent.actionGroup != 0 && actionName != null) OS.g_action_map_remove_action(parent.actionGroup, Converter.javaStringToCString(actionName));
+ /*
+ * Remove by actionId, not actionName: GActionMap keys actions by their own
+ * name, so passing the prefixed detailed name silently removes nothing and
+ * leaks the action for the lifetime of the parent's action group.
+ */
+ if (parent.actionGroup != 0 && actionId != null) OS.g_action_map_remove_action(parent.actionGroup, Converter.javaStringToCString(actionId));
} else {
long accelGroup = getAccelGroup();
if (accelGroup != 0) removeAccelerator(accelGroup);
@@ -1144,6 +1187,18 @@ public void setMenu (Menu menu) {
OS.g_menu_remove(section.getSectionHandle(), section.getItemPosition(this));
OS.g_menu_insert_item(section.getSectionHandle(), section.getItemPosition(this), handle);
+
+ /*
+ * If a DROP_DOWN is attached while its parent is already mapped (contributions
+ * added/rebuilt after the parent was shown, e.g. workspace restore), wire its
+ * SHOW/HIDE now; otherwise SWT.Show never fires and the submenu appears empty
+ * (issue #3451). The g_menu calls above already emit "items-changed" on the
+ * section model, so this is normally redundant; go through modelItemsChanged()
+ * anyway so both paths re-wire from the same (root) menu.
+ */
+ if (menu != null) {
+ parent.modelItemsChanged();
+ }
} else {
long accelGroup = getAccelGroup ();
if (accelGroup != 0) removeAccelerators (accelGroup);
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Table.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Table.java
index f750fa7b97e..f75676a3650 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Table.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Table.java
@@ -711,6 +711,13 @@ void createHandle (int index) {
if (!searchEnabled ()) {
GTK.gtk_tree_view_set_search_column (handle, -1);
}
+ if (GTK.GTK4) {
+ // Required before TableDropTargetEffect may use
+ // gtk_tree_view_set_drag_dest_row(), see Tree.createHandle() for details.
+ long formats = GTK4.gdk_content_formats_builder_free_to_formats(GTK4.gdk_content_formats_builder_new());
+ GTK4.gtk_tree_view_enable_model_drag_dest(handle, formats, 0);
+ GTK4.gdk_content_formats_unref(formats);
+ }
}
void createItem (TableColumn column, int index) {
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/ToolBar.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/ToolBar.java
index 4a9782a213c..fb63abb0903 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/ToolBar.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/ToolBar.java
@@ -309,8 +309,10 @@ public int getItemCount () {
int itemCount = 0;
if (GTK.GTK4) {
+ /* Must match _getItems(): a Menu's GtkPopover is also parented here
+ * as a native child and would otherwise count as a phantom item. */
for (long child = GTK4.gtk_widget_get_first_child(handle); child != 0; child = GTK4.gtk_widget_get_next_sibling(child)) {
- itemCount++;
+ if (display.getWidget(child) instanceof ToolItem) itemCount++;
}
} else {
long list = GTK3.gtk_container_get_children (handle);
@@ -348,8 +350,9 @@ ToolItem[] _getItems () {
ArrayList childrenList = new ArrayList<>();
for (long child = GTK4.gtk_widget_get_first_child(handle); child != 0; child = GTK4.gtk_widget_get_next_sibling(child)) {
Widget childWidget = display.getWidget(child);
- if (childWidget != null) {
- childrenList.add((ToolItem)childWidget);
+ /* A Menu's GtkPopover is also a native child here; skip non-ToolItems. */
+ if (childWidget instanceof ToolItem toolItem) {
+ childrenList.add(toolItem);
}
}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/ToolItem.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/ToolItem.java
index d02f04f91f5..a9cafd3febf 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/ToolItem.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/ToolItem.java
@@ -910,6 +910,22 @@ int gtk_gesture_press_event(long gesture, int n_press, double x, double y, long
sendSelectionEvent(SWT.Selection, e, false);
return GTK4.GTK_EVENT_SEQUENCE_CLAIMED;
}
+ /*
+ * GTK4: Handle right-click (button 3) to fire SWT.MenuDetect on the parent
+ * ToolBar, replicating the GTK3 gtk3_event_after behavior. The gesture
+ * coordinates are item-local; translate them to the ToolBar's coordinate
+ * space and then to screen coordinates before calling parent.showMenu().
+ */
+ if (n_press == 1 && GTK.gtk_gesture_single_get_current_button(gesture) == 3) {
+ double[] destX = new double[1];
+ double[] destY = new double[1];
+ boolean translated = GTK4.gtk_widget_translate_coordinates(handle, parent.handle, x, y, destX, destY);
+ int barX = translated ? (int) destX[0] : (int) x;
+ int barY = translated ? (int) destY[0] : (int) y;
+ Point screenPt = parent.toDisplay(barX, barY);
+ parent.showMenu(screenPt.x, screenPt.y);
+ return GTK4.GTK_EVENT_SEQUENCE_CLAIMED;
+ }
return GTK4.GTK_EVENT_SEQUENCE_NONE;
}
@@ -949,8 +965,8 @@ void hookEvents () {
OS.g_signal_connect(motionController, OS.enter, display.enterMotionProc, ENTER);
OS.g_signal_connect(motionController, OS.leave, display.leaveProc, LEAVE);
- //TODO: event-after
long clickController = GTK4.gtk_gesture_click_new();
+ GTK.gtk_gesture_single_set_button(clickController, 0);
GTK4.gtk_widget_add_controller(handle, clickController);
OS.g_signal_connect(clickController, OS.pressed, display.gesturePressReleaseProc, GESTURE_PRESSED);
} else {
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Tree.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Tree.java
index ec7d5ab9c09..7fd132ed41c 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Tree.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Tree.java
@@ -881,7 +881,23 @@ void createHandle (int index) {
GTK.gtk_tree_view_set_search_column (handle, -1);
}
- if (GTK.GTK4) bindArrowKeyBindings();
+ if (GTK.GTK4) {
+ bindArrowKeyBindings();
+ /*
+ * GTK renders the drop highlight requested through
+ * gtk_tree_view_set_drag_dest_row() from the private TreeViewDragInfo struct,
+ * but only gtk_tree_view_enable_model_drag_dest() ever allocates it and the
+ * snapshot code dereferences it without a NULL check. Driving the highlight
+ * ourselves, as setInsertMark() and TreeDropTargetEffect do, would therefore
+ * crash on the next repaint, so allocate the struct up front. The drop target
+ * GTK installs alongside it gets an empty format list and no actions, which
+ * makes it reject every drag so that GTK's own tree view drag handlers never
+ * compete with SWT's DropTarget.
+ */
+ long formats = GTK4.gdk_content_formats_builder_free_to_formats(GTK4.gdk_content_formats_builder_new());
+ GTK4.gtk_tree_view_enable_model_drag_dest(handle, formats, 0);
+ GTK4.gdk_content_formats_unref(formats);
+ }
}
/**
@@ -2977,7 +2993,15 @@ public void removeAll () {
long selection = GTK.gtk_tree_view_get_selection (handle);
OS.g_signal_handlers_block_matched (selection, OS.G_SIGNAL_MATCH_DATA, 0, 0, 0, 0, CHANGED);
- GTK.gtk_tree_store_clear (modelHandle);
+ // Disconnect the model from the view before clearing it.
+ // gtk_tree_store_clear fires cell-data / row-changed callbacks for every
+ // row it removes. Those callbacks re-enter SWT (cellDataProc -> checkData
+ // -> getParentItem -> gtk_tree_model_get_path) with iterators that are
+ // already being freed, causing a SIGSEGV. With no model attached the view
+ // has nothing to render, so no callbacks are fired during the clear.
+ GTK.gtk_tree_view_set_model (handle, 0);
+ GTK.gtk_tree_store_clear (modelHandle);
+ GTK.gtk_tree_view_set_model (handle, modelHandle);
OS.g_signal_handlers_unblock_matched (selection, OS.G_SIGNAL_MATCH_DATA, 0, 0, 0, 0, CHANGED);
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java
index e6b99131a7b..525cf88296d 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java
@@ -1250,11 +1250,15 @@ private Rectangle computeSourceRectangle(ImageHandle imageHandle, Rectangle full
* computed to pixels depending on the factor of the full image bounds to the
* actual OS handle size that will be used.
*/
- float scaleFactor = Math.min(1f * imageHandle.width() / fullImageBounds.width, 1f * imageHandle.height() / fullImageBounds.height);
- int closestZoomOfHandle = Math.round(scaleFactor * 100);
- Rectangle srcPixels = Win32DPIUtils.pointToPixel(drawable, src, closestZoomOfHandle);
-
- if (closestZoomOfHandle != 100) {
+ float scaleFactorX = (1f * imageHandle.width()) / fullImageBounds.width;
+ float scaleFactorY = (1f * imageHandle.height()) / fullImageBounds.height;
+ int srcXPixels = Math.round(scaleFactorX * src.x);
+ int srcWidthPixels = Math.round(scaleFactorX * (src.x + src.width)) - srcXPixels;
+ int srcYPixels = Math.round(scaleFactorY * src.y);
+ int srcHeightPixels = Math.round(scaleFactorY * (src.y + src.height)) - srcYPixels;
+ Rectangle srcPixels = new Rectangle(srcXPixels, srcYPixels, srcWidthPixels, srcHeightPixels);
+
+ if (Math.abs(scaleFactorX - 1f) >= 0.01f || Math.abs(scaleFactorY - 1f) >= 0.01f) {
/*
* This is a HACK! Due to rounding errors at fractional scale factors,
* the coordinates may be slightly off. The workaround is to restrict
@@ -1263,7 +1267,7 @@ private Rectangle computeSourceRectangle(ImageHandle imageHandle, Rectangle full
int errX = srcPixels.x + srcPixels.width - imageHandle.width();
int errY = srcPixels.y + srcPixels.height - imageHandle.height();
if (errX != 0 || errY != 0) {
- if (errX <= closestZoomOfHandle / 100 && errY <= closestZoomOfHandle / 100) {
+ if (errX <= Math.max(1, scaleFactorX) && errY <= Math.max(1, scaleFactorY)) {
srcPixels.intersect(new Rectangle(0, 0, imageHandle.width(), imageHandle.height()));
} else {
SWT.error (SWT.ERROR_INVALID_ARGUMENT);
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java
index 12a78ed8464..098efadcb9d 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java
@@ -18,6 +18,7 @@
import java.io.*;
import java.util.*;
+import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;
@@ -29,6 +30,7 @@
import org.eclipse.swt.internal.gdip.*;
import org.eclipse.swt.internal.image.*;
import org.eclipse.swt.internal.win32.*;
+import org.eclipse.swt.widgets.*;
/**
* Instances of this class are graphics which have been prepared
@@ -173,6 +175,15 @@ Set getAllZooms() {
return zoomLevelToImageHandle.keySet();
}
+ Integer getNearestAvailableZoom(int zoom) {
+ TreeSet availableZooms = new TreeSet<>(getAllZooms());
+ if (availableZooms.contains(zoom)) {
+ return zoom;
+ }
+ Integer higher = availableZooms.higher(zoom);
+ return higher != null ? higher : availableZooms.lower(zoom);
+ }
+
void destroyHandles(Predicate filter) {
zoomLevelToImageHandle.entrySet().removeIf(entry -> {
if (filter.test(entry.getKey())) {
@@ -201,28 +212,39 @@ public String toString() {
}
private class HandleAtSize {
+ record TemporaryHandleForZoom(DestroyableImageHandle handle, int zoom) {}
+
private InternalImageHandle handleContainer = null;
- private DestroyableImageHandle temporaryHandleContainer = null;
+ private TemporaryHandleForZoom temporaryHandleContainer = null;
private int requestedWidth = -1;
private int requestedHeight = -1;
public void destroy() {
- if (temporaryHandleContainer != null) {
- temporaryHandleContainer.destroy();
- temporaryHandleContainer = null;
+ TemporaryHandleForZoom previousHandle = reset();
+ if (previousHandle != null) {
+ previousHandle.handle().destroy();
}
+ }
+
+ private TemporaryHandleForZoom reset() {
+ TemporaryHandleForZoom previousHandle = temporaryHandleContainer;
+ temporaryHandleContainer = null;
handleContainer = null;
requestedWidth = -1;
requestedHeight = -1;
+ return previousHandle;
}
public ImageHandle refresh(int width, int height) {
if (!isReusable(width, height)) {
- destroy();
+ TemporaryHandleForZoom previousHandle = reset();
requestedWidth = width;
requestedHeight = height;
handleContainer = createHandleAtExactSize(width, height)
- .orElseGet(() -> getOrCreateImageHandleAtClosestSize(width, height));
+ .orElseGet(() -> getOrCreateImageHandleAtClosestSize(width, height, previousHandle));
+ if (previousHandle != null && previousHandle.handle() != handleContainer) {
+ previousHandle.handle().destroy();
+ }
}
return handleContainer;
}
@@ -238,23 +260,58 @@ private boolean isReusable(int width, int height) {
private Optional createHandleAtExactSize(int width, int height) {
Optional imageData = imageProvider.loadImageDataAtExactSize(width, height);
if (imageData.isPresent()) {
- temporaryHandleContainer = init(imageData.get(), -1);
- return Optional.of(temporaryHandleContainer);
+ temporaryHandleContainer = new TemporaryHandleForZoom(init(imageData.get(), -1), 0);
+ return Optional.of(temporaryHandleContainer.handle());
}
return Optional.empty();
}
- private InternalImageHandle getOrCreateImageHandleAtClosestSize(int widthHint, int heightHint) {
+ private InternalImageHandle getOrCreateImageHandleAtClosestSize(int widthHint, int heightHint, TemporaryHandleForZoom previousHandle) {
Rectangle bounds = getBounds(100);
int imageZoomForWidth = 100 * widthHint / bounds.width;
int imageZoomForHeight = 100 * heightHint / bounds.height;
int imageZoom = DPIUtil.getZoomForAutoscaleProperty(Math.max(imageZoomForWidth, imageZoomForHeight));
+ int nearestAvailableZoom = imageProvider.nearestAvailableZoom(imageZoom);
+ InternalImageHandle bestFittingHandle = getPersistentHandle(imageZoom, nearestAvailableZoom);
+ if (bestFittingHandle != null) {
+ return bestFittingHandle;
+ }
+ if (previousHandle != null && previousHandle.zoom() == nearestAvailableZoom) {
+ temporaryHandleContainer = previousHandle;
+ return previousHandle.handle();
+ }
+ ElementAtZoom imageData = imageProvider.loadImageData(imageZoom);
+ temporaryHandleContainer = new TemporaryHandleForZoom(init(imageData.element(), -1), imageData.zoom());
+ return temporaryHandleContainer.handle();
+ }
+
+ private InternalImageHandle getPersistentHandle(int imageZoom, int nearestAvailableZoom) {
InternalImageHandle bestFittingHandle = imageHandleManager.get(imageZoom);
- if (bestFittingHandle == null) {
- ImageData bestFittingImageData = imageProvider.loadImageData(imageZoom).element();
- bestFittingHandle = temporaryHandleContainer = init(bestFittingImageData, -1);
+ if (bestFittingHandle != null) {
+ return bestFittingHandle;
+ }
+ if (getShellZooms().contains(imageZoom)) {
+ return getHandleInternal(imageZoom, imageZoom);
+ }
+ bestFittingHandle = imageHandleManager.get(nearestAvailableZoom);
+ if (bestFittingHandle != null) {
+ return bestFittingHandle;
+ }
+ if (nearestAvailableZoom == 100) {
+ return getHandleInternal(100, 100);
}
- return bestFittingHandle;
+ return null;
+ }
+
+ private Set getShellZooms() {
+ if (getDevice() instanceof Display display) {
+ try {
+ return Arrays.stream(display.getShells()).map(Shell::getZoom).collect(Collectors.toSet());
+ } catch (SWTException e) {
+ return Collections.emptySet();
+ }
+ }
+ return Collections.emptySet();
}
}
@@ -957,6 +1014,10 @@ public static long win32_getHandle (Image image, int zoom) {
}
ImageHandle getHandle (int targetZoom, int nativeZoom) {
+ return getHandleInternal(targetZoom, nativeZoom);
+}
+
+InternalImageHandle getHandleInternal (int targetZoom, int nativeZoom) {
if (isDisposed()) {
return null;
}
@@ -2090,6 +2151,8 @@ protected boolean isPersistentImageHandleRequriedForImageData() {
return false;
}
+ abstract int nearestAvailableZoom(int zoom);
+
/**
* Returns image data at the best-fitting available zoom for the given zoom.
* The returned data will have a potential gray/disable style applied.
@@ -2101,8 +2164,7 @@ protected boolean isPersistentImageHandleRequriedForImageData() {
abstract AbstractImageProviderWrapper createCopy(Image image);
ElementAtZoom getClosestAvailableImageData(int zoom) {
- TreeSet availableZooms = new TreeSet<>(imageHandleManager.getAllZooms());
- int closestZoom = Optional.ofNullable(availableZooms.higher(zoom)).orElse(availableZooms.lower(zoom));
+ int closestZoom = imageHandleManager.getNearestAvailableZoom(zoom);
ImageData imageData = imageHandleManager.get(closestZoom).getImageData();
return new ElementAtZoom<>(imageData, closestZoom);
}
@@ -2179,6 +2241,11 @@ protected DestroyableImageHandle newImageHandle(ZoomContext zoomContext) {
ImageData resizedData = newImageData (zoomContext.targetZoom());
return newImageHandle(resizedData, zoomContext);
}
+
+ @Override
+ int nearestAvailableZoom(int zoom) {
+ return zoomForHandle;
+ }
}
private abstract class ImageFromImageDataProviderWrapper extends AbstractImageProviderWrapper {
@@ -2249,6 +2316,11 @@ protected ElementAtZoom loadImageData(int zoom) {
AbstractImageProviderWrapper createCopy(Image image) {
return image.new PlainImageDataProviderWrapper(this.imageDataAtBaseZoom);
}
+
+ @Override
+ int nearestAvailableZoom(int zoom) {
+ return baseZoom;
+ }
}
private class MaskedImageDataProviderWrapper extends ImageFromImageDataProviderWrapper {
@@ -2281,6 +2353,11 @@ protected ElementAtZoom loadImageData(int zoom) {
AbstractImageProviderWrapper createCopy(Image image) {
return image.new MaskedImageDataProviderWrapper(this.srcAt100, this.maskAt100);
}
+
+ @Override
+ int nearestAvailableZoom(int zoom) {
+ return 100;
+ }
}
private class ImageDataLoaderStreamProviderWrapper extends ImageFromImageDataProviderWrapper {
@@ -2325,6 +2402,14 @@ protected Optional loadImageDataAtExactSize(int targetWidth, int targ
}
return Optional.empty();
}
+
+ @Override
+ int nearestAvailableZoom(int zoom) {
+ if (ImageDataLoader.isDynamicallySizable(new ByteArrayInputStream(this.inputStreamData))) {
+ return zoom;
+ }
+ return FileFormat.DEFAULT_ZOOM;
+ }
}
private class PlainImageProviderWrapper extends AbstractImageProviderWrapper {
@@ -2388,6 +2473,14 @@ protected ElementAtZoom loadImageData(int zoom) {
return getClosestAvailableImageData(zoom);
}
+ @Override
+ int nearestAvailableZoom(int zoom) {
+ if (imageHandleManager.isEmpty()) {
+ return 100;
+ }
+ return imageHandleManager.getNearestAvailableZoom(zoom);
+ }
+
@Override
protected DestroyableImageHandle newImageHandle(ZoomContext zoomContext) {
int targetZoom = zoomContext.targetZoom();
@@ -2533,6 +2626,7 @@ private class ImageFileNameProviderWrapper extends BaseImageProviderWrapper loadImageData(int zoom) {
ElementAtZoom fileForZoom = DPIUtil.validateAndGetImagePathAtZoom(provider, zoom);
@@ -2564,6 +2658,14 @@ protected ElementAtZoom loadImageData(int zoom) {
return adaptImageDataIfDisabledOrGray(imageDataAtZoom);
}
+ @Override
+ int nearestAvailableZoom(int zoom) {
+ if (provider instanceof ImageDataAtSizeProvider) {
+ return zoom;
+ }
+ return DPIUtil.validateAndGetImagePathAtZoom(provider, zoom).zoom();
+ }
+
@Override
public int hashCode() {
return Objects.hash(provider, styleFlag);
@@ -2807,6 +2909,14 @@ protected Optional loadImageDataAtExactSize(int targetWidth, int targ
}
return Optional.empty();
}
+
+ @Override
+ int nearestAvailableZoom(int zoom) {
+ if (provider instanceof ImageDataAtSizeProvider) {
+ return zoom;
+ }
+ return DPIUtil.validateAndGetImageDataAtZoom (provider, zoom).zoom();
+ }
}
private class ImageGcDrawerWrapper extends DynamicImageProviderWrapper {
@@ -2848,6 +2958,11 @@ protected ElementAtZoom loadImageData(int zoom) {
return new ElementAtZoom<>(loadImageData(new ZoomContext(zoom)), zoom);
}
+ @Override
+ int nearestAvailableZoom(int zoom) {
+ return zoom;
+ }
+
private ImageData loadImageData(ZoomContext zoomContext) {
currentZoom = zoomContext;
int targetZoom = zoomContext.targetZoom();
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/ImageList.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/ImageList.java
index 05658442854..c32726acb1b 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/ImageList.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/ImageList.java
@@ -394,14 +394,6 @@ public void put (int index, Image image) {
images [index] = image;
}
-public void remove (int index) {
- int count = OS.ImageList_GetImageCount (handle);
- if (!(0 <= index && index < count)) return;
- zoomToHandle.values().forEach(handle -> OS.ImageList_Remove (handle, index));
- System.arraycopy (images, index + 1, images, index, --count - index);
- images [index] = null;
-}
-
public int removeRef() {
return --refCount;
}
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Table.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Table.java
index 75b22c3c684..a9ade374d1d 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Table.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Table.java
@@ -7161,9 +7161,11 @@ private LRESULT positionTooltip(NMHDR hdr, long lParam) {
long hwndToolTip = OS.SendMessage(handle, OS.LVM_GETTOOLTIPS, 0, 0);
int flags = OS.SWP_NOACTIVATE | OS.SWP_NOZORDER;
Rectangle adjustedTooltipBounds = getDisplay().fitRectangleBoundsIntoMonitorWithCursor(toolRect);
- OS.SetWindowPos(hwndToolTip, 0, adjustedTooltipBounds.x, adjustedTooltipBounds.y,
- adjustedTooltipBounds.width, adjustedTooltipBounds.height, flags);
- result = LRESULT.ONE;
+ if (adjustedTooltipBounds != null) {
+ OS.SetWindowPos(hwndToolTip, 0, adjustedTooltipBounds.x, adjustedTooltipBounds.y,
+ adjustedTooltipBounds.width, adjustedTooltipBounds.height, flags);
+ result = LRESULT.ONE;
+ }
} else if (isCustomToolTip()) {
RECT itemRect = getItemBounds(pinfo, item, hDC);
NMTTDISPINFO lpnmtdi = new NMTTDISPINFO();
diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/ToolBar.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/ToolBar.java
index 6e523eef5b3..68e839fcefe 100644
--- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/ToolBar.java
+++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/ToolBar.java
@@ -1712,8 +1712,6 @@ record ToolItemData(ToolItem toolItem, TBBUTTON button) {
// Remove and re-add all button the let Windows resize the tool bar
Stack buttondata = new Stack<>();
for (int i = itemCount - 1; i >= 0; i--) {
- TBBUTTON lpButton = new TBBUTTON ();
- OS.SendMessage (handle, OS.TB_GETBUTTON, i, lpButton);
ToolItem item = toolItems[i];
if ((item.style & SWT.SEPARATOR) != 0 && item.getControl() != null) {
// Take note of widths of separators with control, so they can be resized
@@ -1721,6 +1719,13 @@ record ToolItemData(ToolItem toolItem, TBBUTTON button) {
seperatorWidth[i] = item.getWidth();
}
item.notifyListeners(SWT.ZoomChanged, event);
+ // Capture the button data AFTER handling the zoom change. The zoom refresh
+ // may update the item's image-list slot (iBitmap), so capturing the button
+ // beforehand could re-add it with a stale image index, resulting in the
+ // wrong (or a blank) icon being shown. Reading the button here ensures the
+ // current, post-refresh image index is preserved.
+ TBBUTTON lpButton = new TBBUTTON ();
+ OS.SendMessage (handle, OS.TB_GETBUTTON, i, lpButton);
buttondata.push(new ToolItemData(item, lpButton));
OS.SendMessage(handle, OS.TB_DELETEBUTTON, i, 0);
}
@@ -1738,6 +1743,11 @@ record ToolItemData(ToolItem toolItem, TBBUTTON button) {
}
}
}
+ // Refresh the image lists so the image list for the correct zoom is used
+ setImageList(getImageList());
+ setDisabledImageList(getDisabledImageList());
+ setHotImageList(getHotImageList());
+ boolean toolBarEnabled = getEnabled();
for (int i = 0; i < itemCount; i++) {
ToolItem item = toolItems[i];
// If the separator is used with a control, we must reset the size to the cached value,
@@ -1745,12 +1755,9 @@ record ToolItemData(ToolItem toolItem, TBBUTTON button) {
if ((item.style & SWT.SEPARATOR) != 0 && item.getControl() != null) {
item.setWidth(seperatorWidth[i]);
}
+ // Make sure the tool item is resized with the new image and font size
+ toolItems[i].updateImages(toolItems[i].getEnabled() && toolBarEnabled);
}
-
- // Refresh the image lists so the image list for the correct zoom is used
- setImageList(getImageList());
- setDisabledImageList(getDisabledImageList());
- setHotImageList(getHotImageList());
OS.SendMessage(handle, OS.TB_AUTOSIZE, 0, 0);
clearSizeCache(true);
}
diff --git a/examples/org.eclipse.swt.examples/META-INF/MANIFEST.MF b/examples/org.eclipse.swt.examples/META-INF/MANIFEST.MF
index b7b9968fa3f..c078217c52e 100644
--- a/examples/org.eclipse.swt.examples/META-INF/MANIFEST.MF
+++ b/examples/org.eclipse.swt.examples/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %plugin.SWTStandaloneExampleSet.name
Bundle-SymbolicName: org.eclipse.swt.examples; singleton:=true
-Bundle-Version: 3.109.100.qualifier
+Bundle-Version: 3.109.200.qualifier
Bundle-Vendor: %providerName
Bundle-Localization: plugin
Bundle-RequiredExecutionEnvironment: JavaSE-21
diff --git a/examples/org.eclipse.swt.examples/src/examples_control.properties b/examples/org.eclipse.swt.examples/src/examples_control.properties
index 54bc35f9c0d..ad7f2ef4442 100644
--- a/examples/org.eclipse.swt.examples/src/examples_control.properties
+++ b/examples/org.eclipse.swt.examples/src/examples_control.properties
@@ -232,6 +232,7 @@ Set_Min_Visible = Minimize
Set_Max_Visible = Maximize
Set_Unselected_Close_Visible = Close on Unselected Tabs
Set_Unselected_Image_Visible = Image on Unselected Tabs
+Set_MRU_Active = Activate MRU
Selection_Foreground_Color = Selection Foreground Color
Selection_Background_Color = Selection Background Color
Item_Foreground_Color = Item Foreground Color
diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/controlexample/CTabFolderTab.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/controlexample/CTabFolderTab.java
index 728f988c7d4..74b7450e906 100644
--- a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/controlexample/CTabFolderTab.java
+++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/controlexample/CTabFolderTab.java
@@ -63,7 +63,7 @@ class CTabFolderTab extends Tab {
/* Other widgets added to the "Other" group */
Button singleTabButton, imageButton, showMinButton, showMaxButton,
- topRightButton, unselectedCloseButton, unselectedImageButton;
+ topRightButton, unselectedCloseButton, unselectedImageButton, activateMRUButton;
ToolBar topRightControl;
@@ -233,6 +233,12 @@ void createOtherGroup () {
unselectedCloseButton.setText (ControlExample.getResourceString("Set_Unselected_Close_Visible"));
unselectedCloseButton.setSelection(true);
unselectedCloseButton.addSelectionListener (widgetSelectedAdapter(event -> setUnselectedCloseVisible()));
+
+ activateMRUButton = new Button (otherGroup, SWT.CHECK);
+ activateMRUButton.setText (ControlExample.getResourceString("Set_MRU_Active"));
+ activateMRUButton.setSelection(false);
+ activateMRUButton.addSelectionListener (widgetSelectedAdapter(event -> setMRUActive()));
+
}
/**
@@ -412,6 +418,7 @@ void setExampleWidgetState () {
setImages();
setMinimizeVisible();
setMaximizeVisible();
+ setMRUActive();
setUnselectedCloseVisible();
setUnselectedImageVisible();
setSelectionBackground ();
@@ -458,6 +465,13 @@ void setMaximizeVisible () {
tabFolder1.setMaximizeVisible(showMaxButton.getSelection ());
setExampleWidgetSize();
}
+ /**
+ * Activates/deactivates the MRU setting
+ */
+ void setMRUActive () {
+ tabFolder1.setMRUVisible(activateMRUButton.getSelection ());
+ setExampleWidgetSize();
+ }
/**
* Sets the top right control to a toolbar
*/
diff --git a/features/org.eclipse.swt.tools.feature/feature.xml b/features/org.eclipse.swt.tools.feature/feature.xml
index e931cf31d75..bbd29d9849f 100644
--- a/features/org.eclipse.swt.tools.feature/feature.xml
+++ b/features/org.eclipse.swt.tools.feature/feature.xml
@@ -2,7 +2,7 @@
diff --git a/tests/org.eclipse.swt.tests.gtk/ManualTests/org/eclipse/swt/tests/gtk/snippets/Issue3329_TreeCrashBug.java b/tests/org.eclipse.swt.tests.gtk/ManualTests/org/eclipse/swt/tests/gtk/snippets/Issue3329_TreeCrashBug.java
new file mode 100644
index 00000000000..4668816fec8
--- /dev/null
+++ b/tests/org.eclipse.swt.tests.gtk/ManualTests/org/eclipse/swt/tests/gtk/snippets/Issue3329_TreeCrashBug.java
@@ -0,0 +1,155 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Andrey Loskutov and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Andrey Loskutov - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.swt.tests.gtk.snippets;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+
+/**
+ * Manual reproducer for the Tree.removeAll() crash on Linux GTK with
+ * SWT.VIRTUAL trees.
+ *
+ * STEPS TO REPRODUCE: 1. Run this snippet. 2. Expand any root item so its
+ * children become visible (this creates Java TreeItem objects with GTK iters
+ * backed by live GNodes). 3. Click GO.
+ *
+ * EXPECTED: Tree content refreshes cleanly. ACTUAL: SIGSEGV inside
+ * gtk_tree_model_get_path (libgtk-3.so.0).
+ *
+ * ROOT CAUSE (mirrors the original JFace crash test provided in the ticket):
+ *
+ * Tree.removeAll() calls gtk_tree_store_clear(). GTK fires cellDataProc
+ * synchronously for rows that become visible as the cursor moves during
+ * deletion. For an UNCACHED root item (never rendered, so no ID column value in
+ * the model), Tree._getItem() calls getId() which calls gtk_tree_store_set() to
+ * assign the ID. That gtk_tree_store_set fires a synchronous "row-changed"
+ * signal which re-enters cellDataProc a second time for the same row. The inner
+ * cellDataProc finds the item still uncached and calls checkData() ->
+ * sendEvent(SWT.SetData).
+ *
+ * At this point gtk_tree_store_clear has already removed (and freed the GNodes
+ * of) the earlier root items and their children. The Java TreeItem objects for
+ * those freed rows are still alive (release() is called after
+ * gtk_tree_store_clear returns), but their handle field contains a GtkTreeIter
+ * whose user_data pointer is now dangling.
+ *
+ * JFace's SetData handler calls viewer.replace() which calls
+ * internalFindItems() -> getTreePathFromItem() -> getParentItem() on every
+ * existing TreeItem widget. Calling getParentItem() on any item whose GTK row
+ * has been freed passes the stale GtkTreeIter to gtk_tree_model_get_path(),
+ * which dereferences the freed GNode -> SIGSEGV at si_addr=0x17 (null-like
+ * offset into freed memory).
+ *
+ * This reproducer replicates that by explicitly calling getParentItem() on the
+ * previously-collected child items (same as getTreePathFromItem does) inside
+ * the SetData listener that fires during gtk_tree_store_clear.
+ */
+public class Issue3329_TreeCrashBug {
+
+ private static final int ROOT_COUNT = 40;
+ private static final int CHILD_COUNT = 60;
+
+ public static void main(String[] args) {
+ var display = new Display();
+ var shell = new Shell(display);
+ shell.setLayout(new GridLayout());
+
+ var button = new Button(shell, SWT.PUSH);
+ button.setText("GO"); //$NON-NLS-1$
+
+ var tree = new Tree(shell, SWT.VIRTUAL | SWT.BORDER);
+ tree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+ // Collect every child TreeItem as it is rendered during v1 population.
+ // These objects hold GTK iters (GtkTreeIter.user_data -> GNode).
+ // After removeAll() frees those GNodes the handles become dangling.
+ List prevChildren = new ArrayList<>();
+
+ tree.addListener(SWT.SetData, e -> {
+ TreeItem item = (TreeItem) e.item;
+ TreeItem parent = item.getParentItem();
+ if (parent == null) {
+ item.setText("v1_" + e.index); //$NON-NLS-1$
+ item.setItemCount(CHILD_COUNT);
+ } else {
+ item.setText(parent.getText() + "_" + e.index); //$NON-NLS-1$
+ prevChildren.add(item); // remember this child — its GNode will be freed on removeAll()
+ }
+ });
+
+ tree.setItemCount(ROOT_COUNT);
+
+ // GO button: replace the tree content with a new version.
+ // Crashes if at least one root was expanded (prevChildren is non-empty)
+ // AND there are uncached roots below the visible viewport.
+ button.addListener(SWT.Selection, e -> {
+ for (var l : tree.getListeners(SWT.SetData)) {
+ tree.removeListener(SWT.SetData, l);
+ }
+
+ // New SetData listener registered BEFORE removeAll().
+ // It fires re-entrantly from inside gtk_tree_store_clear (via the
+ // cellDataProc -> getId -> gtk_tree_store_set -> row-changed -> cellDataProc
+ // -> checkData -> SetData chain described in the root-cause comment above).
+ //
+ // At the time it fires, the earlier roots and their children have
+ // already been removed; prevChildren[i].handle contains a freed GNode.
+ // Calling getParentItem() on those items passes the stale GtkTreeIter
+ // to gtk_tree_model_get_path() -> SIGSEGV.
+ //
+ // This mirrors AbstractTreeViewer.getTreePathFromItem() which walks all
+ // existing TreeItem widgets via getParentItem() inside internalFindItems().
+ tree.addListener(SWT.SetData, ev -> {
+ for (TreeItem child : prevChildren) {
+ // child is NOT disposed yet (release() happens after gtk_tree_store_clear),
+ // but child.handle is a stale GtkTreeIter with a freed GNode pointer.
+ if (!child.isDisposed()) {
+ child.getParentItem(); // <-- SIGSEGV on production GTK3
+ // G_DISABLE_CHECKS)
+ }
+ }
+ TreeItem item = (TreeItem) ev.item;
+ TreeItem parent = item.getParentItem();
+ if (parent == null) {
+ item.setText("v2_" + ev.index); //$NON-NLS-1$
+ item.setItemCount(CHILD_COUNT);
+ } else {
+ item.setText(parent.getText() + "_" + ev.index); //$NON-NLS-1$
+ }
+ });
+
+ tree.removeAll(); // triggers the crash
+ tree.setItemCount(ROOT_COUNT);
+ });
+
+ shell.setSize(300, 250); // small window -> more uncached (out-of-viewport) roots
+ shell.open();
+
+ while (!shell.isDisposed()) {
+ if (!display.readAndDispatch()) {
+ display.sleep();
+ }
+ }
+ display.dispose();
+ }
+}
\ No newline at end of file
diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java
index ffef5ae21e7..651f6d0fd2d 100644
--- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java
+++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java
@@ -3045,6 +3045,79 @@ public void test_BrowserFunction_availableOnLoad_concurrentInstances_issue20() {
assertTrue(browser2FuncAvailable.get(), "BrowserFunction for second browser missing when page load completed");
}
+/**
+ * Regression test: a disposed BrowserFunction must no longer be available (re-injected) after a
+ * subsequent navigation. This verifies that deregistration removes the persistent document-created
+ * script (whose ID is captured asynchronously on the Edge backend).
+ */
+@Test
+public void test_BrowserFunction_disposedFunctionRemovedAfterNavigation() {
+ BrowserFunction function = new BrowserFunction(browser, "disposableFunc") {
+ @Override
+ public Object function(Object[] arguments) {
+ return "alive";
+ }
+ };
+
+ AtomicBoolean firstPageLoaded = new AtomicBoolean(false);
+ ProgressListener firstPageListener = completedAdapter(e -> firstPageLoaded.set(true));
+ browser.addProgressListener(firstPageListener);
+ browser.setText("first page");
+ shell.open();
+ assertTrue(waitForPassCondition(firstPageLoaded::get), "First page did not load");
+ // The function is registered and usable now (this also ensures its registration has settled).
+ assertEquals("alive", browser.evaluate("return disposableFunc();"));
+ browser.removeProgressListener(firstPageListener);
+
+ // Dispose the function, then navigate: it must be gone on the new page.
+ function.dispose();
+ AtomicBoolean secondPageLoaded = new AtomicBoolean(false);
+ browser.addProgressListener(completedAdapter(e -> secondPageLoaded.set(true)));
+ browser.setText("second page");
+ assertTrue(waitForPassCondition(secondPageLoaded::get), "Second page did not load");
+
+ Object stillDefined = browser.evaluate("return typeof disposableFunc === 'function';");
+ assertEquals(Boolean.FALSE, stillDefined,
+ "A disposed BrowserFunction must not be re-injected on a subsequently loaded page");
+}
+
+/**
+ * Regression test: redefining a BrowserFunction with the same name (which deregisters the previous
+ * one and registers the new one) must take effect and survive navigations - the newest definition
+ * wins and the previous one is not resurrected.
+ */
+@Test
+public void test_BrowserFunction_redefineSameNameSurvivesNavigation() {
+ new BrowserFunction(browser, "f") {
+ @Override
+ public Object function(Object[] arguments) {
+ return "v1";
+ }
+ };
+ new BrowserFunction(browser, "f") {
+ @Override
+ public Object function(Object[] arguments) {
+ return "v2";
+ }
+ };
+
+ AtomicBoolean firstPageLoaded = new AtomicBoolean(false);
+ ProgressListener firstPageListener = completedAdapter(e -> firstPageLoaded.set(true));
+ browser.addProgressListener(firstPageListener);
+ browser.setText("first page");
+ shell.open();
+ assertTrue(waitForPassCondition(firstPageLoaded::get), "First page did not load");
+ assertEquals("v2", browser.evaluate("return f();"), "The most recent definition of 'f' must win");
+ browser.removeProgressListener(firstPageListener);
+
+ AtomicBoolean secondPageLoaded = new AtomicBoolean(false);
+ browser.addProgressListener(completedAdapter(e -> secondPageLoaded.set(true)));
+ browser.setText("second page");
+ assertTrue(waitForPassCondition(secondPageLoaded::get), "Second page did not load");
+ assertEquals("v2", browser.evaluate("return f();"),
+ "The redefined BrowserFunction must survive navigation and the previous definition must not be resurrected");
+}
+
@Test
@Disabled("Too fragile on CI, Display.getDefault().post(event) does not work reliably")
public void test_TabTraversalOutOfBrowser() {
diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_CTabFolder.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_CTabFolder.java
index ac61c9e2984..66ca7167ca3 100644
--- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_CTabFolder.java
+++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_CTabFolder.java
@@ -45,7 +45,9 @@
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.internal.DPIUtil;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
@@ -54,6 +56,7 @@
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
@@ -69,6 +72,7 @@
*
* @see org.eclipse.swt.custom.CTabFolder
*/
+@SuppressWarnings("restriction")
public class Test_org_eclipse_swt_custom_CTabFolder extends Test_org_eclipse_swt_widgets_Composite {
@Override
@@ -522,6 +526,51 @@ public void test_topRightWrapOverflow() {
+ "topRight.y=" + topRightBounds.y + " tab.bottom=" + (tabBounds.y + tabBounds.height));
}
+/**
+ * A tab control is rescaled after the folder itself, so the folder must recompute
+ * its tab height once a tab control reports a zoom change. Test for issue 3456.
+ */
+@Test
+public void test_tabHeightRecomputedOnTabControlZoomChange() {
+ makeCleanEnvironment();
+ shell.setSize(800, 400);
+
+ CTabItem item = new CTabItem(ctabFolder, SWT.NONE);
+ item.setText("Tab 1");
+ ctabFolder.setSelection(0);
+
+ int topRightHeight = 20;
+ Composite topRight = new Composite(ctabFolder, SWT.NONE);
+ FixedSizeLayout topRightLayout = new FixedSizeLayout(60, topRightHeight);
+ topRight.setLayout(topRightLayout);
+ ctabFolder.setTopRight(topRight, SWT.RIGHT | SWT.WRAP);
+
+ SwtTestUtil.openShell(shell);
+ processEvents();
+
+ int defaultTabHeight = ctabFolder.getTabHeight();
+
+ // A taller tab control makes the folder grow its tab height
+ topRightLayout.height = defaultTabHeight * 3;
+ ctabFolder.setTabHeight(SWT.DEFAULT);
+ processEvents();
+ int grownTabHeight = ctabFolder.getTabHeight();
+ assertTrue(grownTabHeight > defaultTabHeight, "tab height should grow with the tab control");
+
+ // Shrinking the tab control alone leaves the tab height stale, which is the state
+ // the folder ends up in after measuring a control that was not yet rescaled
+ topRightLayout.height = topRightHeight;
+ assertEquals(grownTabHeight, ctabFolder.getTabHeight(), "precondition: tab height is stale");
+
+ Event zoomChanged = new Event();
+ zoomChanged.detail = DPIUtil.getDeviceZoom();
+ topRight.notifyListeners(SWT.ZoomChanged, zoomChanged);
+ processEvents();
+
+ assertEquals(defaultTabHeight, ctabFolder.getTabHeight(),
+ "tab height should be recomputed after a zoom change of a tab control");
+}
+
/**
* Min/max and chevron icon can appear below tab row.
* Test for bug 499215, 533582.
@@ -1117,4 +1166,24 @@ public void test_moveItem_errorCases() {
"out-of-range to index must be rejected");
}
+/** Layout with a preferred size the test can change at will. */
+private static final class FixedSizeLayout extends Layout {
+ int width;
+ int height;
+
+ FixedSizeLayout(int width, int height) {
+ this.width = width;
+ this.height = height;
+ }
+
+ @Override
+ protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) {
+ return new Point(wHint == SWT.DEFAULT ? width : wHint, hHint == SWT.DEFAULT ? height : hHint);
+ }
+
+ @Override
+ protected void layout(Composite composite, boolean flushCache) {
+ }
+}
+
}
diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_graphics_GC.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_graphics_GC.java
index 29b2292b5ab..5d7449c8e36 100644
--- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_graphics_GC.java
+++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_graphics_GC.java
@@ -382,6 +382,20 @@ public void test_drawImageLorg_eclipse_swt_graphics_ImageIIIIIIII() {
images.dispose();
}
+/**
+ * See https://github.com/eclipse-platform/eclipse.platform.swt/issues/3442
+ */
+@Test
+public void test_drawImage_emptyImage() {
+ Image emptyImage = new Image(display, IMAGE_SIZE, IMAGE_SIZE);
+ try {
+ gc.drawImage(emptyImage, 0, 0, IMAGE_SIZE, IMAGE_SIZE, 0, 0, IMAGE_SIZE / 3, IMAGE_SIZE / 3);
+ ImageDataTestHelper.assertImageDataEqual(image.getImageData(), emptyImage.getImageData(), image.getImageData());
+ } finally {
+ emptyImage.dispose();
+ }
+}
+
@Test
public void test_drawImageLorg_eclipse_swt_graphics_ImageIIII() {
diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_MenuItem.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_MenuItem.java
index 1db0b857eca..5ea3cbc497d 100644
--- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_MenuItem.java
+++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_MenuItem.java
@@ -195,6 +195,19 @@ public void test_setEnabledZ() {
assertFalse(menuItem.getEnabled());
}
+@Test
+public void test_setEnabledZ_cascade() {
+ MenuItem cascadeItem = new MenuItem(menu, SWT.CASCADE);
+ Menu subMenu = new Menu(shell, SWT.DROP_DOWN);
+ cascadeItem.setMenu(subMenu);
+ assertTrue(cascadeItem.getEnabled());
+ cascadeItem.setEnabled(false);
+ assertFalse(cascadeItem.getEnabled());
+ cascadeItem.setEnabled(true);
+ assertTrue(cascadeItem.getEnabled());
+ cascadeItem.dispose();
+}
+
@Tag("gtk4-todo")
@Override
@Test