From b3a108702e9143f0aef6798c785cb519c0442f50 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: Wed, 15 Jul 2026 19:35:41 +0300 Subject: [PATCH] [Gtk4] Support MenuItem images on GTK4 GtkModelButton (GTK4's native menu row) never shows both an icon and text, so PUSH 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 items. CASCADE, CHECK and RADIO items keep the native GtkModelButton, since GTK ties their submenu link / selection indicator to it and there is no way to reproduce that in a custom widget. Injection is self-healing: GTK's "custom" ids are single-use once their placeholder slot is destroyed (e.g. by menu repopulation), so a detached widget is re-injected under a fresh id rather than reusing the stale one, which previously left dynamically-rebuilt items (like Run/Debug) invisible. Fixes #2511 Co-Authored-By: Claude Sonnet 5 --- .../Eclipse SWT PI/gtk/library/gtk4.c | 30 ++ .../Eclipse SWT PI/gtk/library/gtk4_stats.h | 2 + .../Eclipse SWT PI/gtk/library/os.c | 14 + .../Eclipse SWT PI/gtk/library/os_stats.h | 1 + .../gtk/org/eclipse/swt/internal/gtk/OS.java | 6 + .../org/eclipse/swt/internal/gtk4/GTK4.java | 13 + .../gtk/org/eclipse/swt/widgets/Menu.java | 14 + .../gtk/org/eclipse/swt/widgets/MenuItem.java | 256 +++++++++++++++++- ...Test_org_eclipse_swt_widgets_MenuItem.java | 2 - 9 files changed, 329 insertions(+), 9 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 0d22afe950..75e8d47877 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 @@ -796,6 +796,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) @@ -2260,6 +2274,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) 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 6766ff954b..00d55fcb1b 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 @@ -83,6 +83,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, @@ -185,6 +186,7 @@ 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, 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 24178bd35c..3a11b3f46f 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 @@ -11984,6 +11984,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) 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 f30ca89d0a..c9585a87c9 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 @@ -969,6 +969,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, 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 aa70f5a3f2..98340cf182 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 @@ -2416,6 +2416,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 ba03262b09..a67863b337 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 @@ -75,6 +75,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 *) @@ -588,6 +595,12 @@ 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 model cast=(GMenuModel *) * @param flags cast=(GtkPopoverMenuFlags) 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 3dc45318bb..781810c48a 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 @@ -864,7 +864,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); } } @@ -886,6 +888,7 @@ private void connectDropDownMenuSignals() { 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); + menuItem.menu.injectCustomMenuIcons(); /* * Also connect SHOW/HIDE signals for nested CASCADE submenus. */ @@ -917,6 +920,7 @@ private void connectCascadeSubMenuSignals(Menu menu, long parentPopoverHandle) { 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); + item.menu.injectCustomMenuIcons(); // Recurse to handle further nested CASCADE submenus connectCascadeSubMenuSignals(item.menu); } @@ -924,6 +928,15 @@ private void connectCascadeSubMenuSignals(Menu menu, long parentPopoverHandle) { } } +void injectCustomMenuIcons() { + if (items == null) return; + for (MenuItem item : items) { + if ((item.style & SWT.SEPARATOR) == 0 && item.customWidgetHandle != 0) { + item.injectCustomWidgetGTK4(); + } + } +} + private long findNestedPopoverForModel(long parentWidget, long targetModel) { if (parentWidget == 0 || targetModel == 0) return 0; long child = GTK4.gtk_widget_get_first_child(parentWidget); @@ -985,6 +998,7 @@ long gtk_show (long widget) { sendEvent (SWT.Show); /* Retry cascade submenu signal hookup 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) { 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 e98a8b0229..2b40e8331f 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 @@ -69,6 +69,12 @@ public class MenuItem extends Item { long modelHandle, actionHandle, shortcutHandle; Section section; String actionName; + /** GTK4 only: custom widget for icon+text display in GtkPopoverMenu */ + long customWidgetHandle, customImageHandle, customLabelHandle, customAccelHandle; + String customId; + boolean customWidgetInjected; + private boolean customWidgetInjecting; + private static int customIdSeq = 0; /** * Constructs a new instance of this class given its parent @@ -517,7 +523,7 @@ public boolean getEnabled () { checkWidget(); if (GTK.GTK4) { - if ((style & SWT.CASCADE) != 0) { + if ((style & SWT.CASCADE) != 0 || actionHandle == 0) { return true; } @@ -744,6 +750,15 @@ void releaseWidget() { if (GTK.GTK4) { if (parent.actionGroup != 0 && actionName != null) OS.g_action_map_remove_action(parent.actionGroup, Converter.javaStringToCString(actionName)); + if (customWidgetHandle != 0) { + detachCustomMenuWidget(); + OS.g_object_unref(customWidgetHandle); + customWidgetHandle = 0; + customImageHandle = 0; + customLabelHandle = 0; + customAccelHandle = 0; + customId = null; + } } else { long accelGroup = getAccelGroup(); if (accelGroup != 0) removeAccelerator(accelGroup); @@ -1021,7 +1036,7 @@ public void setID (int id) { *

* Note: This operation is a HINT and is not supported on * platforms that do not have this concept (for example, Windows NT). - * Some platforms (such as GTK3) support images alongside check boxes. + * Some platforms (such as GTK3 and GTK4) support images alongside check boxes. *

* * @param image the image to display on the receiver (may be null) @@ -1033,9 +1048,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; @@ -1046,6 +1058,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 (); @@ -1091,6 +1107,225 @@ private void _setImage (Image image) { } } +private void _setImageGTK4(Image image) { + // Only plain PUSH items use the custom icon+label widget. Other styles must keep + // their default GtkModelButton rendering: + // - CASCADE: GtkMenuSectionBox checks the submenu link before the "custom" attribute, + // so a cascade item never gets a placeholder slot for a custom widget. + // - CHECK / RADIO: the custom GtkButton cannot draw the check/radio selection + // indicator that GtkModelButton derives from the action state, so using it would + // hide the item's selection. GtkModelButton hides the icon when it has text anyway, + // so these items are label + indicator only (no icon) on GTK4. + if ((style & (SWT.CASCADE | SWT.CHECK | 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) { + // Attempt injection now in case the popover already exists (item + // added to an already-open menu). If it is not ready yet, injection + // is retried - and self-heals - from Menu.injectCustomMenuIcons() + // when the menu is shown. + injectCustomWidgetGTK4(); + } + } + } + } else { + if (customWidgetHandle != 0) { + destroyCustomMenuWidget(); + refreshMenuModelGTK4(); + } + } +} + +private void createCustomMenuWidget() { + customWidgetHandle = GTK.gtk_button_new(); + OS.g_object_ref_sink(customWidgetHandle); + GTK.gtk_widget_add_css_class(customWidgetHandle, Converter.javaStringToCString("flat")); + + long hbox = GTK.gtk_box_new(GTK.GTK_ORIENTATION_HORIZONTAL, 6); + customImageHandle = GTK.gtk_image_new(); + GTK4.gtk_box_append(hbox, customImageHandle); + + customLabelHandle = GTK.gtk_label_new_with_mnemonic(null); + GTK.gtk_label_set_xalign(customLabelHandle, 0.0f); + // Expand the label so the accelerator label is pushed to the trailing edge, + // matching the layout of a normal GtkModelButton menu row. + GTK.gtk_widget_set_hexpand(customLabelHandle, true); + GTK4.gtk_box_append(hbox, customLabelHandle); + + customAccelHandle = GTK.gtk_label_new(null); + GTK.gtk_label_set_xalign(customAccelHandle, 1.0f); + GTK.gtk_widget_add_css_class(customAccelHandle, Converter.javaStringToCString("dim-label")); + 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)); + } + + // Mark this GMenuItem slot as "custom" so GtkPopoverMenu creates a placeholder. + // injectCustomWidgetGTK4() reassigns a fresh id before actually embedding. + reassignCustomId(); +} + +/** + * 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) { + OS.g_object_unref(customWidgetHandle); + } + customWidgetHandle = 0; + customImageHandle = 0; + customLabelHandle = 0; + customAccelHandle = 0; + customId = null; + customWidgetInjected = false; +} + +/** + * Cleanly unparents an injected custom widget from its GtkPopoverMenu placeholder + * slot before the widget's last reference is + * dropped. GTK's GtkMenuSectionBox never removes the placeholder's "custom" id + * from its internal hash table, and the gizmo placeholder holds its own + * parent-child reference on the child widget: unref'ing the widget without first + * unparenting it here would drop what may be its only remaining reference while + * GTK still has it linked into the popover's widget tree, corrupting layout and + * producing "gtk_widget_unparent: assertion 'GTK_IS_WIDGET (widget)' failed". + */ +private void detachCustomMenuWidget() { + if (customWidgetHandle != 0) { + // Equivalent to gtk_popover_menu_remove_child(): the widget is parented into + // its placeholder gizmo slot, so unparent it while it is still valid. Guard on + // having a parent to avoid the "gtk_widget_unparent: assertion 'GTK_IS_WIDGET + // (widget)' failed" that fires when unparenting an already-detached widget. + if (GTK.gtk_widget_get_parent(customWidgetHandle) != 0) { + GTK.gtk_widget_unparent(customWidgetHandle); + } + customWidgetInjected = false; + } +} + +void refreshMenuModelGTK4() { + OS.g_menu_remove(section.getSectionHandle(), section.getItemPosition(this)); + OS.g_menu_insert_item(section.getSectionHandle(), section.getItemPosition(this), handle); +} + +/** + * Ensures this item's custom icon+label widget is embedded into its GtkPopoverMenu + * placeholder slot. Safe to call repeatedly (e.g. every time the menu is shown). + * + * A GTK "custom" id is single-use: GtkMenuSectionBox keeps every registered id in an + * append-only hash table, and destroying the placeholder (which happens whenever the + * model item is removed+reinserted - by separator/section restructuring, cascade + * changes, etc.) unparents our widget without freeing the id. Reinserting under the + * same id then hits "Duplicate custom ID" and no new slot is created, orphaning the + * widget so the row renders empty. We therefore treat "widget currently has no + * parent" as the trigger to (re)establish it under a FRESH id: set the new id, refresh + * the model so GtkMenuTracker materialises a new placeholder, then embed the widget. + */ +void injectCustomWidgetGTK4() { + if (customWidgetInjecting) return; + if (customWidgetHandle == 0 || customId == null) 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; + customWidgetInjecting = true; + try { + // Mint a fresh id and rematerialise the placeholder. The previous id (if any) + // is now burned in GTK's custom_slots and cannot be reused. + reassignCustomId(); + refreshMenuModelGTK4(); + boolean added = GTK4.gtk_popover_menu_add_child(popoverHandle, customWidgetHandle, + Converter.javaStringToCString(customId)); + customWidgetInjected = added; + 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(); + } + } finally { + customWidgetInjecting = false; + } +} + +/** + * Assigns a fresh, never-before-used "custom" id to this item and writes it onto the + * GMenuItem's "custom" attribute. Used both at creation and whenever a stale id must + * be replaced (see {@link #injectCustomWidgetGTK4()}). + */ +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) { + OS.g_object_unref(customWidgetHandle); + customWidgetHandle = 0; + customImageHandle = 0; + customLabelHandle = 0; + customAccelHandle = 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 @@ -1281,8 +1516,15 @@ 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 { + OS.g_menu_remove(section.getSectionHandle(), section.getItemPosition(this)); + OS.g_menu_insert_item(section.getSectionHandle(), section.getItemPosition(this), handle); + } } 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 1db0b857ec..d0d8cb9cfa 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; /** @@ -195,7 +194,6 @@ public void test_setEnabledZ() { assertFalse(menuItem.getEnabled()); } -@Tag("gtk4-todo") @Override @Test public void test_setImageLorg_eclipse_swt_graphics_Image() {