From fead2fd6820cc72b81034fb2eb5ef62eab8c5535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=8A?= =?UTF-8?q?=D1=80=20=D0=9A=D1=83=D1=80=D1=82=D0=B0=D0=BA=D0=BE=D0=B2?= Date: Mon, 3 Aug 2026 17:41:55 +0300 Subject: [PATCH] [Gtk4] Support MenuItem images on GTK4 GtkModelButton (GTK4's native menu row) never shows both an icon and text, so menu items with images rendered without their icon. Work around this by embedding a custom icon+label(+accelerator) GtkButton into the popover via gtk_popover_menu_add_child() for PUSH and CHECK items. The custom row uses the "modelbutton" CSS node so the theme's menu row rules apply to it, and its icons and the native rows' share one column via a GtkSizeGroup. CHECK rows carry a GtkCheckButton bound to the item's action as indicator; CASCADE and RADIO items keep the native GtkModelButton. Selection is kept in sync by motion and focus controllers on every popover: the highlight is re-derived from each row's prelight so it survives a submenu holding the pointer grab, stray focus selections are swept, and hovering a sibling closes an open submenu after a short delay. Themed workbenches (e.g. the e4 dark theme) override the row's selected background, so the highlight is painted by a display-wide provider keyed on a class instead. Injection is self-healing: GTK's "custom" ids are single-use, so a detached or rebuilt row is re-injected under a fresh id. Menubar drop-downs are located by their menu model rather than positionally, avoiding leaks and lost SWT.Show events during item changes. Contributes to #2511 Assisted-by: Anthropic Claude Code (claude-sonnet-5) Assisted-by: Anthropic Claude Code (claude-fable-5-1) --- .../Eclipse SWT PI/gtk/library/gtk4.c | 64 +++ .../Eclipse SWT PI/gtk/library/gtk4_stats.h | 5 + .../Eclipse SWT PI/gtk/library/os.c | 113 +++- .../Eclipse SWT PI/gtk/library/os_stats.h | 9 +- .../gtk/org/eclipse/swt/internal/gtk/GTK.java | 24 + .../gtk/org/eclipse/swt/internal/gtk/OS.java | 13 + .../org/eclipse/swt/internal/gtk4/GTK4.java | 28 + .../gtk/org/eclipse/swt/widgets/Display.java | 29 + .../gtk/org/eclipse/swt/widgets/Menu.java | 258 ++++++++- .../gtk/org/eclipse/swt/widgets/MenuItem.java | 496 ++++++++++++++++-- ...Test_org_eclipse_swt_widgets_MenuItem.java | 2 - 11 files changed, 979 insertions(+), 62 deletions(-) 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 4f41025f6c9..1b1a6f1f684 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 @@ -806,6 +806,20 @@ JNIEXPORT void JNICALL GTK4_NATIVE(gsk_1render_1node_1unref) } #endif +#ifndef NO_gtk_1actionable_1set_1action_1name +JNIEXPORT void JNICALL GTK4_NATIVE(gtk_1actionable_1set_1action_1name) + (JNIEnv *env, jclass that, jlong arg0, jbyteArray arg1) +{ + jbyte *lparg1=NULL; + GTK4_NATIVE_ENTER(env, that, gtk_1actionable_1set_1action_1name_FUNC); + if (arg1) if ((lparg1 = (*env)->GetByteArrayElements(env, arg1, NULL)) == NULL) goto fail; + gtk_actionable_set_action_name((GtkActionable *)arg0, (const char *)lparg1); +fail: + if (arg1 && lparg1) (*env)->ReleaseByteArrayElements(env, arg1, lparg1, 0); + GTK4_NATIVE_EXIT(env, that, gtk_1actionable_1set_1action_1name_FUNC); +} +#endif + #ifndef NO_gtk_1box_1append JNIEXPORT void JNICALL GTK4_NATIVE(gtk_1box_1append) (JNIEnv *env, jclass that, jlong arg0, jlong arg1) @@ -2270,6 +2284,22 @@ JNIEXPORT void JNICALL GTK4_NATIVE(gtk_1picture_1set_1paintable) } #endif +#ifndef NO_gtk_1popover_1menu_1add_1child +JNIEXPORT jboolean JNICALL GTK4_NATIVE(gtk_1popover_1menu_1add_1child) + (JNIEnv *env, jclass that, jlong arg0, jlong arg1, jbyteArray arg2) +{ + jbyte *lparg2=NULL; + jboolean rc = 0; + GTK4_NATIVE_ENTER(env, that, gtk_1popover_1menu_1add_1child_FUNC); + if (arg2) if ((lparg2 = (*env)->GetByteArrayElements(env, arg2, NULL)) == NULL) goto fail; + rc = (jboolean)gtk_popover_menu_add_child((GtkPopoverMenu *)arg0, (GtkWidget *)arg1, (const char *)lparg2); +fail: + if (arg2 && lparg2) (*env)->ReleaseByteArrayElements(env, arg2, lparg2, 0); + GTK4_NATIVE_EXIT(env, that, gtk_1popover_1menu_1add_1child_FUNC); + return rc; +} +#endif + #ifndef NO_gtk_1popover_1menu_1bar_1new_1from_1model JNIEXPORT jlong JNICALL GTK4_NATIVE(gtk_1popover_1menu_1bar_1new_1from_1model) (JNIEnv *env, jclass that, jlong arg0) @@ -2306,6 +2336,18 @@ JNIEXPORT jlong JNICALL GTK4_NATIVE(gtk_1popover_1menu_1new_1from_1model_1full) } #endif +#ifndef NO_gtk_1popover_1menu_1remove_1child +JNIEXPORT jboolean JNICALL GTK4_NATIVE(gtk_1popover_1menu_1remove_1child) + (JNIEnv *env, jclass that, jlong arg0, jlong arg1) +{ + jboolean rc = 0; + GTK4_NATIVE_ENTER(env, that, gtk_1popover_1menu_1remove_1child_FUNC); + rc = (jboolean)gtk_popover_menu_remove_child((GtkPopoverMenu *)arg0, (GtkWidget *)arg1); + GTK4_NATIVE_EXIT(env, that, gtk_1popover_1menu_1remove_1child_FUNC); + return rc; +} +#endif + #ifndef NO_gtk_1popover_1menu_1set_1menu_1model JNIEXPORT void JNICALL GTK4_NATIVE(gtk_1popover_1menu_1set_1menu_1model) (JNIEnv *env, jclass that, jlong arg0, jlong arg1) @@ -2488,6 +2530,28 @@ JNIEXPORT jlong JNICALL GTK4_NATIVE(gtk_1shortcut_1new) } #endif +#ifndef NO_gtk_1size_1group_1add_1widget +JNIEXPORT void JNICALL GTK4_NATIVE(gtk_1size_1group_1add_1widget) + (JNIEnv *env, jclass that, jlong arg0, jlong arg1) +{ + GTK4_NATIVE_ENTER(env, that, gtk_1size_1group_1add_1widget_FUNC); + gtk_size_group_add_widget((GtkSizeGroup *)arg0, (GtkWidget *)arg1); + GTK4_NATIVE_EXIT(env, that, gtk_1size_1group_1add_1widget_FUNC); +} +#endif + +#ifndef NO_gtk_1size_1group_1new +JNIEXPORT jlong JNICALL GTK4_NATIVE(gtk_1size_1group_1new) + (JNIEnv *env, jclass that, jint arg0) +{ + jlong rc = 0; + GTK4_NATIVE_ENTER(env, that, gtk_1size_1group_1new_FUNC); + rc = (jlong)gtk_size_group_new((GtkSizeGroupMode)arg0); + GTK4_NATIVE_EXIT(env, that, gtk_1size_1group_1new_FUNC); + return rc; +} +#endif + #ifndef NO_gtk_1snapshot_1append_1cairo JNIEXPORT jlong JNICALL GTK4_NATIVE(gtk_1snapshot_1append_1cairo) (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 f240d03cae9..3d221478b10 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 @@ -84,6 +84,7 @@ typedef enum { gdk_1toplevel_1size_1set_1size_FUNC, gsk_1render_1node_1draw_FUNC, gsk_1render_1node_1unref_FUNC, + gtk_1actionable_1set_1action_1name_FUNC, gtk_1box_1append_FUNC, gtk_1box_1insert_1child_1after_FUNC, gtk_1box_1prepend_FUNC, @@ -186,9 +187,11 @@ typedef enum { gtk_1picture_1new_FUNC, gtk_1picture_1set_1can_1shrink_FUNC, gtk_1picture_1set_1paintable_FUNC, + gtk_1popover_1menu_1add_1child_FUNC, gtk_1popover_1menu_1bar_1new_1from_1model_FUNC, gtk_1popover_1menu_1get_1menu_1model_FUNC, gtk_1popover_1menu_1new_1from_1model_1full_FUNC, + gtk_1popover_1menu_1remove_1child_FUNC, gtk_1popover_1menu_1set_1menu_1model_FUNC, gtk_1popover_1new_FUNC, gtk_1popover_1set_1autohide_FUNC, @@ -205,6 +208,8 @@ typedef enum { gtk_1shortcut_1controller_1remove_1shortcut_FUNC, gtk_1shortcut_1controller_1set_1scope_FUNC, gtk_1shortcut_1new_FUNC, + gtk_1size_1group_1add_1widget_FUNC, + gtk_1size_1group_1new_FUNC, gtk_1snapshot_1append_1cairo_FUNC, gtk_1snapshot_1free_1to_1node_FUNC, gtk_1snapshot_1new_FUNC, diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os.c b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os.c index 24178bd35c9..d4627b94e33 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os.c +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os.c @@ -3774,6 +3774,18 @@ JNIEXPORT void JNICALL GTK_NATIVE(gtk_1box_1set_1spacing) } #endif +#ifndef NO_gtk_1button_1get_1type +JNIEXPORT jlong JNICALL GTK_NATIVE(gtk_1button_1get_1type) + (JNIEnv *env, jclass that) +{ + jlong rc = 0; + GTK_NATIVE_ENTER(env, that, gtk_1button_1get_1type_FUNC); + rc = (jlong)gtk_button_get_type(); + GTK_NATIVE_EXIT(env, that, gtk_1button_1get_1type_FUNC); + return rc; +} +#endif + #ifndef NO_gtk_1button_1new JNIEXPORT jlong JNICALL GTK_NATIVE(gtk_1button_1new) (JNIEnv *env, jclass that) @@ -7068,6 +7080,24 @@ JNIEXPORT void JNICALL GTK_NATIVE(gtk_1style_1context_1add_1provider) } #endif +#ifndef NO_gtk_1style_1context_1add_1provider_1for_1display +JNIEXPORT void JNICALL GTK_NATIVE(gtk_1style_1context_1add_1provider_1for_1display) + (JNIEnv *env, jclass that, jlong arg0, jlong arg1, jint arg2) +{ + GTK_NATIVE_ENTER(env, that, gtk_1style_1context_1add_1provider_1for_1display_FUNC); +/* + gtk_style_context_add_provider_for_display((GdkDisplay *)arg0, (GtkStyleProvider *)arg1, (guint)arg2); +*/ + { + GTK_LOAD_FUNCTION(fp, gtk_style_context_add_provider_for_display) + if (fp) { + ((void (CALLING_CONVENTION*)(GdkDisplay *, GtkStyleProvider *, guint))fp)((GdkDisplay *)arg0, (GtkStyleProvider *)arg1, (guint)arg2); + } + } + GTK_NATIVE_EXIT(env, that, gtk_1style_1context_1add_1provider_1for_1display_FUNC); +} +#endif + #ifndef NO_gtk_1style_1context_1remove_1class JNIEXPORT void JNICALL GTK_NATIVE(gtk_1style_1context_1remove_1class) (JNIEnv *env, jclass that, jlong arg0, jbyteArray arg1) @@ -9424,6 +9454,28 @@ JNIEXPORT void JNICALL GTK_NATIVE(gtk_1widget_1realize) } #endif +#ifndef NO_gtk_1widget_1remove_1css_1class +JNIEXPORT void JNICALL GTK_NATIVE(gtk_1widget_1remove_1css_1class) + (JNIEnv *env, jclass that, jlong arg0, jbyteArray arg1) +{ + jbyte *lparg1=NULL; + GTK_NATIVE_ENTER(env, that, gtk_1widget_1remove_1css_1class_FUNC); + if (arg1) if ((lparg1 = (*env)->GetByteArrayElements(env, arg1, NULL)) == NULL) goto fail; +/* + gtk_widget_remove_css_class((GtkWidget *)arg0, (const char *)lparg1); +*/ + { + GTK_LOAD_FUNCTION(fp, gtk_widget_remove_css_class) + if (fp) { + ((void (CALLING_CONVENTION*)(GtkWidget *, const char *))fp)((GtkWidget *)arg0, (const char *)lparg1); + } + } +fail: + if (arg1 && lparg1) (*env)->ReleaseByteArrayElements(env, arg1, lparg1, 0); + GTK_NATIVE_EXIT(env, that, gtk_1widget_1remove_1css_1class_FUNC); +} +#endif + #ifndef NO_gtk_1widget_1set_1can_1focus JNIEXPORT void JNICALL GTK_NATIVE(gtk_1widget_1set_1can_1focus) (JNIEnv *env, jclass that, jlong arg0, jboolean arg1) @@ -9630,6 +9682,16 @@ JNIEXPORT void JNICALL GTK_NATIVE(gtk_1widget_1set_1size_1request) } #endif +#ifndef NO_gtk_1widget_1set_1state_1flags +JNIEXPORT void JNICALL GTK_NATIVE(gtk_1widget_1set_1state_1flags) + (JNIEnv *env, jclass that, jlong arg0, jint arg1, jboolean arg2) +{ + GTK_NATIVE_ENTER(env, that, gtk_1widget_1set_1state_1flags_FUNC); + gtk_widget_set_state_flags((GtkWidget *)arg0, (GtkStateFlags)arg1, arg2); + GTK_NATIVE_EXIT(env, that, gtk_1widget_1set_1state_1flags_FUNC); +} +#endif + #ifndef NO_gtk_1widget_1set_1tooltip_1text JNIEXPORT void JNICALL GTK_NATIVE(gtk_1widget_1set_1tooltip_1text) (JNIEnv *env, jclass that, jlong arg0, jbyteArray arg1) @@ -9684,6 +9746,16 @@ JNIEXPORT void JNICALL GTK_NATIVE(gtk_1widget_1unparent) } #endif +#ifndef NO_gtk_1widget_1unset_1state_1flags +JNIEXPORT void JNICALL GTK_NATIVE(gtk_1widget_1unset_1state_1flags) + (JNIEnv *env, jclass that, jlong arg0, jint arg1) +{ + GTK_NATIVE_ENTER(env, that, gtk_1widget_1unset_1state_1flags_FUNC); + gtk_widget_unset_state_flags((GtkWidget *)arg0, (GtkStateFlags)arg1); + GTK_NATIVE_EXIT(env, that, gtk_1widget_1unset_1state_1flags_FUNC); +} +#endif + #ifndef NO_gtk_1window_1fullscreen JNIEXPORT void JNICALL GTK_NATIVE(gtk_1window_1fullscreen) (JNIEnv *env, jclass that, jlong arg0) @@ -11984,6 +12056,20 @@ JNIEXPORT void JNICALL OS_NATIVE(g_1menu_1item_1set_1attribute) } #endif +#ifndef NO_g_1menu_1item_1set_1attribute_1value +JNIEXPORT void JNICALL OS_NATIVE(g_1menu_1item_1set_1attribute_1value) + (JNIEnv *env, jclass that, jlong arg0, jbyteArray arg1, jlong arg2) +{ + jbyte *lparg1=NULL; + OS_NATIVE_ENTER(env, that, g_1menu_1item_1set_1attribute_1value_FUNC); + if (arg1) if ((lparg1 = (*env)->GetByteArrayElements(env, arg1, NULL)) == NULL) goto fail; + g_menu_item_set_attribute_value((GMenuItem *)arg0, (const gchar *)lparg1, (GVariant *)arg2); +fail: + if (arg1 && lparg1) (*env)->ReleaseByteArrayElements(env, arg1, lparg1, 0); + OS_NATIVE_EXIT(env, that, g_1menu_1item_1set_1attribute_1value_FUNC); +} +#endif + #ifndef NO_g_1menu_1item_1set_1label JNIEXPORT void JNICALL OS_NATIVE(g_1menu_1item_1set_1label) (JNIEnv *env, jclass that, jlong arg0, jbyteArray arg1) @@ -12076,14 +12162,33 @@ JNIEXPORT jlong JNICALL OS_NATIVE(g_1object_1get_1qdata) } #endif -#ifndef NO_g_1object_1new -JNIEXPORT jlong JNICALL OS_NATIVE(g_1object_1new) +#ifndef NO_g_1object_1new__JJ +JNIEXPORT jlong JNICALL OS_NATIVE(g_1object_1new__JJ) (JNIEnv *env, jclass that, jlong arg0, jlong arg1) { jlong rc = 0; - OS_NATIVE_ENTER(env, that, g_1object_1new_FUNC); + OS_NATIVE_ENTER(env, that, g_1object_1new__JJ_FUNC); rc = (jlong)g_object_new((GType)arg0, (const gchar *)arg1); - OS_NATIVE_EXIT(env, that, g_1object_1new_FUNC); + OS_NATIVE_EXIT(env, that, g_1object_1new__JJ_FUNC); + return rc; +} +#endif + +#ifndef NO_g_1object_1new__J_3B_3BJ +JNIEXPORT jlong JNICALL OS_NATIVE(g_1object_1new__J_3B_3BJ) + (JNIEnv *env, jclass that, jlong arg0, jbyteArray arg1, jbyteArray arg2, jlong arg3) +{ + jbyte *lparg1=NULL; + jbyte *lparg2=NULL; + jlong rc = 0; + OS_NATIVE_ENTER(env, that, g_1object_1new__J_3B_3BJ_FUNC); + if (arg1) if ((lparg1 = (*env)->GetByteArrayElements(env, arg1, NULL)) == NULL) goto fail; + if (arg2) if ((lparg2 = (*env)->GetByteArrayElements(env, arg2, NULL)) == NULL) goto fail; + rc = (jlong)g_object_new((GType)arg0, (const gchar *)lparg1, (const gchar *)lparg2, (const gchar *)NULL); +fail: + if (arg2 && lparg2) (*env)->ReleaseByteArrayElements(env, arg2, lparg2, JNI_ABORT); + if (arg1 && lparg1) (*env)->ReleaseByteArrayElements(env, arg1, lparg1, JNI_ABORT); + OS_NATIVE_EXIT(env, that, g_1object_1new__J_3B_3BJ_FUNC); return rc; } #endif diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os_stats.h b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os_stats.h index f30ca89d0ad..51c1e64be29 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os_stats.h +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/library/os_stats.h @@ -296,6 +296,7 @@ typedef enum { gtk_1box_1new_FUNC, gtk_1box_1set_1homogeneous_FUNC, gtk_1box_1set_1spacing_FUNC, + gtk_1button_1get_1type_FUNC, gtk_1button_1new_FUNC, gtk_1button_1set_1label_FUNC, gtk_1button_1set_1use_1underline_FUNC, @@ -567,6 +568,7 @@ typedef enum { gtk_1spin_1button_1update_FUNC, gtk_1style_1context_1add_1class_FUNC, gtk_1style_1context_1add_1provider_FUNC, + gtk_1style_1context_1add_1provider_1for_1display_FUNC, gtk_1style_1context_1remove_1class_FUNC, gtk_1style_1context_1restore_FUNC, gtk_1style_1context_1save_FUNC, @@ -751,6 +753,7 @@ typedef enum { gtk_1widget_1queue_1draw_FUNC, gtk_1widget_1queue_1resize_FUNC, gtk_1widget_1realize_FUNC, + gtk_1widget_1remove_1css_1class_FUNC, gtk_1widget_1set_1can_1focus_FUNC, gtk_1widget_1set_1default_1direction_FUNC, gtk_1widget_1set_1direction_FUNC, @@ -766,11 +769,13 @@ typedef enum { gtk_1widget_1set_1receives_1default_FUNC, gtk_1widget_1set_1sensitive_FUNC, gtk_1widget_1set_1size_1request_FUNC, + gtk_1widget_1set_1state_1flags_FUNC, gtk_1widget_1set_1tooltip_1text_FUNC, gtk_1widget_1set_1valign_FUNC, gtk_1widget_1set_1vexpand_FUNC, gtk_1widget_1set_1visible_FUNC, gtk_1widget_1unparent_FUNC, + gtk_1widget_1unset_1state_1flags_FUNC, gtk_1window_1fullscreen_FUNC, gtk_1window_1get_1default_1size_FUNC, gtk_1window_1get_1default_1widget_FUNC, @@ -969,6 +974,7 @@ typedef enum { g_1menu_1item_1new_1section_FUNC, g_1menu_1item_1new_1submenu_FUNC, g_1menu_1item_1set_1attribute_FUNC, + g_1menu_1item_1set_1attribute_1value_FUNC, g_1menu_1item_1set_1label_FUNC, g_1menu_1item_1set_1submenu_FUNC, g_1menu_1new_FUNC, @@ -976,7 +982,8 @@ typedef enum { g_1object_1get__J_3B_3IJ_FUNC, g_1object_1get__J_3B_3JJ_FUNC, g_1object_1get_1qdata_FUNC, - g_1object_1new_FUNC, + g_1object_1new__JJ_FUNC, + g_1object_1new__J_3B_3BJ_FUNC, g_1object_1notify_FUNC, g_1object_1ref_FUNC, g_1object_1ref_1sink_FUNC, diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/GTK.java b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/GTK.java index 9a6c2cb9cf8..9f9cce70c94 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/GTK.java +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/gtk/org/eclipse/swt/internal/gtk/GTK.java @@ -258,6 +258,7 @@ public class GTK extends OS { /* GtkButton */ + public static final native long gtk_button_get_type(); public static final native long gtk_button_new(); /** * @method flags=dynamic @@ -1038,6 +1039,13 @@ public class GTK extends OS { * @param priority cast=(guint) */ public static final native void gtk_style_context_add_provider(long context, long provider, int priority); + /** + * @method flags=dynamic + * @param display cast=(GdkDisplay *) + * @param provider cast=(GtkStyleProvider *) + * @param priority cast=(guint) + */ + public static final native void gtk_style_context_add_provider_for_display(long display, long provider, int priority); /** * @param context cast=(GtkStyleContext *) * @param class_name cast=(const gchar *) @@ -2270,6 +2278,16 @@ public class GTK extends OS { public static final native void gtk_widget_set_margin_bottom(long widget, int margin); /** @param self cast=(GtkWidget *) */ public static final native int gtk_widget_get_state_flags(long self); + /** + * @param widget cast=(GtkWidget *) + * @param flags cast=(GtkStateFlags) + */ + public static final native void gtk_widget_unset_state_flags(long widget, int flags); + /** + * @param widget cast=(GtkWidget *) + * @param flags cast=(GtkStateFlags) + */ + public static final native void gtk_widget_set_state_flags(long widget, int flags, boolean clear); /** @param widget cast=(GtkWidget *) */ public static final native boolean gtk_widget_has_default(long widget); @@ -2281,6 +2299,12 @@ public class GTK extends OS { * @param css_class cast=(const char *) * */ public static final native void gtk_widget_add_css_class(long widget, byte[] css_class); + /** + * @method flags=dynamic + * @param widget cast=(GtkWidget *) + * @param css_class cast=(const char *) + * */ + public static final native void gtk_widget_remove_css_class(long widget, byte[] css_class); /** @param widget cast=(GtkWidget *) */ public static final native boolean gtk_widget_child_focus(long widget, int direction); /** 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 a070c4d1329..c9a9359d3ac 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 @@ -1277,6 +1277,13 @@ public static boolean isX11 () { * @param first_property_name cast=(const gchar *) */ public static final native long g_object_new(long type, long first_property_name); +/** + * @param type cast=(GType) + * @param first_property_name cast=(const gchar *),flags=no_out + * @param value cast=(const gchar *),flags=no_out + * @param terminator cast=(const gchar *),flags=sentinel + */ +public static final native long g_object_new(long type, byte[] first_property_name, byte[] value, long terminator); /** * @param object cast=(GObject *) * @param property_name cast=(const gchar *) @@ -2417,6 +2424,12 @@ public static final native long g_dbus_proxy_new_for_bus_sync(int bus_type, int * @param data cast=(const gchar *) */ public static final native void g_menu_item_set_attribute(long menu_item, byte[] attribute, byte[] format_string, long data); +/** + * @param menu_item cast=(GMenuItem *) + * @param attribute cast=(const gchar *) + * @param value cast=(GVariant *) + */ +public static final native void g_menu_item_set_attribute_value(long menu_item, byte[] attribute, long value); /* GSimpleActionGroup */ public static final native long g_simple_action_group_new(); 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 3c33dfcfb79..45fa77c7549 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 @@ -21,6 +21,7 @@ public class GTK4 { public static final int GTK_POPOVER_MENU_NESTED = 1 << 0; + public static final int GTK_SIZE_GROUP_HORIZONTAL = 1; /* * GdkDragAction values. Note that GTK4 redefined these compared to GTK3 @@ -75,6 +76,13 @@ public class GTK4 { */ public static final native void gtk_rgb_to_hsv(float r, float g, float b, float[] h, float[] s, float[] v); + /* GtkActionable */ + /** + * @param actionable cast=(GtkActionable *) + * @param action_name cast=(const char *) + */ + public static final native void gtk_actionable_set_action_name(long actionable, byte[] action_name); + /* GtkBox */ /** * @param box cast=(GtkBox *) @@ -533,6 +541,15 @@ public class GTK4 { /** @param window cast=(GtkWindow *) */ public static final native void gtk_window_present(long window) ; + /* GtkSizeGroup */ + /** @param mode cast=(GtkSizeGroupMode) */ + public static final native long gtk_size_group_new(int mode); + /** + * @param size_group cast=(GtkSizeGroup *) + * @param widget cast=(GtkWidget *) + */ + public static final native void gtk_size_group_add_widget(long size_group, long widget); + /* GtkShortcutController */ public static final native long gtk_shortcut_controller_new(); /** @@ -590,6 +607,17 @@ public class GTK4 { public static final native void gtk_text_set_tabs(long entry, long tabs); /* GtkPopoverMenu */ + /** + * @param popover cast=(GtkPopoverMenu *) + * @param child cast=(GtkWidget *) + * @param id cast=(const char *) + */ + public static final native boolean gtk_popover_menu_add_child(long popover, long child, byte[] id); + /** + * @param popover cast=(GtkPopoverMenu *) + * @param child cast=(GtkWidget *) + */ + public static final native boolean gtk_popover_menu_remove_child(long popover, long child); /** * @param model cast=(GMenuModel *) * @param flags cast=(GtkPopoverMenuFlags) 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 4a1bc89cce3..a4e74520f0f 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 @@ -136,6 +136,16 @@ public class Display extends Device implements Executor { long snapshotDrawProc, keyPressReleaseProc, focusProc, windowActiveProc, enterMotionProc, leaveProc, scrollProc, resizeProc, layoutProc, activateProc, gesturePressReleaseProc; long menuItemsChangedProc; + /** GTK4 only: when the last key event was dispatched, see Menu#clearStrayRowSelection. */ + long lastKeyEventTime; + /** + * GTK4 only: set while SWT is mutating a GMenu. The removal's synchronous + * "items-changed" re-enters menu wiring, so position-based custom widget + * injection must wait until the model is whole again (see MenuItem#refreshMenuModelGTK4). + */ + boolean menuModelMutating; + /** GTK4 only: the MenuItem custom-icon-row selection CSS has been installed, see ensureMenuIconRowCss. */ + private boolean menuIconRowCssApplied; long notifyProc; long computeSizeProc; Callback windowCallback2, windowCallback3, windowCallback4, windowCallback5, windowCallback6; @@ -862,6 +872,24 @@ void addSkinnableWidget (Widget widget) { skinList [skinCount++] = widget; } +/** + * GTK4 only: installs, once per display, the rule that paints the selection + * highlight of MenuItem custom icon rows (see MenuItem.setCustomRowSelected). + * Installed above the user priority so it wins over the per-widget background a + * themed workbench (e.g. the Eclipse e4 dark theme) attaches to these SWT-owned + * rows. Painting with the row's text color at a low alpha matches native rows in + * any theme without a hard-coded color. + */ +void ensureMenuIconRowCss() { + if (menuIconRowCssApplied) return; + menuIconRowCssApplied = true; + long provider = GTK.gtk_css_provider_new(); + GTK4.gtk_css_provider_load_from_data(provider, + Converter.wcsToMbcs("modelbutton." + MenuItem.CUSTOM_SELECTED_CLASS + " { background-color: alpha(currentColor, 0.1); }", true), -1); + GTK.gtk_style_context_add_provider_for_display(GDK.gdk_display_get_default(), provider, GTK.GTK_STYLE_PROVIDER_PRIORITY_USER + 1); + OS.g_object_unref(provider); +} + void addWidget (long handle, Widget widget) { if (handle == 0) return; // Last element in the indexTable is -1, so if freeSlot == -1 we have no place anymore @@ -6123,6 +6151,7 @@ boolean scrollProc(long controller, double dx, double dy, long user_data) { } boolean keyPressReleaseProc(long controller, int keyval, int keycode, int state, long user_data) { + lastKeyEventTime = System.currentTimeMillis(); long handle = GTK.gtk_event_controller_get_widget(controller); Widget widget = getWidget(handle); if (widget == null) return false; 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 19467e4b2d3..e76a3b3c9cd 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 @@ -56,6 +56,8 @@ public class Menu extends Widget { /** GTK4 only fields */ long modelHandle, popoverHandle, actionGroup, shortcutController; + /** GTK4 only: aligns the label column of custom and native rows, see alignRowLabels(). */ + long sizeGroupHandle; class Section { LinkedList sectionItems; @@ -576,6 +578,7 @@ void createHandle (int index) { default: handle = GTK4.gtk_popover_menu_new_from_model_full(modelHandle, GTK4.GTK_POPOVER_MENU_NESTED); GTK.gtk_widget_set_parent(handle, parent.handle); + hookPopoverFocusEnter(handle); GTK.gtk_popover_set_position(handle, GTK.GTK_POS_BOTTOM); GTK4.gtk_popover_set_has_arrow(handle, false); GTK.gtk_widget_set_halign(handle, GTK.GTK_ALIGN_START); @@ -937,7 +940,9 @@ long gtk_map (long widget) { * The POP_UP GtkPopoverMenu has been mapped. Its handle IS the GtkPopoverMenu, * and nested GtkPopoverMenu children for any CASCADE submenus already exist in * its widget tree. Connect SHOW/HIDE signals for those nested submenus now. + * Also inject any custom icon widgets into the popover. */ + injectCustomMenuIcons(); connectCascadeSubMenuSignals(this, handle); } } @@ -1020,28 +1025,29 @@ private void wireSubMenuPopover(Menu submenu, long popover) { OS.g_object_ref(popover); submenu.popoverHandle = popover; display.addWidget(popover, submenu); + submenu.hookPopoverFocusEnter(popover); 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); - for (MenuItem menuItem : items) { - if (barItem == 0) break; - if ((menuItem.style & SWT.SEPARATOR) != 0) continue; - if (menuItem.menu != null) { - long popover = findGtkPopoverMenuChild(barItem); - /* 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); - } + if (menuItem.menu == null) continue; + /* + * Locate the popover by its menu model, not positionally: this also runs + * from "items-changed" mid create/dispose, when the item list and the + * bar's children are out of step and a positional walk wires the wrong menu. + */ + long popover = findNestedPopoverForModel(handle, menuItem.menu.modelHandle); + /* 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) { + menuItem.menu.injectCustomMenuIcons(); + connectCascadeSubMenuSignals(menuItem.menu); } - barItem = GTK4.gtk_widget_get_next_sibling(barItem); } } @@ -1065,12 +1071,220 @@ private void connectCascadeSubMenuSignals(Menu menu, long parentPopoverHandle) { if (item.menu.popoverHandle != nestedPopover) { wireSubMenuPopover(item.menu, nestedPopover); } + item.menu.injectCustomMenuIcons(); connectCascadeSubMenuSignals(item.menu); } } } } +/** + * GTK4: hides any showing CASCADE submenu of this menu, mirroring GtkModelButton + * closing an open sibling submenu as the pointer moves on. Hidden via + * gtk_widget_set_visible(FALSE) like GTK does, not gtk_popover_popdown(), which + * would cascade to the parent popover and close the whole menu. + */ +void hideOpenSubmenus() { + // Walk the live widget tree rather than trusting each submenu's cached + // popoverHandle, which GTK may have rebuilt on a model change (a stale handle + // then hits GTK_IS_WIDGET). + long popover = (style & SWT.POP_UP) != 0 ? handle : popoverHandle; + if (popover != 0) hideVisibleSubmenus(popover, popover); +} + +private void hideVisibleSubmenus(long widget, long root) { + for (long child = GTK4.gtk_widget_get_first_child(widget); child != 0; child = GTK4.gtk_widget_get_next_sibling(child)) { + if (GTK4.GTK_IS_POPOVER_MENU(child)) { + if (child != root && GTK.gtk_widget_get_visible(child)) GTK.gtk_widget_set_visible(child, false); + } else { + hideVisibleSubmenus(child, root); + } + } +} + +/** + * GTK4: hooks focus-enter and motion controllers onto every menu popover. Hiding + * a focused row (a submenu closing, a row being rebuilt) makes GtkWindow move + * focus onto some other row of the popup, selecting it although the pointer is + * elsewhere; the focus controller sweeps that stray selection (see + * clearStrayRowSelection). The motion controller keeps the pointer's row + * highlighted grab-independently (see syncRowSelectionRecursive). + */ +void hookPopoverFocusEnter(long popover) { + long focusController = GTK4.gtk_event_controller_focus_new(); + OS.g_signal_connect(focusController, OS.enter, display.focusProc, FOCUS_IN); + GTK4.gtk_widget_add_controller(popover, focusController); + long motionController = GTK4.gtk_event_controller_motion_new(); + OS.g_signal_connect(motionController, OS.enter, display.enterMotionProc, ENTER); + OS.g_signal_connect(motionController, OS.motion, display.enterMotionProc, MOTION); + GTK4.gtk_widget_add_controller(popover, motionController); +} + +@Override +void gtk4_enter_event(long controller, double x, double y, long event) { + gtk4_motion_event(controller, x, y, event); +} + +@Override +void gtk4_motion_event(long controller, double x, double y, long event) { + if (System.currentTimeMillis() - display.lastKeyEventTime < 500) return; + // The motion may be delivered to any popover of the menu (the grab holder), so + // sync from the root popover down, into open submenus. + long popover = GTK.gtk_event_controller_get_widget(controller); + for (long p = GTK.gtk_widget_get_parent(popover); p != 0; p = GTK.gtk_widget_get_parent(p)) { + if (GTK4.GTK_IS_POPOVER_MENU(p)) popover = p; + } + syncRowSelectionRecursive(popover); +} + +/* + * GtkPopoverMenu stops updating a row's selection once a submenu holds the pointer + * grab, so re-derive every row's selection from its "prelight" state, which + * crossing events set on the row under the pointer regardless of the grab. + */ +private void syncRowSelectionRecursive(long widget) { + // Skip a closed submenu's whole subtree: it has no hoverable rows, and a large + // menu keeps all submenus instantiated, so walking them would slow every motion. + if (GTK4.GTK_IS_POPOVER_MENU(widget) && !GTK.gtk_widget_get_visible(widget)) return; + Widget item = display.getWidget(widget); + boolean custom = item instanceof MenuItem menuItem && menuItem.customWidgetHandle == widget; + if (custom || isModelButton(widget)) { + int flags = GTK.gtk_widget_get_state_flags(widget); + boolean selected = (flags & GTK.GTK_STATE_FLAG_PRELIGHT) != 0 || hasVisibleSubmenu(widget); + if (custom) { + /* Toggles the highlight class and forces the repaint, see MenuItem. */ + ((MenuItem) item).setCustomRowSelected(selected); + } else if (selected && (flags & GTK.GTK_STATE_FLAG_SELECTED) == 0) { + GTK.gtk_widget_set_state_flags(widget, GTK.GTK_STATE_FLAG_SELECTED, false); + } else if (!selected && (flags & GTK.GTK_STATE_FLAG_SELECTED) != 0) { + GTK.gtk_widget_unset_state_flags(widget, GTK.GTK_STATE_FLAG_SELECTED); + } + /* A cascade row hosts its submenu popover as a child; keep descending. */ + } + for (long child = GTK4.gtk_widget_get_first_child(widget); child != 0; child = GTK4.gtk_widget_get_next_sibling(child)) { + syncRowSelectionRecursive(child); + } +} + +@Override +void gtk4_focus_enter_event(long controller, long event) { + long popover = GTK.gtk_event_controller_get_widget(controller); + display.asyncExec(() -> { + if (!isDisposed()) clearStrayRowSelection(popover); + }); +} + +/** GTK4: clears a "selected" highlight left on a row that is neither hovered nor showing a submenu. */ +void clearStrayRowSelection(long popover) { + if (popover == 0) return; + /* Keyboard navigation selects rows through focus as well; leave those alone. */ + if (System.currentTimeMillis() - display.lastKeyEventTime < 500) return; + for (long child = GTK4.gtk_widget_get_first_child(popover); child != 0; child = GTK4.gtk_widget_get_next_sibling(child)) { + clearStrayRowSelectionRecursive(child); + } +} + +private void clearStrayRowSelectionRecursive(long widget) { + /* Nested submenu popovers are children of their cascade row; leave them alone. */ + if (GTK4.GTK_IS_POPOVER_MENU(widget)) return; + int flags = GTK.gtk_widget_get_state_flags(widget); + Widget item = display.getWidget(widget); + boolean custom = item instanceof MenuItem menuItem && menuItem.customWidgetHandle == widget; + if ((custom || isModelButton(widget)) && (flags & GTK.GTK_STATE_FLAG_SELECTED) != 0 + && (flags & GTK.GTK_STATE_FLAG_PRELIGHT) == 0 && !hasVisibleSubmenu(widget)) { + if (custom) { + ((MenuItem) item).setCustomRowSelected(false); + } else { + GTK.gtk_widget_unset_state_flags(widget, GTK.GTK_STATE_FLAG_SELECTED); + } + } + for (long child = GTK4.gtk_widget_get_first_child(widget); child != 0; child = GTK4.gtk_widget_get_next_sibling(child)) { + clearStrayRowSelectionRecursive(child); + } +} + +private static boolean hasVisibleSubmenu(long row) { + for (long child = GTK4.gtk_widget_get_first_child(row); child != 0; child = GTK4.gtk_widget_get_next_sibling(child)) { + if (GTK4.GTK_IS_POPOVER_MENU(child) && GTK.gtk_widget_get_visible(child)) return true; + } + return false; +} + +/** + * (Re-)injects the custom icon+label widgets of this menu's PUSH items into the + * GtkPopoverMenu, for those items that have one. A no-op for items whose widget + * is already attached. + */ +void injectCustomMenuIcons() { + if (items == null || display.menuModelMutating) return; + for (MenuItem item : items) { + if (item.customWidgetHandle != 0) { + item.injectCustomWidgetGTK4(); + } + } + updateCustomRowGutters(); +} + +/** + * GTK4: shows the invisible indicator gutter of custom PUSH rows while the menu + * has a CHECK/RADIO item, so their icons line up with a CHECK row's icon, as GTK3 + * menus reserve toggle space for all items once one has a toggle. + */ +void updateCustomRowGutters() { + if (items == null) return; + boolean hasToggle = false; + for (MenuItem item : items) { + if ((item.style & (SWT.CHECK | SWT.RADIO)) != 0) { + hasToggle = true; + break; + } + } + for (MenuItem item : items) { + if (item.customIndicatorHandle != 0 && (item.style & SWT.CHECK) == 0) { + GTK.gtk_widget_set_visible(item.customIndicatorHandle, hasToggle); + } + } + alignRowLabels(); +} + +/** + * GTK4: keeps custom and native row labels in one column by joining the custom + * rows' leading boxes and the native rows' indicator boxes into one horizontal + * GtkSizeGroup. The group drops destroyed widgets and ignores duplicates, so this + * can simply run again whenever custom rows are (re)injected. + */ +private void alignRowLabels() { + long popover = (style & SWT.POP_UP) != 0 ? handle : popoverHandle; + if (popover == 0) return; + boolean hasCustom = false; + for (MenuItem item : items) { + if (item.customLeadingBoxHandle != 0) hasCustom = true; + } + if (!hasCustom) return; + if (sizeGroupHandle == 0) sizeGroupHandle = GTK4.gtk_size_group_new(GTK4.GTK_SIZE_GROUP_HORIZONTAL); + for (MenuItem item : items) { + if (item.customLeadingBoxHandle != 0) GTK4.gtk_size_group_add_widget(sizeGroupHandle, item.customLeadingBoxHandle); + } + addNativeIndicatorBoxes(popover); +} + +private void addNativeIndicatorBoxes(long widget) { + for (long child = GTK4.gtk_widget_get_first_child(widget); child != 0; child = GTK4.gtk_widget_get_next_sibling(child)) { + /* Nested submenu popovers are children of their cascade row and align their own rows. */ + if (GTK4.GTK_IS_POPOVER_MENU(child)) continue; + if (isModelButton(child)) { + long box = GTK4.gtk_widget_get_first_child(child); + if (box != 0 && GTK.GTK_IS_BOX(box)) GTK4.gtk_size_group_add_widget(sizeGroupHandle, box); + } else { + addNativeIndicatorBoxes(child); + } + } +} + +private static boolean isModelButton(long widget) { + return "GtkModelButton".equals(Converter.cCharPtrToJavaString(OS.g_type_name(OS.G_OBJECT_TYPE(widget)), false)); +} + /** * Recursively searches the widget subtree rooted at {@code parentWidget} for the * nested GtkPopoverMenu whose GMenuModel is {@code targetModel}, returning its @@ -1094,17 +1308,6 @@ private long findNestedPopoverForModel(long parentWidget, long targetModel) { } -private long findGtkPopoverMenuChild(long barItem) { - long child = GTK4.gtk_widget_get_first_child(barItem); - while (child != 0) { - if (GTK4.GTK_IS_POPOVER_MENU(child)) { - return child; - } - child = GTK4.gtk_widget_get_next_sibling(child); - } - return 0; -} - @Override long gtk_hide (long widget) { if ((style & SWT.POP_UP) != 0) { @@ -1137,6 +1340,7 @@ long gtk_show (long widget) { 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) { + injectCustomMenuIcons(); connectCascadeSubMenuSignals(this, popoverHandle); } if (OS.ubuntu_menu_proxy_get() != 0) { @@ -1336,6 +1540,10 @@ void releaseWidget () { cascade = null; if (imageList != null) imageList.dispose (); imageList = null; + if (sizeGroupHandle != 0) { + OS.g_object_unref(sizeGroupHandle); + sizeGroupHandle = 0; + } } /** diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/MenuItem.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/MenuItem.java index b28f41db7e1..e79d6e9b718 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/MenuItem.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/MenuItem.java @@ -82,6 +82,30 @@ public class MenuItem extends Item { * API. */ String actionName; + /** GTK4 only: custom widget for icon+text display in GtkPopoverMenu */ + long customWidgetHandle, customImageHandle, customLabelHandle, customAccelHandle; + /** + * GTK4 only: leading indicator slot of the custom row. A live GtkCheckButton for + * CHECK items, an invisible width-reserving one for PUSH items (see updateCustomRowGutters). + */ + long customIndicatorHandle; + /** + * GTK4 only: box holding the indicator slot and icon; shares a GtkSizeGroup with + * the native rows' indicator boxes so all labels start in one column. + */ + long customLeadingBoxHandle; + String customId; + private static int customIdSeq = 0; + /** GTK4 only: CSS class toggled on the custom row to show its selection highlight, see Display.ensureMenuIconRowCss. */ + static final String CUSTOM_SELECTED_CLASS = "swt-selected"; + /** GTK4 only: pointer is over the custom widget, see gtk4_enter_event. */ + private boolean customRowHovered; + /** GTK4 only: custom widget holds the keyboard focus, see gtk4_focus_enter_event. */ + private boolean customRowFocused; + /** GTK4 only: identifies the latest hover timer, older ones are no-ops. */ + private int hoverSerial; + /** Hover time (ms) before a custom row closes a sibling's open submenu. */ + private static final int SUBMENU_CLOSE_DELAY = 100; /** * Constructs a new instance of this class given its parent @@ -355,18 +379,33 @@ void createHandle(int index) { section = parent.new Section(this); int itemsToMove = selectedSection.sectionItems.size() - sectionRelativeIndex; - for (int i = 0; i < itemsToMove; i++) { - MenuItem removedItem = selectedSection.sectionItems.remove(sectionRelativeIndex); - section.sectionItems.add(removedItem); + /* Moving items walks the model out of step; hold off injection until the split completes. */ + boolean wasMutating = display.menuModelMutating; + display.menuModelMutating = true; + try { + for (int i = 0; i < itemsToMove; i++) { + MenuItem removedItem = selectedSection.sectionItems.remove(sectionRelativeIndex); + section.sectionItems.add(removedItem); + + OS.g_menu_remove(selectedSection.getSectionHandle(), sectionRelativeIndex); + /* + * The moved row is rebuilt from scratch; give it a fresh id. A stale one + * can still match the destroyed slot in GtkMenuSectionBox's id table + * ("Duplicate custom ID"), leaving the new row without a slot. + */ + if (removedItem.customWidgetHandle != 0) removedItem.reassignCustomId(); + OS.g_menu_insert_item(modelHandle, section.sectionItems.indexOf(removedItem), removedItem.handle); + removedItem.section = section; + } - OS.g_menu_remove(selectedSection.getSectionHandle(), sectionRelativeIndex); - OS.g_menu_insert_item(modelHandle, section.sectionItems.indexOf(removedItem), removedItem.handle); - removedItem.section = section; + int sectionInsertIndex = parent.sections.indexOf(selectedSection) + 1; + parent.sections.add(sectionInsertIndex, section); + OS.g_menu_insert_item(parent.modelHandle, sectionInsertIndex, handle); + } finally { + display.menuModelMutating = wasMutating; } - - int sectionInsertIndex = parent.sections.indexOf(selectedSection) + 1; - parent.sections.add(sectionInsertIndex, section); - OS.g_menu_insert_item(parent.modelHandle, sectionInsertIndex, handle); + /* The split destroyed the moved rows' custom widgets; re-inject now that the model is whole. */ + parent.injectCustomMenuIcons(); } else { section = selectedSection; selectedSection.sectionItems.add(sectionRelativeIndex, this); @@ -374,6 +413,8 @@ void createHandle(int index) { } parent.items.add(index, this); + /* A first toggle item makes the custom rows show their indicator gutter. */ + if ((style & (SWT.CHECK | SWT.RADIO)) != 0) parent.updateCustomRowGutters(); } else { byte[] buffer = new byte[1]; @@ -783,6 +824,18 @@ void releaseWidget() { * 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)); + if (customWidgetHandle != 0) { + detachCustomMenuWidget(); + display.removeWidget(customWidgetHandle); + OS.g_object_unref(customWidgetHandle); + customWidgetHandle = 0; + customImageHandle = 0; + customLabelHandle = 0; + customAccelHandle = 0; + customIndicatorHandle = 0; + customLeadingBoxHandle = 0; + customId = null; + } } else { long accelGroup = getAccelGroup(); if (accelGroup != 0) removeAccelerator(accelGroup); @@ -800,24 +853,42 @@ void releaseWidget() { @Override void destroyWidget() { if (GTK.GTK4) { - if ((style & SWT.SEPARATOR) != 0) { - Section aboveSection = parent.sections.get(parent.sections.indexOf(section) - 1); - aboveSection.sectionItems.addAll(section.sectionItems); + /* + * Removing this item (or merging its section away) walks the model out of + * step with SWT's bookkeeping; hold off custom widget injection until the + * model is whole again (see refreshMenuModelGTK4). + */ + boolean wasMutating = display.menuModelMutating; + display.menuModelMutating = true; + try { + if ((style & SWT.SEPARATOR) != 0) { + Section aboveSection = parent.sections.get(parent.sections.indexOf(section) - 1); + aboveSection.sectionItems.addAll(section.sectionItems); + + /* Drop the old section before re-inserting its items, so old rows are gone before new ones are built. */ + OS.g_menu_remove(parent.modelHandle, parent.sections.indexOf(section)); + + for (MenuItem item : section.sectionItems) { + item.section = aboveSection; + /* Fresh id for the rebuilt row, see the SEPARATOR split in createHandle. */ + if (item.customWidgetHandle != 0) item.reassignCustomId(); + OS.g_menu_insert_item(aboveSection.getSectionHandle(), aboveSection.sectionItems.indexOf(item), item.handle); + } - for (MenuItem item : section.sectionItems) { - item.section = aboveSection; - OS.g_menu_insert_item(aboveSection.getSectionHandle(), aboveSection.sectionItems.indexOf(item), item.handle); + parent.sections.remove(section); + } else { + OS.g_menu_remove(section.getSectionHandle(), section.sectionItems.indexOf(this)); + section.sectionItems.remove(this); } - - OS.g_menu_remove(parent.modelHandle, parent.sections.indexOf(section)); - - parent.sections.remove(section); - } else { - OS.g_menu_remove(section.getSectionHandle(), section.sectionItems.indexOf(this)); - section.sectionItems.remove(this); + } finally { + display.menuModelMutating = wasMutating; } + /* The merge destroyed the moved rows' custom widgets; re-inject now that the model is whole. */ + parent.injectCustomMenuIcons(); parent.items.remove(this); + /* The last toggle item leaving the menu collapses the custom rows' gutter. */ + parent.updateCustomRowGutters(); parent = null; if (modelHandle != 0) OS.g_object_unref(modelHandle); @@ -1072,9 +1143,6 @@ public void setID (int id) { */ @Override public void setImage (Image image) { - //TODO: GTK4 Menu images with text are no longer supported - if (GTK.GTK4) return; - checkWidget(); if (this.image == image) return; if ((style & SWT.SEPARATOR) != 0) return; @@ -1085,6 +1153,10 @@ public void setImage (Image image) { } private void _setImage (Image image) { + if (GTK.GTK4) { + _setImageGTK4(image); + return; + } if (image != null) { ImageList imageList = parent.imageList; if (imageList == null) imageList = parent.imageList = new ImageList (); @@ -1130,6 +1202,365 @@ private void _setImage (Image image) { } } +private void _setImageGTK4(Image image) { + // Only PUSH and CHECK items use a custom icon+label row; CASCADE and RADIO keep + // their native GtkModelButton. CASCADE never gets a custom slot (its submenu link + // wins), and RADIO needs a detailed action name a lone GtkCheckButton cannot give. + if ((style & (SWT.CASCADE | SWT.RADIO)) != 0) { + return; + } + if (image != null) { + long pixbuf = ImageList.createPixbuf(image); + if (pixbuf != 0) { + long texture = GDK.gdk_texture_new_for_pixbuf(pixbuf); + OS.g_object_unref(pixbuf); + if (texture != 0) { + boolean firstTime = customWidgetHandle == 0; + if (firstTime) { + createCustomMenuWidget(); + } + GTK4.gtk_image_set_from_paintable(customImageHandle, texture); + OS.g_object_unref(texture); + if (firstTime) { + // Inject now in case the menu is already open; otherwise + // Menu.injectCustomMenuIcons() retries when the menu is shown. + injectCustomWidgetGTK4(); + } + } + } + } else { + if (customWidgetHandle != 0) { + destroyCustomMenuWidget(); + refreshMenuModelGTK4(); + } + } +} + +private void createCustomMenuWidget() { + /* + * Use the "modelbutton" CSS node, not "button", so theme and SWT menu-row rules + * give the custom row the same padding, border and height as the native rows. The + * CSS name is construct-only, hence g_object_new instead of gtk_button_new. + */ + customWidgetHandle = OS.g_object_new(GTK.gtk_button_get_type(), + Converter.javaStringToCString("css-name"), Converter.javaStringToCString("modelbutton"), 0); + OS.g_object_ref_sink(customWidgetHandle); + GTK.gtk_widget_add_css_class(customWidgetHandle, Converter.javaStringToCString("flat")); + + display.ensureMenuIconRowCss(); + + long hbox = GTK.gtk_box_new(GTK.GTK_ORIENTATION_HORIZONTAL, 0); + /* Indicator slot and icon share one leading box; Menu.alignRowLabels() size-groups it with the native rows. */ + customLeadingBoxHandle = GTK.gtk_box_new(GTK.GTK_ORIENTATION_HORIZONTAL, 0); + /* + * An inert GtkCheckButton leads the row, excluded from hit testing and focus so + * clicks reach the enclosing button. For CHECK it is the indicator, bound to the + * item's action; for PUSH it is transparent and only reserves the toggle-gutter + * width (shown while the menu has toggle items, see updateCustomRowGutters). + */ + customIndicatorHandle = GTK.gtk_check_button_new(); + OS.g_object_set(customIndicatorHandle, Converter.javaStringToCString("can-target"), false, 0); + GTK4.gtk_widget_set_focusable(customIndicatorHandle, false); + GTK.gtk_widget_set_margin_end(customIndicatorHandle, 4); + if ((style & SWT.CHECK) != 0) { + if (actionName != null) { + GTK4.gtk_actionable_set_action_name(customIndicatorHandle, Converter.javaStringToCString(actionName)); + } + } else { + GTK.gtk_widget_set_opacity(customIndicatorHandle, 0); + GTK.gtk_widget_set_visible(customIndicatorHandle, false); + } + GTK4.gtk_box_append(customLeadingBoxHandle, customIndicatorHandle); + customImageHandle = GTK.gtk_image_new(); + GTK.gtk_widget_set_margin_end(customImageHandle, 4); + GTK4.gtk_box_append(customLeadingBoxHandle, customImageHandle); + GTK4.gtk_box_append(hbox, customLeadingBoxHandle); + + customLabelHandle = GTK.gtk_label_new_with_mnemonic(null); + GTK.gtk_label_set_xalign(customLabelHandle, 0.0f); + // Expand the label so the accelerator is pushed to the trailing edge, as in a native row. + GTK.gtk_widget_set_hexpand(customLabelHandle, true); + GTK4.gtk_box_append(hbox, customLabelHandle); + + // Same "accelerator" CSS node as GtkModelButton's accel label, so the theme spaces it alike. + customAccelHandle = OS.g_object_new(GTK.gtk_label_get_type(), + Converter.javaStringToCString("css-name"), Converter.javaStringToCString("accelerator"), 0); + GTK.gtk_label_set_xalign(customAccelHandle, 1.0f); + GTK4.gtk_box_append(hbox, customAccelHandle); + + GTK4.gtk_button_set_child(customWidgetHandle, hbox); + updateCustomWidgetLabels(); + + if (actionName != null) { + GTK4.gtk_actionable_set_action_name(customWidgetHandle, Converter.javaStringToCString(actionName)); + } + + /* + * A plain GtkButton does not pop down its popover when clicked, so do it from + * gtk_clicked() (the action, dispatching SWT.Selection, has already run by then). + */ + display.addWidget(customWidgetHandle, this); + OS.g_signal_connect_closure(customWidgetHandle, OS.clicked, display.getClosure(CLICKED), false); + + /* + * A plain GtkButton does not close a sibling's open submenu on hover as + * GtkModelButton does; track enter/leave and mirror that from gtk4_enter_event(). + */ + long motionController = GTK4.gtk_event_controller_motion_new(); + OS.g_signal_connect(motionController, OS.enter, display.enterMotionProc, ENTER); + OS.g_signal_connect(motionController, OS.motion, display.enterMotionProc, MOTION); + OS.g_signal_connect(motionController, OS.leave, display.leaveProc, LEAVE); + GTK4.gtk_widget_add_controller(customWidgetHandle, motionController); + /* Give the custom row the "selected" look while focused, as GtkPopoverMenu does its native rows. */ + long focusController = GTK4.gtk_event_controller_focus_new(); + OS.g_signal_connect(focusController, OS.enter, display.focusProc, FOCUS_IN); + OS.g_signal_connect(focusController, OS.leave, display.focusProc, FOCUS_OUT); + GTK4.gtk_widget_add_controller(customWidgetHandle, focusController); + + // Mark this GMenuItem slot as "custom" so GtkPopoverMenu creates a placeholder. + // injectCustomWidgetGTK4() reassigns a fresh id before actually embedding. + reassignCustomId(); + parent.updateCustomRowGutters(); +} + +@Override +long gtk_clicked (long widget) { + /* Only ever connected on the custom menu row (GTK4), see createCustomMenuWidget. */ + long popover = getParentPopoverHandle(); + if (popover != 0) GTK.gtk_popover_popdown(popover); + return 0; +} + +/** + * Pointer entered the custom menu row (GTK4). Like GtkModelButton, close a sibling's + * open submenu once the pointer has rested here briefly; the delay lets a diagonal + * move from a CASCADE row into its submenu cross this row without closing it. + */ +@Override +void gtk4_enter_event(long controller, double x, double y, long event) { + customRowHovered = true; + setCustomRowSelected(true); + armSubmenuCloseTimer(); +} + +@Override +void gtk4_motion_event(long controller, double x, double y, long event) { + /* Like GtkModelButton, restart the delay on every motion: only a pointer that + * rests on the row closes the submenu, one merely passing through does not. */ + armSubmenuCloseTimer(); +} + +private void armSubmenuCloseTimer() { + int serial = ++hoverSerial; + display.timerExec(SUBMENU_CLOSE_DELAY, () -> { + if (serial != hoverSerial || !customRowHovered || isDisposed()) return; + // Hiding the submenu hands focus back and leaves a stray row selected; clear it. + // We do not grab focus for this row: focusing a row a model rebuild can invalidate + // risks a crash in GtkWindow's focus handling, and the pointer sync keeps it highlighted. + parent.hideOpenSubmenus(); + parent.clearStrayRowSelection(getParentPopoverHandle()); + // The focus hand-back is partly async, so sweep once more when it has settled. + display.timerExec(SUBMENU_CLOSE_DELAY * 3, () -> { + if (!isDisposed()) parent.clearStrayRowSelection(getParentPopoverHandle()); + }); + }); +} + +@Override +void gtk4_leave_event(long controller, long event) { + customRowHovered = false; + /* Drop the highlight unless the row still holds the keyboard focus. */ + if (!customRowFocused) setCustomRowSelected(false); +} + +@Override +void gtk4_focus_enter_event(long controller, long event) { + // Highlight the focused row like a focused native row. A stray focus (e.g. after + // a submenu hides) is swept by Menu.clearStrayRowSelection and the pointer sync. + customRowFocused = true; + setCustomRowSelected(true); +} + +@Override +void gtk4_focus_leave_event(long controller, long event) { + customRowFocused = false; + /* Keep the highlight if the pointer is still over the row. */ + if (!customRowHovered) setCustomRowSelected(false); +} + +/** + * GTK4 only: shows or hides the custom row's selection highlight - as the + * "selected" state (a11y and the native theme rule) and as the CSS class the + * display provider paints where a workbench theme suppresses the state (see + * Display.ensureMenuIconRowCss). + */ +void setCustomRowSelected(boolean selected) { + if (customWidgetHandle == 0) return; + if (((GTK.gtk_widget_get_state_flags(customWidgetHandle) & GTK.GTK_STATE_FLAG_SELECTED) != 0) == selected) return; + if (selected) { + GTK.gtk_widget_set_state_flags(customWidgetHandle, GTK.GTK_STATE_FLAG_SELECTED, false); + GTK.gtk_widget_add_css_class(customWidgetHandle, Converter.javaStringToCString(CUSTOM_SELECTED_CLASS)); + } else { + GTK.gtk_widget_unset_state_flags(customWidgetHandle, GTK.GTK_STATE_FLAG_SELECTED); + GTK.gtk_widget_remove_css_class(customWidgetHandle, Converter.javaStringToCString(CUSTOM_SELECTED_CLASS)); + } + // A widget in a custom popover slot does not repaint on its own style change, so + // force it - the class change would otherwise stay invisible until a later redraw. + GTK.gtk_widget_queue_draw(customWidgetHandle); +} + +/** + * Updates the custom widget's label and accelerator sub-labels from {@link #text}. + * SWT menu text carries the accelerator display after a tab (e.g. "Run\tCtrl+F11"); + * a GtkModelButton renders that via its "accel" property, but our custom GtkButton + * must render it explicitly in a trailing, right-aligned label. + */ +private void updateCustomWidgetLabels() { + if (customLabelHandle == 0) return; + String full = text != null ? text : ""; + String label = full; + String accel = ""; + int tab = full.indexOf('\t'); + if (tab != -1) { + label = full.substring(0, tab); + accel = full.substring(tab + 1); + } + char[] chars = fixMnemonic(label); + GTK.gtk_label_set_text_with_mnemonic(customLabelHandle, Converter.wcsToMbcs(chars, true)); + if (customAccelHandle != 0) { + GTK.gtk_label_set_text(customAccelHandle, Converter.wcsToMbcs(accel, true)); + GTK.gtk_widget_set_visible(customAccelHandle, !accel.isEmpty()); + } +} + +private void destroyCustomMenuWidget() { + // Clear the "custom" attribute so the next model rebuild shows a normal model button + OS.g_menu_item_set_attribute_value(handle, Converter.javaStringToCString("custom"), 0); + detachCustomMenuWidget(); + if (customWidgetHandle != 0) { + display.removeWidget(customWidgetHandle); + OS.g_object_unref(customWidgetHandle); + } + customWidgetHandle = 0; + customImageHandle = 0; + customLabelHandle = 0; + customAccelHandle = 0; + customIndicatorHandle = 0; + customLeadingBoxHandle = 0; + customId = null; +} + +/** + * Detaches an injected custom widget from its GtkPopoverMenu slot via + * gtk_popover_menu_remove_child() before its last reference is dropped. Detaching + * first matters: unreffing while GTK still holds the widget would leave the popover + * pointing at freed memory. + */ +private void detachCustomMenuWidget() { + if (customWidgetHandle == 0) return; + if (GTK.gtk_widget_get_parent(customWidgetHandle) == 0) return; + /* Walk up from the widget itself; the cached popover handle can be stale after a rebuild. */ + long popover = GTK.gtk_widget_get_parent(customWidgetHandle); + while (popover != 0 && !GTK4.GTK_IS_POPOVER_MENU(popover)) { + popover = GTK.gtk_widget_get_parent(popover); + } + if (popover != 0) { + GTK4.gtk_popover_menu_remove_child(popover, customWidgetHandle); + } + /* Defensive: if no owning popover was found, at least unparent. */ + if (GTK.gtk_widget_get_parent(customWidgetHandle) != 0) { + GTK.gtk_widget_unparent(customWidgetHandle); + } +} + +/** + * Pushes this item's current GMenuItem attributes into the parent GMenu by + * removing and re-inserting it (GMenu snapshots attributes on insertion). Injection + * is held off meanwhile: the removal's synchronous "items-changed" re-enters wiring, + * and while this item is out every later position is shifted, so a nested + * position-based refresh would remove the wrong item. + */ +void refreshMenuModelGTK4() { + boolean wasMutating = display.menuModelMutating; + display.menuModelMutating = true; + try { + OS.g_menu_remove(section.getSectionHandle(), section.getItemPosition(this)); + OS.g_menu_insert_item(section.getSectionHandle(), section.getItemPosition(this), handle); + } finally { + display.menuModelMutating = wasMutating; + } +} + +/** + * Ensures this item's custom widget is embedded into its GtkPopoverMenu slot. Safe + * to call repeatedly (e.g. every time the menu is shown). + * + * A GTK "custom" id is single-use: destroying the placeholder (on any model + * remove+reinsert) unparents our widget without freeing the id, and reusing that id + * hits "Duplicate custom ID" with no new slot, so the row renders empty. Hence + * "widget has no parent" triggers a (re)embed under a FRESH id: set the id, refresh + * the model so a new placeholder materialises, then embed. + */ +void injectCustomWidgetGTK4() { + if (customWidgetHandle == 0 || customId == null) return; + if (display.menuModelMutating) return; + long popoverHandle = getParentPopoverHandle(); + if (popoverHandle == 0) return; + // Already embedded in a live slot: nothing to do. + if (GTK.gtk_widget_get_parent(customWidgetHandle) != 0) return; + // Fresh id and a new placeholder; the previous id is burned in GTK's custom_slots. + reassignCustomId(); + refreshMenuModelGTK4(); + boolean added = GTK4.gtk_popover_menu_add_child(popoverHandle, customWidgetHandle, + Converter.javaStringToCString(customId)); + if (!added) { + // No placeholder slot was created (e.g. a cascade item, whose submenu link + // wins over the custom attribute). Fall back to a plain GtkModelButton so the + // item stays visible (label only, no icon) rather than an empty placeholder. + fallbackToModelButtonGTK4(); + } +} + +/** + * Assigns a fresh "custom" id to this item and writes it onto the GMenuItem's + * "custom" attribute, at creation and whenever a stale id must be replaced. + */ +private void reassignCustomId() { + customId = "swt-menu-" + (++customIdSeq); + long variant = OS.g_variant_new_string(Converter.javaStringToCString(customId)); + OS.g_menu_item_set_attribute_value(handle, Converter.javaStringToCString("custom"), variant); + // g_menu_item_set_attribute_value sinks the floating ref; do not unref variant +} + +/** + * Reverts this item to plain GtkModelButton rendering after a failed custom-widget + * injection: clears the "custom" attribute, releases the widget we built, and + * re-inserts the model item so GtkMenuTracker rebuilds it as a normal (visible) + * button. Prevents an un-injectable item from showing as an empty placeholder slot. + */ +private void fallbackToModelButtonGTK4() { + OS.g_menu_item_set_attribute_value(handle, Converter.javaStringToCString("custom"), 0); + if (customWidgetHandle != 0) { + display.removeWidget(customWidgetHandle); + OS.g_object_unref(customWidgetHandle); + customWidgetHandle = 0; + customImageHandle = 0; + customLabelHandle = 0; + customAccelHandle = 0; + customIndicatorHandle = 0; + customLeadingBoxHandle = 0; + customId = null; + } + refreshMenuModelGTK4(); +} + +long getParentPopoverHandle() { + if ((parent.style & SWT.POP_UP) != 0) { + return parent.handle; + } + return parent.popoverHandle; +} + /** * Sets the receiver's pull down menu to the argument. * Only CASCADE menu items can have a @@ -1185,8 +1616,7 @@ public void setMenu (Menu menu) { OS.g_menu_item_set_submenu(handle, 0); } - OS.g_menu_remove(section.getSectionHandle(), section.getItemPosition(this)); - OS.g_menu_insert_item(section.getSectionHandle(), section.getItemPosition(this), handle); + refreshMenuModelGTK4(); /* * If a DROP_DOWN is attached while its parent is already mapped (contributions @@ -1332,8 +1762,14 @@ public void setText (String string) { GTK.gtk_accelerator_name(maskKeysym.keysym, maskKeysym.mask) ); } - OS.g_menu_remove(section.getSectionHandle(), section.getItemPosition(this)); - OS.g_menu_insert_item(section.getSectionHandle(), section.getItemPosition(this), handle); + if (customWidgetHandle != 0) { + // The custom widget renders its own label; do not refresh the model position. + // GTK never forgets a registered "custom" id, so refreshing again here would + // create a duplicate, orphaned placeholder and drop the item from view. + updateCustomWidgetLabels(); + } else { + refreshMenuModelGTK4(); + } } else { if (labelHandle != 0 && GTK.GTK_IS_LABEL (labelHandle)) { GTK.gtk_label_set_text_with_mnemonic (labelHandle, buffer); 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 5ea3cbc497d..c8a512590b3 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 @@ -29,7 +29,6 @@ import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** @@ -208,7 +207,6 @@ public void test_setEnabledZ_cascade() { cascadeItem.dispose(); } -@Tag("gtk4-todo") @Override @Test public void test_setImageLorg_eclipse_swt_graphics_Image() {