Commit: patch 9.2.0719: GTK4: default menu is lacking

1 view
Skip to first unread message

Christian Brabandt

unread,
2:30 PM (8 hours ago) 2:30 PM
to vim...@googlegroups.com
patch 9.2.0719: GTK4: default menu is lacking

Commit: https://github.com/vim/vim/commit/037a19e1c1f737abc560ffc1d2edf2b535194816
Author: Foxe Chen <chen...@gmail.com>
Date: Wed Jun 24 18:19:53 2026 +0000

patch 9.2.0719: GTK4: default menu is lacking

Problem: GTK4: default menu is lacking: accelerator
text is not shown, mnemonics and 'winaltkeys' do not work
Solution: Replace the GMenuModel-based menus with a custom widget set
(VimMenuBar, VimMenuBarItem, VimMenu, VimMenuItem) in a new
gui_gtk4_menu.c, modelled on the GTK3 menu bar: show accelerator
text, support mnemonics and 'winaltkeys', add keyboard
navigation, instant tooltips, and the popup and F10 menus, and
implement the previously stubbed menu functions (Foxe Chen).

closes: #20593

Signed-off-by: Foxe Chen <chen...@gmail.com>
Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/Filelist b/Filelist
index 019fa1aee..c073e1ad6 100644
--- a/Filelist
+++ b/Filelist
@@ -518,6 +518,8 @@ SRC_UNIX = \
src/gui_gtk4_da.h \
src/gui_gtk4_tb.c \
src/gui_gtk4_tb.h \
+ src/gui_gtk4_menu.c \
+ src/gui_gtk4_menu.h \
src/gui_gtk_res.xml \
src/gui_motif.c \
src/gui_xmdlg.c \
diff --git a/runtime/doc/gui_x11.txt b/runtime/doc/gui_x11.txt
index 2e558d8a0..8a084505e 100644
--- a/runtime/doc/gui_x11.txt
+++ b/runtime/doc/gui_x11.txt
@@ -1,4 +1,4 @@
-*gui_x11.txt* For Vim version 9.2. Last change: 2026 Jun 13
+*gui_x11.txt* For Vim version 9.2. Last change: 2026 Jun 24


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -782,5 +782,16 @@ Most newer applications will provide their current selection via PRIMARY ("*)
and use CLIPBOARD ("+) for cut/copy/paste operations. You thus have access to
both by choosing to use either of the "* or "+ registers.

+ *gtk4-menu-navigation*
+In the GTK 4 GUI, you may also navigate the menu items with these keyboard
+mappings:
+ key meaning ~
+ <Tab> <Down> Go to next item
+ <S-Tab> <Up> Go to previous item
+ <Left> Go to parent submenu
+ <Right> Go to current item's submenu
+ <C-Left> Go to next menu bar item
+ <C-Right> Go to previous menu bar item
+

vim:tw=78:sw=4:ts=8:noet:ft=help:norl:
diff --git a/runtime/doc/tags b/runtime/doc/tags
index 11ae058bb..3abd3e035 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -8260,6 +8260,7 @@ gtk-css gui_x11.txt /*gtk-css*
gtk-tooltip-colors gui_x11.txt /*gtk-tooltip-colors*
gtk3-slow gui_x11.txt /*gtk3-slow*
gtk4-hwaccel gui_x11.txt /*gtk4-hwaccel*
+gtk4-menu-navigation gui_x11.txt /*gtk4-menu-navigation*
gtk4-slow gui_x11.txt /*gtk4-slow*
gu change.txt /*gu*
gugu change.txt /*gugu*
diff --git a/src/Makefile b/src/Makefile
index 89e023c9d..e0f7057b8 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -1244,6 +1244,7 @@ GTK4_SRC = gui.c gui_gtk4.c gui_gtk4_f.c \
gui_gtk4_da.c \
gui_beval.o \
gui_gtk4_tb.c \
+ gui_gtk4_menu.c \
$(GRESOURCE_SRC)
GTK4_OBJ = objects/gui.o objects/gui_gtk4.o \
objects/gui_gtk4_f.o \
@@ -1251,6 +1252,7 @@ GTK4_OBJ = objects/gui.o objects/gui_gtk4.o \
objects/gui_gtk4_da.o \
objects/gui_beval.o \
objects/gui_gtk4_tb.o \
+ objects/gui_gtk4_menu.o \
$(GRESOURCE_OBJ)
GTK4_DEFS = -DFEAT_GUI_GTK $(NARROW_PROTO)
GTK4_IPATH = $(GUI_INC_LOC)
@@ -1320,7 +1322,7 @@ HAIKUGUI_TESTTARGET = gui
HAIKUGUI_BUNDLE =

# All GUI files
-ALL_GUI_SRC = gui.c gui_gtk.c gui_gtk_f.c gui_gtk4.c gui_gtk4_f.c gui_gtk4_cb.c gui_gtk4_da.c gui_gtk4_tb.c gui_motif.c gui_xmdlg.c gui_xmebw.c gui_gtk_x11.c gui_x11.c gui_haiku.cc
+ALL_GUI_SRC = gui.c gui_gtk.c gui_gtk_f.c gui_gtk4.c gui_gtk4_f.c gui_gtk4_cb.c gui_gtk4_da.c gui_gtk4_tb.c gui_gtk4_menu.c gui_motif.c gui_xmdlg.c gui_xmebw.c gui_gtk_x11.c gui_x11.c gui_haiku.cc
ALL_GUI_PRO = proto/gui.pro proto/gui_gtk.pro proto/gui_gtk4.pro proto/gui_motif.pro proto/gui_xmdlg.pro proto/gui_gtk_x11.pro proto/gui_x11.pro proto/gui_w32.pro proto/gui_photon.pro

# }}}
@@ -3421,6 +3423,9 @@ objects/gui_gtk4_da.o: gui_gtk4_da.c
objects/gui_gtk4_tb.o: gui_gtk4_tb.c
$(CCC) -o $@ gui_gtk4_tb.c

+objects/gui_gtk4_menu.o: gui_gtk4_menu.c
+ $(CCC) -o $@ gui_gtk4_menu.c
+

objects/gui_haiku.o: gui_haiku.cc
$(CCC) -o $@ gui_haiku.cc
@@ -4518,7 +4523,7 @@ objects/gui_gtk4.o: auto/osdef.h gui_gtk4.c vim.h protodef.h auto/config.h featu
structs.h regexp.h gui.h libvterm/include/vterm.h \
libvterm/include/vterm_keycodes.h alloc.h ex_cmds.h spell.h proto.h \
globals.h errors.h gui_gtk4_f.h auto/gui_gtk_gresources.h \
- gui_gtk4_cb.h gui_gtk4_da.h gui_gtk4_tb.h
+ gui_gtk4_cb.h gui_gtk4_da.h gui_gtk4_tb.h gui_gtk4_menu.h
objects/gui_gtk4_f.o: auto/osdef.h gui_gtk4_f.c vim.h protodef.h auto/config.h feature.h \
os_unix.h ascii.h keymap.h termdefs.h macros.h option.h \
beval.h structs.h regexp.h gui.h \
@@ -4539,6 +4544,11 @@ objects/gui_gtk4_tb.o: auto/osdef.h gui_gtk4_tb.c vim.h protodef.h auto/config.h
beval.h structs.h regexp.h gui.h \
libvterm/include/vterm.h libvterm/include/vterm_keycodes.h alloc.h \
ex_cmds.h spell.h proto.h globals.h errors.h gui_gtk4_tb.h
+objects/gui_gtk4_menu.o: auto/osdef.h gui_gtk4_menu.c vim.h protodef.h auto/config.h feature.h \
+ os_unix.h ascii.h keymap.h termdefs.h macros.h option.h \
+ beval.h structs.h regexp.h gui.h \
+ libvterm/include/vterm.h libvterm/include/vterm_keycodes.h alloc.h \
+ ex_cmds.h spell.h proto.h globals.h errors.h gui_gtk4_menu.h
objects/gui_gtk_f.o: auto/osdef.h gui_gtk_f.c vim.h protodef.h auto/config.h feature.h \
os_unix.h ascii.h keymap.h termdefs.h macros.h option.h \
beval.h structs.h regexp.h gui.h \
diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c
index fe9381f2f..b5f1f93b4 100644
--- a/src/gui_gtk4.c
+++ b/src/gui_gtk4.c
@@ -36,6 +36,9 @@
#ifdef FEAT_TOOLBAR
# include "gui_gtk4_tb.h"
#endif
+#ifdef FEAT_MENU
+# include "gui_gtk4_menu.h"
+#endif

/*
* Geometry string parser, replacing XParseGeometry to remove X11 dependency.
@@ -126,9 +129,6 @@ static int last_shape = 0;

#define DEFAULT_FONT "Monospace 10"

-// Menu action group for GMenu-based menus
-static GSimpleActionGroup *menu_action_group = NULL;
-
// Cursor blinking state
static enum {
BLINK_NONE,
@@ -283,9 +283,6 @@ static void enter_notify_event(GtkEventControllerMotion *controller, double x, d
static gboolean scroll_event(GtkEventControllerScroll *controller, double dx, double dy, gpointer data);
static void focus_in_event(GtkEventControllerFocus *controller, gpointer data);
static void focus_out_event(GtkEventControllerFocus *controller, gpointer data);
-#ifdef FEAT_MENU
-static gboolean menubar_popover_closed_hook(GSignalInvocationHint *ihint, guint n_param_values, const GValue *param_values, gpointer data);
-#endif
#ifdef FEAT_DND
static gboolean drop_cb(GtkDropTarget *target, const GValue *value, double x, double y, gpointer data);
#endif
@@ -293,7 +290,7 @@ static gboolean drop_cb(GtkDropTarget *target, const GValue *value, double x, do
static void tabline_enter_cb(GtkEventController *controller, double x, double y, void *udata);
static void on_select_tab(GtkNotebook *notebook, gpointer *page, gint idx, gpointer data);
static void on_tab_reordered(GtkNotebook *notebook, gpointer *page, gint idx, gpointer data);
-static GMenu *create_tabline_popup_menu(GActionGroup **agroup_store);
+static VimMenu *create_tabline_popup_menu(void);
static void tabline_menu_press_event(GtkGestureClick *gesture, int n_press, double x, double y, GtkWidget *popover);
#endif
static void mainwin_destroy_cb(GObject *object, gpointer data);
@@ -481,30 +478,10 @@ gui_mch_init(void)
gtk_window_set_child(GTK_WINDOW(gui.mainwin), vbox);

#ifdef FEAT_MENU
- {
- GMenu *gmenu = g_menu_new();
- gui.menubar = gtk_popover_menu_bar_new_from_model(
- G_MENU_MODEL(gmenu));
- g_object_set_data_full(G_OBJECT(gui.menubar), "vim-gmenu",
- gmenu, g_object_unref);
- gtk_widget_set_name(gui.menubar, "vim-menubar");
- gtk_widget_set_visible(gui.menubar, FALSE);
- gtk_box_append(GTK_BOX(vbox), gui.menubar);
- }
- // Return keyboard focus to the drawing area when a menubar popover
- // closes (issue #20274). GtkPopoverMenuBar owns its popovers
- // privately, so attach via an emission hook on GtkPopover::closed
- // and filter for popovers under our menubar inside the callback.
- {
- GTypeClass *cls = g_type_class_ref(GTK_TYPE_POPOVER);
- guint sig_id = g_signal_lookup("closed", GTK_TYPE_POPOVER);
-
- if (sig_id != 0)
- g_signal_add_emission_hook(sig_id, 0,
- menubar_popover_closed_hook, NULL, NULL);
- if (cls != NULL)
- g_type_class_unref(cls);
- }
+ gui.menubar = vim_menu_bar_new();
+ gtk_widget_set_name(gui.menubar, "vim-menubar");
+ gtk_widget_set_visible(gui.menubar, FALSE);
+ gtk_box_append(GTK_BOX(vbox), gui.menubar);
#endif

#ifdef FEAT_TOOLBAR
@@ -544,28 +521,25 @@ gui_mch_init(void)
// Create right click popup menu for tabline
{
GtkGesture *click;
- GActionGroup *agroup;
- GMenu *menu;
- GtkWidget *popover;
+ VimMenu *menu;

click = gtk_gesture_click_new();
- menu = create_tabline_popup_menu(&agroup);
- popover = gtk_popover_menu_new_from_model(G_MENU_MODEL(menu));
- g_object_unref(menu);
+ menu = create_tabline_popup_menu();

- gtk_widget_set_parent(popover, gui.tabline);
- g_object_set_data(G_OBJECT(gui.tabline), "menu", popover);
- gtk_widget_insert_action_group(gui.tabline, "tabline", agroup);
- g_object_unref(agroup);
+ gtk_widget_set_parent(GTK_WIDGET(menu), gui.tabline);
+ g_object_set_data(G_OBJECT(gui.tabline), "menu", menu);

- gtk_popover_set_has_arrow(GTK_POPOVER(popover), FALSE);
- gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
+ gtk_popover_set_has_arrow(GTK_POPOVER(menu), FALSE);
+ gtk_popover_set_position(GTK_POPOVER(menu), GTK_POS_BOTTOM);
+ // Make popover start at top left corner
+ gtk_widget_set_halign(GTK_WIDGET(menu), GTK_ALIGN_START);

// Listen for anny mouse button
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click), 0);

g_signal_connect_object(click, "pressed",
- G_CALLBACK(tabline_menu_press_event), popover, G_CONNECT_DEFAULT);
+ G_CALLBACK(tabline_menu_press_event),
+ menu, G_CONNECT_DEFAULT);
gtk_widget_add_controller(gui.tabline, GTK_EVENT_CONTROLLER(click));
}
#endif
@@ -812,6 +786,19 @@ gui_mch_exit(int rc UNUSED)
// Make sure to destroy popover used for balloon eval, or we will get a
// warning from GTK that the draw area still has children left.
gui_mch_destroy_beval_area(balloonEval);
+#endif
+#ifdef FEAT_MENU
+ // Make sure to unparent any popover menus
+ {
+ vimmenu_T *menu;
+
+ FOR_ALL_MENUS(menu)
+ {
+ if ((menu->name[0] == ']' || menu_is_popup(menu->name))
+ && menu->submenu_id != NULL)
+ gtk_widget_unparent(menu->submenu_id);
+ }
+ }
#endif
gtk_window_destroy(GTK_WINDOW(gui.mainwin));
}
@@ -1939,6 +1926,19 @@ key_press_event(GtkEventControllerKey *controller UNUSED,
state |= GDK_SHIFT_MASK;
}

+#ifdef FEAT_MENU
+ // If there is a menu and 'wak' is "yes", or 'wak' is "menu" and the key
+ // is a menu shortcut, we ignore everything with the ALT modifier.
+ if ((state & GDK_ALT_MASK)
+ && gui.menu_is_active
+ && (*p_wak == 'y'
+ || (*p_wak == 'm'
+ && len == 1
+ && gui_is_menu_shortcut(string[0]))))
+ // Tell GTK we have not handled the key (so it can handle it).
+ return FALSE;
+#endif
+
// Check for special keys
if (len == 0 || len == 1)
{
@@ -2227,6 +2227,10 @@ motion_notify_event(GtkEventControllerMotion *controller UNUSED,

prev_mouse_x = x;
prev_mouse_y = y;
+
+ // Make sure keyboard input goes to the drawing area. Fixes issues with menu
+ // still being focused.
+ gtk_widget_grab_focus(gui.drawarea);
}

static void
@@ -2237,8 +2241,7 @@ enter_notify_event(GtkEventControllerMotion *controller UNUSED,
prev_mouse_y = y;

// Make sure keyboard input goes to the drawing area.
- if (!gtk_widget_has_focus(gui.drawarea))
- gtk_widget_grab_focus(gui.drawarea);
+ gtk_widget_grab_focus(gui.drawarea);
}

static gboolean
@@ -2300,48 +2303,6 @@ focus_out_event(GtkEventControllerFocus *controller UNUSED,
}
}

-#ifdef FEAT_MENU
- static gboolean
-grab_drawarea_focus_idle(gpointer data UNUSED)
-{
- if (gui.drawarea != NULL && !gtk_widget_has_focus(gui.drawarea))
- gtk_widget_grab_focus(gui.drawarea);
- return G_SOURCE_REMOVE;
-}
-
- static gboolean
-menubar_popover_closed_hook(GSignalInvocationHint *ihint UNUSED,
- guint n_param_values, const GValue *param_values,
- gpointer data UNUSED)
-{
- GObject *obj;
- GtkWidget *popover;
- GtkWidget *parent;
-
- if (n_param_values < 1 || gui.menubar == NULL || gui.drawarea == NULL)
- return TRUE;
- obj = g_value_get_object(&param_values[0]);
- if (!GTK_IS_POPOVER(obj))
- return TRUE;
- popover = GTK_WIDGET(obj);
-
- // Only react to popovers that descend from the menubar.
- for (parent = gtk_widget_get_parent(popover);
- parent != NULL;
- parent = gtk_widget_get_parent(parent))
- {
- if (parent != gui.menubar)
- continue;
- // Defer the grab to the next main loop iteration; calling it
- // synchronously while GTK is still completing the popover close
- // has no effect (issue #20274).
- g_idle_add(grab_drawarea_focus_idle, NULL);
- break;
- }
- return TRUE; // keep the emission hook installed
-}
-#endif
-
static void
drawarea_realize_cb(GtkWidget *widget UNUSED, gpointer data UNUSED)
{
@@ -2818,6 +2779,7 @@ gui_mch_enable_scrollbar(scrollbar_T *sb, int flag)
gtk_widget_set_visible(sb->id, flag);
}

+#if defined(FEAT_MENU)
/*
* ============================================================
* Menu stubs
@@ -2827,56 +2789,22 @@ gui_mch_enable_scrollbar(scrollbar_T *sb, int flag)
void
gui_mch_menu_grey(vimmenu_T *menu, int grey)
{
- if (menu->id == NULL || menu_action_group == NULL)
- return;
-
- // For toolbar items, use gtk_widget_set_sensitive
- if (menu->parent != NULL && menu_is_toolbar(menu->parent->name))
- {
- if (menu->id != (GtkWidget *)1)
- gtk_widget_set_sensitive(menu->id, !grey);
+ if (menu->id == NULL)
return;
- }
-
- // For menu items, enable/disable the GSimpleAction
- if (menu->label != NULL)
- {
- GAction *action = g_action_map_lookup_action(
- G_ACTION_MAP(menu_action_group),
- (const char *)menu->label);
- if (action != NULL)
- g_simple_action_set_enabled(G_SIMPLE_ACTION(action), !grey);
- }
+ gtk_widget_set_sensitive(menu->id, !grey);
+ gui_mch_update();
}

-#if defined(FEAT_MENU)
/*
* Make menu item hidden or not hidden.
*/
void
gui_mch_menu_hidden(vimmenu_T *menu, int hidden)
{
- // GMenu-based menu items have no real widget, only the (GtkWidget *)1
- // marker; they cannot be toggled via the widget API.
- if (menu->id == NULL || menu->id == (GtkWidget *)1)
+ if (menu->id == NULL)
return;
-
- if (hidden)
- {
- if (gtk_widget_get_visible(menu->id))
- {
- gtk_widget_set_visible(menu->id, FALSE);
- gui_mch_update();
- }
- }
- else
- {
- if (!gtk_widget_get_visible(menu->id))
- {
- gtk_widget_set_visible(menu->id, TRUE);
- gui_mch_update();
- }
- }
+ gtk_widget_set_visible(menu->id, !hidden);
+ gui_mch_update();
}

void
@@ -3053,52 +2981,34 @@ on_tab_reordered(
* Handle selecting an item in the tab line popup menu.
*/
static void
-tabline_menu_action_cb(
- GSimpleAction *action UNUSED,
- GVariant *parameter UNUSED,
- void *udata)
+tabline_menu_event_cb(VimMenuItem *item, VimMenuItemEvent event, void *udata)
{
- send_tabline_menu_event(tabpage_hover, GPOINTER_TO_INT(udata));
+ if (event == VIM_MENU_ITEM_CLICKED)
+ send_tabline_menu_event(tabpage_hover, GPOINTER_TO_INT(udata));
}

static void
-add_tabline_menu_item(
- GMenu *gmenu,
- GActionMap *amap,
- const char *name,
- const char *action,
- int resp)
+add_tabline_menu_item(VimMenu *menu, const char *name, int resp)
{
- GSimpleAction *act = g_simple_action_new(action, NULL);
- char detailed[32];
+ VimMenuItem *item = VIM_MENU_ITEM(vim_menu_item_new(name,
+ tabline_menu_event_cb, GINT_TO_POINTER(resp)));

- g_signal_connect(act, "activate", G_CALLBACK(tabline_menu_action_cb),
- GINT_TO_POINTER(resp));
- g_action_map_add_action(amap, G_ACTION(act));
- g_object_unref(act);
-
- vim_snprintf(detailed, sizeof(detailed), "tabline.%s", action);
- g_menu_append(gmenu, name, detailed);
+ vim_menu_insert_item(menu, item, -1);
}

/*
* Create a menu for the tab line.
*/
- static GMenu *
-create_tabline_popup_menu(GActionGroup **agroup_store)
+ static VimMenu *
+create_tabline_popup_menu(void)
{
- GMenu *gmenu = g_menu_new();
- GSimpleActionGroup *agroup = g_simple_action_group_new();
+ VimMenu *menu = VIM_MENU(vim_menu_new());

- add_tabline_menu_item(gmenu, G_ACTION_MAP(agroup),
- _("Close Tab"), "close-tab", TABLINE_MENU_CLOSE);
- add_tabline_menu_item(gmenu, G_ACTION_MAP(agroup),
- _("New Tab"), "new-tab", TABLINE_MENU_NEW);
- add_tabline_menu_item(gmenu, G_ACTION_MAP(agroup),
- _("Open Tab..."), "open-tab", TABLINE_MENU_OPEN);
+ add_tabline_menu_item(menu, _("Close Tab"), TABLINE_MENU_CLOSE);
+ add_tabline_menu_item(menu, _("New Tab"), TABLINE_MENU_NEW);
+ add_tabline_menu_item(menu, _("Open Tab..."), TABLINE_MENU_OPEN);

- *agroup_store = G_ACTION_GROUP(agroup);
- return gmenu;
+ return menu;
}

static void
@@ -3826,43 +3736,92 @@ gui_get_x11_windis(Window *win UNUSED, Display **dis UNUSED)
}

#if defined(FEAT_MENU)
- void
-gui_gtk_set_mnemonics(int enable UNUSED)
+/*
+ * Translate Vim's mnemonic tagging to GTK+ style and convert to UTF-8
+ * if necessary. The caller must vim_free() the returned string.
+ *
+ * Input Output
+ * _ __
+ * && &
+ * & _ stripped if use_mnemonic == FALSE
+ * <Tab> end of menu label text
+ */
+ static char_u *
+translate_mnemonic_tag(char_u *name, int use_mnemonic)
{
- // TODO: implement?
-}
+ char_u *buf;
+ char_u *psrc;
+ char_u *pdest;
+ int n_underscores = 0;

- static void
-popupmenu_closed_cb(GtkPopover *popover, gpointer data UNUSED)
-{
- gtk_widget_unparent(GTK_WIDGET(popover));
- if (gui.drawarea != NULL)
- gtk_widget_queue_draw(gui.drawarea);
-}
+ name = CONVERT_TO_UTF8(name);
+ if (name == NULL)
+ return NULL;

-typedef struct {
- GtkPopover *popover;
- vimmenu_T *menu;
-} popup_item_data_T;
+ for (psrc = name; *psrc != NUL && *psrc != TAB; ++psrc)
+ if (*psrc == '_')
+ ++n_underscores;

- static void
-popup_item_clicked_cb(GtkButton *button UNUSED, gpointer data)
+ buf = alloc(psrc - name + n_underscores + 1);
+ if (buf != NULL)
+ {
+ pdest = buf;
+ for (psrc = name; *psrc != NUL && *psrc != TAB; ++psrc)
+ {
+ if (*psrc == '_')
+ {
+ *pdest++ = '_';
+ *pdest++ = '_';
+ }
+ else if (*psrc != '&')
+ {
+ *pdest++ = *psrc;
+ }
+ else if (*(psrc + 1) == '&')
+ {
+ *pdest++ = *psrc++;
+ }
+ else if (use_mnemonic)
+ {
+ *pdest++ = '_';
+ }
+ }
+ *pdest = NUL;
+ }
+
+ CONVERT_TO_UTF8_FREE(name);
+ return buf;
+}
+
+/*
+ * Enable or disable accelerators for the toplevel menus.
+ */
+ void
+gui_gtk_set_mnemonics(int enable)
{
- popup_item_data_T *d = data;
+ vimmenu_T *menu;
+ char_u *name;

- if (d->popover != NULL)
- gtk_popover_popdown(d->popover);
- if (d->menu != NULL)
+ FOR_ALL_MENUS(menu)
{
- gui_menu_cb(d->menu);
- gui_mch_flush();
+ if (menu->id == NULL)
+ continue;
+
+ name = translate_mnemonic_tag(menu->name, enable);
+ // Don't think the check if necessary but still do it anyways
+ if (VIM_IS_MENU_BAR_ITEM(menu->id))
+ vim_menu_bar_item_set_text(VIM_MENU_BAR_ITEM(menu->id),
+ (const char *)name);
+ vim_free(name);
}
}

static void
-popup_item_data_free(gpointer data, GClosure *closure UNUSED)
+popupmenu_closed_cb(GtkWidget *popover, void *udata UNUSED)
{
- g_free(data);
+ gtk_widget_unparent(popover);
+ if (gui.drawarea != NULL)
+ gtk_widget_queue_draw(gui.drawarea);
}

/*
@@ -3872,93 +3831,24 @@ popup_item_data_free(gpointer data, GClosure *closure UNUSED)
gui_gtk_popup_at(vimmenu_T *menu, int x, int y)
{
GtkWidget *popover;
- GtkWidget *box;
- GtkWidget *parent;
GdkRectangle rect;
- vimmenu_T *child;
- int mode;
- int natural_width = 0;

- if (menu == NULL || menu->children == NULL)
+ if (menu == NULL || menu->submenu_id == NULL)
return;

- // Attach the popover to drawarea's parent rather than to drawarea itself.
- // GtkDrawingArea is a leaf widget whose snapshot does not iterate children,
- // and parenting a popover to it has been observed to leave the drawing area
- // blank while the popover is open.
- parent = gtk_widget_get_parent(gui.drawarea);
- if (parent == NULL)
- parent = gui.drawarea;
-
- // Build the popover by hand instead of using gtk_popover_menu_new_from_model.
- // GtkPopoverMenu relies on the "menu.<name>" action-group lookup walking up
- // the parent chain, which has been observed to silently fail on some
- // compositors when the popover is parented via gtk_widget_set_parent. Wiring
- // each menu item to a plain "clicked" signal sidesteps that entirely.
- popover = gtk_popover_new();
- gtk_widget_set_parent(popover, parent);
- gtk_popover_set_has_arrow(GTK_POPOVER(popover), FALSE);
- gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
- gtk_widget_add_css_class(popover, "menu");
-
- box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- gtk_popover_set_child(GTK_POPOVER(popover), box);
-
- mode = get_menu_mode_flag();
-
- for (child = menu->children; child != NULL; child = child->next)
- {
- GtkWidget *item;
- char_u *label;
- popup_item_data_T *cb_data;
-
- if (menu_is_separator(child->name))
- {
- item = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
- gtk_box_append(GTK_BOX(box), item);
- continue;
- }
-
- label = CONVERT_TO_UTF8(child->dname);
- item = gtk_button_new_with_mnemonic(
- label != NULL ? (const char *)label : "");
- CONVERT_TO_UTF8_FREE(label);
-
- gtk_widget_add_css_class(item, "flat");
- gtk_widget_add_css_class(item, "model");
- gtk_button_set_has_frame(GTK_BUTTON(item), FALSE);
- gtk_widget_set_halign(item, GTK_ALIGN_FILL);
- {
- GtkWidget *btn_label = gtk_button_get_child(GTK_BUTTON(item));
- if (GTK_IS_LABEL(btn_label))
- gtk_label_set_xalign(GTK_LABEL(btn_label), 0.0);
- }
-
- if (!(child->modes & child->enabled & mode))
- gtk_widget_set_sensitive(item, FALSE);
-
- cb_data = g_new0(popup_item_data_T, 1);
- cb_data->popover = GTK_POPOVER(popover);
- cb_data->menu = child;
- g_signal_connect_data(item, "clicked",
- G_CALLBACK(popup_item_clicked_cb),
- cb_data, popup_item_data_free, 0);
-
- gtk_box_append(GTK_BOX(box), item);
- }
+ popover = vim_menu_copy(VIM_MENU(menu->submenu_id));
+ gtk_widget_set_parent(popover, gui.drawarea);

rect.x = x;
rect.y = y;
- // GtkPopover with GTK_POS_BOTTOM centres horizontally on the pointing-to
- // rectangle. Use the box's natural width so the popover's left edge ends
- // up at the cursor (down-and-to-the-right of the pointer).
- gtk_widget_measure(box, GTK_ORIENTATION_HORIZONTAL, -1,
- NULL, &natural_width, NULL, NULL);
- rect.width = natural_width > 0 ? natural_width : 1;
- rect.height = 1;
+ rect.width = rect.height = 1;
+
+ // Make sure popover aligns down-and-to-the-right of the pointer.
+ gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
+ gtk_widget_set_halign(popover, GTK_ALIGN_START);
gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect);

- g_signal_connect(popover, "closed",
+ g_signal_connect(GTK_POPOVER(popover), "closed",
G_CALLBACK(popupmenu_closed_cb), NULL);
gtk_popover_popup(GTK_POPOVER(popover));
}
@@ -3977,23 +3867,8 @@ gui_make_popup(char_u *path_name, int mouse_pos)
gui_mch_getmouse(&x, &y);
else
{
- // Find the cursor position relative to parent of drawarea
- GtkWidget *parent = gtk_widget_get_parent(gui.drawarea);
- graphene_point_t point;
- if (parent == NULL)
- parent = gui.drawarea;
-
- if (!gtk_widget_compute_point(gui.drawarea, parent,
- &GRAPHENE_POINT_INIT(0, 0), &point))
- x = y = 0;
- else
- {
- x = point.x;
- y = point.y;
- }
-
- x += FILL_X(curwin->w_wincol + curwin->w_wcol + 1) + 1;
- y += FILL_Y(W_WINROW(curwin) + curwin->w_wrow + 1) + 1;
+ x = FILL_X(curwin->w_wincol + curwin->w_wcol + 1) + 1;
+ y = FILL_Y(W_WINROW(curwin) + curwin->w_wrow + 1) + 1;
}

gui_gtk_popup_at(menu, x, y);
@@ -4339,7 +4214,6 @@ static int last_text_area_h = 0;
* ============================================================
* Menu functions
* ============================================================
- * TODO: Implement using GMenu + GtkPopoverMenuBar
*/

/*
@@ -4427,109 +4301,107 @@ create_toolbar_icon(vimmenu_T *menu)
return image;
}

-/*
- * GTK4 Menu system using GMenu + GSimpleActionGroup + GtkPopoverMenuBar.
- *
- * Each menu/submenu has a GMenu stored in menu->submenu_id (cast to
- * GtkWidget* to fit the struct field type).
- * Actions are added to a GSimpleActionGroup attached to gui.mainwin.
- */
-
-static int menu_action_id = 0;
-
static void
-menu_action_cb(GSimpleAction *action UNUSED, GVariant *parameter UNUSED,
- gpointer data)
+menu_button_clicked_cb(
+ VimMenuItem *item,
+ VimMenuItemEvent event,
+ vimmenu_T *menu)
{
- // Force-close any open popover menus in the menubar.
- // GTK4 marks them as not-visible but Vim's custom main loop
- // may not process the rendering update, so we flush explicitly.
- if (gui.menubar != NULL)
+ if (event == VIM_MENU_ITEM_CLICKED)
+ gui_menu_cb(menu);
+ else if (event == VIM_MENU_ITEM_SELECTED)
{
- GtkWidget *item;
+ // Show tooltip instantly in cmdline message.
+ char_u *tooltip;
+ static gboolean did_msg = FALSE;

- for (item = gtk_widget_get_first_child(gui.menubar);
- item != NULL;
- item = gtk_widget_get_next_sibling(item))
- {
- GtkWidget *child;
+ if (State & MODE_CMDLINE)
+ return;

- for (child = gtk_widget_get_first_child(item);
- child != NULL;
- child = gtk_widget_get_next_sibling(child))
- {
- if (GTK_IS_POPOVER(child))
- gtk_popover_popdown(GTK_POPOVER(child));
- }
+ tooltip = CONVERT_TO_UTF8(menu->strings[MENU_INDEX_TIP]);
+ if (tooltip != NULL && utf_valid_string(tooltip, NULL))
+ {
+ msg((char *)tooltip);
+ did_msg = TRUE;
+ setcursor();
+ out_flush_cursor(TRUE, FALSE);
}
+ else if (did_msg)
+ {
+ msg("");
+ did_msg = FALSE;
+ setcursor();
+ out_flush_cursor(TRUE, FALSE);
+ }
+ CONVERT_TO_UTF8_FREE(tooltip);
}
-
- gui_menu_cb((vimmenu_T *)data);
- gui_mch_flush();
-}
-
- static char *
-make_action_name(vimmenu_T *menu)
-{
- // Create a unique action name from the menu pointer
- static char buf[64];
- vim_snprintf(buf, sizeof(buf), "menu%d", menu_action_id++);
- return buf;
}

void
-gui_mch_add_menu(vimmenu_T *menu, int idx UNUSED)
+gui_mch_add_menu(vimmenu_T *menu, int idx)
{
- GMenu *submenu;
+ vimmenu_T *parent;
+ GtkWidget *parent_widget;
+ gboolean use_mnemonic;
+ char_u *text;

if (menu->name[0] == ']' || menu_is_popup(menu->name))
{
- // Popup menus - just create a GMenu, don't add to menubar
- submenu = g_menu_new();
- menu->submenu_id = (GtkWidget *)(gpointer)submenu;
+ // Attach the popover to drawarea's parent rather than to drawarea
+ // itself. GtkDrawingArea is a leaf widget whose snapshot does not
+ // iterate children, and parenting a popover to it has been observed to
+ // leave the drawing area blank while the popover is open.
+ menu->submenu_id = g_object_ref_sink(vim_menu_new());
+ gtk_widget_set_parent(menu->submenu_id, gui.drawarea);
return;
}

- if (menu->parent != NULL && menu->parent->submenu_id == NULL)
- return;
- if (!menu_is_menubar(menu->name))
- return;
+ parent = menu->parent;

- // Create a submenu for this menu
- submenu = g_menu_new();
- menu->submenu_id = (GtkWidget *)(gpointer)submenu;
+ if ((parent != NULL && parent->submenu_id == NULL)
+ || !menu_is_menubar(menu->name))
+ return;

- // Add to parent menu or menubar's model
- {
- GMenu *parent_menu;
- char_u *label;
+ menu->submenu_id = g_object_ref_sink(vim_menu_new());

- label = CONVERT_TO_UTF8(menu->dname);
+ use_mnemonic = parent != NULL || p_wak[0] != 'n';
+ text = translate_mnemonic_tag(menu->name, use_mnemonic);

- if (menu->parent != NULL)
- parent_menu = (GMenu *)(gpointer)menu->parent->submenu_id;
- else
- parent_menu = (GMenu *)(gpointer)g_object_get_data(
- G_OBJECT(gui.menubar), "vim-gmenu");
+ if (parent != NULL)
+ {
+ parent_widget = parent->submenu_id;
+ menu->id = g_object_ref_sink(vim_menu_item_new(
+ (const char *)text, NULL, NULL));

- if (parent_menu != NULL)
- g_menu_append_submenu(parent_menu, (const char *)label,
- G_MENU_MODEL(submenu));
+ vim_menu_item_set_submenu(VIM_MENU_ITEM(menu->id),
+ VIM_MENU(menu->submenu_id));
+ vim_menu_insert_item(VIM_MENU(parent_widget),
+ VIM_MENU_ITEM(menu->id), idx);
+ }
+ else
+ {
+ parent_widget = gui.menubar;
+ menu->id = g_object_ref_sink(vim_menu_bar_item_new(
+ (const char *)text, VIM_MENU(menu->submenu_id)));

- CONVERT_TO_UTF8_FREE(label);
+ vim_menu_bar_insert_item(VIM_MENU_BAR(parent_widget),
+ VIM_MENU_BAR_ITEM(menu->id), idx);
}
+
+ vim_free(text);
}

void
gui_mch_add_menu_item(vimmenu_T *menu, int idx)
{
- vimmenu_T *parent = menu->parent;
+ vimmenu_T *parent = menu->parent;

#ifdef FEAT_TOOLBAR
if (parent != NULL && menu_is_toolbar(parent->name))
{
if (menu_is_separator(menu->name))
{
+ // TODO
menu->id =
vim_toolbar_insert_separator(VIM_TOOLBAR(gui.toolbar), idx);
}
@@ -4565,51 +4437,36 @@ gui_mch_add_menu_item(vimmenu_T *menu, int idx)
if (parent == NULL || parent->submenu_id == NULL)
return;

+ if (menu_is_separator(menu->name))
+ {
+ menu->id = g_object_ref_sink(vim_menu_insert_separator(
+ VIM_MENU(parent->submenu_id), idx));
+ }
+ else
{
- GMenu *parent_menu = (GMenu *)(gpointer)parent->submenu_id;
+ char_u *text;
+ char_u *accel_text = NULL;
+ gboolean use_mnemonic;

- if (menu_is_separator(menu->name))
- {
- // GMenu doesn't have real separators; use a section
- GMenu *section = g_menu_new();
- g_menu_insert_section(parent_menu, idx, NULL,
- G_MENU_MODEL(section));
- g_object_unref(section);
- menu->id = NULL;
- }
- else
- {
- char *action_name;
- char detailed[80];
- char_u *label;
- GSimpleAction *action;
-
- // Create a unique action
- action_name = make_action_name(menu);
- action = g_simple_action_new(action_name, NULL);
- g_signal_connect(action, "activate",
- G_CALLBACK(menu_action_cb), menu);
-
- if (menu_action_group == NULL)
- {
- menu_action_group = g_simple_action_group_new();
- gtk_widget_insert_action_group(gui.mainwin, "menu",
- G_ACTION_GROUP(menu_action_group));
- }
- g_action_map_add_action(G_ACTION_MAP(menu_action_group),
- G_ACTION(action));
- g_object_unref(action);
-
- label = CONVERT_TO_UTF8(menu->dname);
- vim_snprintf(detailed, sizeof(detailed), "menu.%s", action_name);
- g_menu_insert(parent_menu, idx, (const char *)label, detailed);
- CONVERT_TO_UTF8_FREE(label);
-
- menu->id = (GtkWidget *)1; // non-NULL marker
- // Store action name for later use (grey/enable)
- menu->label = (GtkWidget *)vim_strsave(
- (char_u *)action_name);
- }
+ use_mnemonic = p_wak[0] != 'n';
+ text = translate_mnemonic_tag(menu->name, use_mnemonic);
+
+ if (menu->actext != NULL && menu->actext[0] != NUL)
+ accel_text = CONVERT_TO_UTF8(menu->actext);
+
+ // Add our own reference to the widget
+ menu->id = g_object_ref_sink(vim_menu_item_new((const char *)text,
+ (VimMenuItemFunc)menu_button_clicked_cb, menu));
+
+ if (accel_text != NULL)
+ vim_menu_item_set_accel(VIM_MENU_ITEM(menu->id),
+ (const char *)accel_text);
+
+ vim_menu_insert_item(VIM_MENU(parent->submenu_id),
+ VIM_MENU_ITEM(menu->id), idx);
+
+ vim_free(text);
+ CONVERT_TO_UTF8_FREE(accel_text);
}
}

@@ -4624,7 +4481,7 @@ gui_mch_menu_set_tip(vimmenu_T *menu)
{
char_u *tooltip;

- if (menu->id == NULL || menu->parent == NULL || gui.toolbar == NULL)
+ if (menu->id == NULL)
return;

tooltip = CONVERT_TO_UTF8(menu->strings[MENU_INDEX_TIP]);
@@ -4633,104 +4490,29 @@ gui_mch_menu_set_tip(vimmenu_T *menu)
CONVERT_TO_UTF8_FREE(tooltip);
}

-/*
- * Return TRUE if "menu" has a corresponding entry in its parent's GMenu.
- * Popup menus, toolbar children and orphaned submenus do not.
- */
- static int
-menu_has_gmenu_slot(vimmenu_T *menu)
-{
- if (menu == NULL || menu->name == NULL)
- return FALSE;
- if (menu->name[0] == ']' || menu_is_popup(menu->name))
- return FALSE;
- if (menu->parent != NULL)
- {
- if (menu_is_toolbar(menu->parent->name))
- return FALSE;
- if (menu->parent->submenu_id == NULL)
- return FALSE;
- return TRUE;
- }
- return menu_is_menubar(menu->name);
-}
-
-/*
- * Find the parent GMenu containing the entry for "menu" and the position of
- * that entry. Returns TRUE on success.
- */
- static int
-get_gmenu_pos_in_parent(vimmenu_T *menu, GMenu **parent_out, int *pos_out)
-{
- GMenu *parent_gmenu;
- vimmenu_T *first_sibling;
- vimmenu_T *sib;
- int pos = 0;
-
- if (!menu_has_gmenu_slot(menu))
- return FALSE;
-
- if (menu->parent != NULL)
- {
- parent_gmenu = (GMenu *)(gpointer)menu->parent->submenu_id;
- first_sibling = menu->parent->children;
- }
- else
- {
- if (gui.menubar == NULL)
- return FALSE;
- parent_gmenu = (GMenu *)(gpointer)g_object_get_data(
- G_OBJECT(gui.menubar), "vim-gmenu");
- first_sibling = root_menu;
- }
- if (parent_gmenu == NULL)
- return FALSE;
-
- for (sib = first_sibling; sib != NULL && sib != menu; sib = sib->next)
- if (menu_has_gmenu_slot(sib))
- pos++;
- if (sib != menu)
- return FALSE;
-
- *parent_out = parent_gmenu;
- *pos_out = pos;
- return TRUE;
-}
-
void
gui_mch_destroy_menu(vimmenu_T *menu)
{
- GMenu *parent_gmenu = NULL;
- int pos = 0;
-
// For toolbar buttons and separators, remove from the toolbar box.
- if (menu->id != NULL && menu->id != (GtkWidget *)1)
+ if (menu->parent != NULL && menu_is_toolbar(menu->parent->name))
{
vim_toolbar_remove(VIM_TOOLBAR(gui.toolbar), menu->id);
menu->id = NULL;
return;
}
- menu->id = NULL;

- // Remove the entry from the parent GMenu so the visible menu updates.
- if (get_gmenu_pos_in_parent(menu, &parent_gmenu, &pos))
- g_menu_remove(parent_gmenu, pos);
-
- // Remove the GAction created for this item and free its name.
- if (menu->label != NULL)
- {
- if (menu_action_group != NULL)
- g_action_map_remove_action(G_ACTION_MAP(menu_action_group),
- (const char *)menu->label);
- VIM_CLEAR(menu->label);
- }
+ // For popup menus, unparent the menu as well
+ if (menu->name[0] == ']' || menu_is_popup(menu->name))
+ gtk_widget_unparent(menu->submenu_id);
+ else if (menu->parent == NULL)
+ // Remove from menubar
+ vim_menu_bar_remove(VIM_MENU_BAR(gui.menubar), menu->id);
+ else
+ // Remove from parent menu
+ vim_menu_remove(VIM_MENU(menu->parent->submenu_id), menu->id);

- // Release our reference on the submenu GMenu (if any).
- if (menu->submenu_id != NULL)
- {
- g_object_unref(menu->submenu_id);
- menu->submenu_id = NULL;
- }
+ g_clear_object(&menu->submenu_id);
+ g_clear_object(&menu->id);
}

void
@@ -4745,28 +4527,32 @@ gui_mch_show_popupmenu(vimmenu_T *menu)
static void
show_menubar_popover(void)
{
- GMenu *gmenu;
- GtkWidget *popover;
+ GtkWidget *menu;
GdkRectangle rect;

if (gui.menubar == NULL || gui.drawarea == NULL)
return;
- gmenu = (GMenu *)g_object_get_data(G_OBJECT(gui.menubar), "vim-gmenu");
- if (gmenu == NULL || g_menu_model_get_n_items(G_MENU_MODEL(gmenu)) == 0)
+
+ if (gtk_widget_is_visible(gui.menubar))
+ {
+ // If menubar is visible, then just show first menu in menubar, like how
+ // GTK traditionally seems to do it?
+ vim_menu_bar_show(VIM_MENU_BAR(gui.menubar), NULL);
return;
+ }

- popover = gtk_popover_menu_new_from_model(G_MENU_MODEL(gmenu));
- gtk_widget_set_parent(popover, gui.drawarea);
- gtk_popover_set_has_arrow(GTK_POPOVER(popover), FALSE);
- gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
+ // Copy and convert the menubar into a menu popover
+ menu = vim_menu_bar_to_menu(VIM_MENU_BAR(gui.menubar));
+
+ gtk_widget_set_parent(menu, gui.drawarea);
+ gtk_popover_set_position(GTK_POPOVER(menu), GTK_POS_BOTTOM);
rect.x = 0;
rect.y = 0;
rect.width = 1;
rect.height = 1;
- gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect);
- g_signal_connect(popover, "closed",
- G_CALLBACK(popupmenu_closed_cb), NULL);
- gtk_popover_popup(GTK_POPOVER(popover));
+ gtk_popover_set_pointing_to(GTK_POPOVER(menu), &rect);
+ g_signal_connect(menu, "closed", G_CALLBACK(popupmenu_closed_cb), NULL);
+ gtk_popover_popup(GTK_POPOVER(menu));
}

/*
diff --git a/src/gui_gtk4_da.c b/src/gui_gtk4_da.c
index 11b746389..8db224ea6 100644
--- a/src/gui_gtk4_da.c
+++ b/src/gui_gtk4_da.c
@@ -140,7 +140,9 @@ vim_draw_area_class_init(VimDrawAreaClass *class)

obj_class->finalize = vim_draw_area_finalize;

+ // Add a layout manager so it can handle child popovers
gtk_widget_class_set_layout_manager_type(widget_class, GTK_TYPE_BIN_LAYOUT);
+
}

static void
diff --git a/src/gui_gtk4_menu.c b/src/gui_gtk4_menu.c
new file mode 100644
index 000000000..174f2f772
--- /dev/null
+++ b/src/gui_gtk4_menu.c
@@ -0,0 +1,1038 @@
+/* vi:set ts=8 sts=4 sw=4 noet:
+ *
+ * VIM - Vi IMproved by Bram Moolenaar
+ *
+ * Do ":help uganda" in Vim to read copying and usage conditions.
+ * Do ":help credits" in Vim to see a list of people who contributed.
+ * See README.txt for an overview of the Vim source code.
+ */
+
+#include "vim.h"
+
+#ifdef FEAT_MENU
+
+#include <gtk/gtk.h>
+#include "gui_gtk4_menu.h"
+
+// Note that this may return NULL for popup menus
+#define GET_MENU_BAR(m) VIM_MENU_BAR(gtk_widget_get_ancestor( \
+ GTK_WIDGET(m), VIM_TYPE_MENU_BAR))
+
+/*
+ * Similar as GtkButton but set CSS name to "item" to emulate GtkPopoverMenuBar
+ * styling. Always has a submenu.
+ */
+struct _VimMenuBarItem
+{
+ GtkButton parent;
+
+ GtkWidget *menu;
+};
+
+G_DEFINE_TYPE(VimMenuBarItem, vim_menu_bar_item, GTK_TYPE_BUTTON)
+
+ static void
+vim_menu_bar_item_dispose(GObject *object)
+{
+ VimMenuBarItem *self = VIM_MENU_BAR_ITEM(object);
+
+ g_clear_pointer((GtkWidget **)&self->menu, gtk_widget_unparent);
+
+ G_OBJECT_CLASS(vim_menu_bar_item_parent_class)->dispose(object);
+}
+
+ static void
+vim_menu_bar_item_class_init(VimMenuBarItemClass *class)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(class);
+ GObjectClass *obj_class = G_OBJECT_CLASS(class);
+
+ obj_class->dispose = vim_menu_bar_item_dispose;
+
+ gtk_widget_class_set_css_name(widget_class, "item");
+}
+
+ static void
+vim_menu_bar_item_init(VimMenuBarItem *self)
+{
+ // Enable mnemonics
+ gtk_button_set_use_underline(GTK_BUTTON(self), TRUE);
+}
+
+ GtkWidget *
+vim_menu_bar_item_new(const char *text, VimMenu *menu)
+{
+ VimMenuBarItem *item = g_object_new(VIM_TYPE_MENU_BAR_ITEM, NULL);
+
+ gtk_button_set_label(GTK_BUTTON(item), text);
+
+ item->menu = GTK_WIDGET(menu);
+ gtk_popover_set_position(GTK_POPOVER(menu), GTK_POS_BOTTOM);
+ // Make popover start at top left corner
+ gtk_widget_set_halign(GTK_WIDGET(menu), GTK_ALIGN_START);
+ gtk_widget_set_parent(GTK_WIDGET(menu), GTK_WIDGET(item));
+
+ return GTK_WIDGET(item);
+}
+
+ void
+vim_menu_bar_item_set_text(VimMenuBarItem *self, const char *text)
+{
+ gtk_button_set_label(GTK_BUTTON(self), text);
+}
+
+/*
+ * Similar to GtkPopoverMenuBar
+ */
+struct _VimMenuBar
+{
+ GtkWidget parent;
+
+ GList *items;
+
+ // Currently visible item that has submenu popped up, else NULL
+ GtkWidget *active_item;
+};
+
+G_DEFINE_TYPE(VimMenuBar, vim_menu_bar, GTK_TYPE_WIDGET)
+
+ static void
+vim_menu_bar_dispose(GObject *object)
+{
+ VimMenuBar *self = VIM_MENU_BAR(object);
+
+ g_clear_list(&self->items, (GDestroyNotify)gtk_widget_unparent);
+
+ G_OBJECT_CLASS(vim_menu_bar_parent_class)->dispose(object);
+}
+
+ static void
+vim_menu_bar_class_init(VimMenuBarClass *class)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(class);
+ GObjectClass *obj_class = G_OBJECT_CLASS(class);
+
+ obj_class->dispose = vim_menu_bar_dispose;
+
+ gtk_widget_class_set_layout_manager_type(widget_class, GTK_TYPE_BOX_LAYOUT);
+ gtk_widget_class_set_css_name(widget_class, "menubar");
+}
+
+ static void
+vim_menu_bar_init(VimMenuBar *self)
+{
+ GtkLayoutManager *lm = gtk_widget_get_layout_manager(GTK_WIDGET(self));
+
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(lm),
+ GTK_ORIENTATION_HORIZONTAL);
+ gtk_box_layout_set_spacing(GTK_BOX_LAYOUT(lm), 0);
+}
+
+ GtkWidget *
+vim_menu_bar_new(void)
+{
+ return g_object_new(VIM_TYPE_MENU_BAR, NULL);
+}
+
+/*
+ * Create a VimMenu widget with the menus of the menu bar as its submenus. Note
+ * that it is a deep copy.
+ */
+ GtkWidget *
+vim_menu_bar_to_menu(VimMenuBar *self)
+{
+ GtkWidget *menu = vim_menu_new();
+ int i = 0;
+
+ for (GList *l = self->items; l != NULL; l = l->next, i++)
+ {
+ VimMenuBarItem *baritem = l->data;
+ GtkWidget *item;
+
+ item = vim_menu_item_new(
+ gtk_button_get_label(GTK_BUTTON(baritem)), NULL, NULL);
+
+ vim_menu_item_set_submenu(VIM_MENU_ITEM(item),
+ VIM_MENU(vim_menu_copy(VIM_MENU(baritem->menu))));
+
+ vim_menu_insert_item(VIM_MENU(menu), VIM_MENU_ITEM(item), i);
+ }
+ return menu;
+}
+
+/*
+ * Set the currently active menu of the menubar to "item". If NULL, then close
+ * any submenus.
+ */
+ static void
+vim_menu_bar_set_active_item(
+ VimMenuBar *self,
+ VimMenuBarItem *item,
+ gboolean force)
+{
+ // Do nothing if currently active item is "item", or if there is not
+ // currently active item. User must click a menu item first for menus to
+ // automatically appear on hover. This is unless "force" is TRUE.
+ //
+ // Only make item selected if there is no active item (no submenu open), or
+ // if the item was set as the active item..
+ if ((!force && self->active_item == NULL)
+ || self->active_item == GTK_WIDGET(item))
+ {
+ if (self->active_item == NULL)
+ gtk_widget_set_state_flags(GTK_WIDGET(item),
+ GTK_STATE_FLAG_SELECTED, FALSE);
+ return;
+ }
+
+ if (self->active_item != NULL)
+ {
+ // Call this before popdown, since "closed" signal may be emitted
+ // immediately.
+ gtk_widget_unset_state_flags(self->active_item,
+ GTK_STATE_FLAG_SELECTED);
+ gtk_popover_popdown(GTK_POPOVER(
+ VIM_MENU_BAR_ITEM(self->active_item)->menu)
+ );
+ }
+
+ self->active_item = GTK_WIDGET(item);
+ if (item != NULL)
+ {
+ gtk_popover_popup(GTK_POPOVER(item->menu));
+ gtk_widget_set_state_flags(GTK_WIDGET(item),
+ GTK_STATE_FLAG_SELECTED, FALSE);
+ }
+}
+
+ static void
+vim_menu_bar_item_enter_cb(
+ GtkEventController *controller,
+ double x UNUSED,
+ double y UNUSED,
+ VimMenuBar *menubar)
+{
+ VimMenuBarItem *self;
+
+ self = VIM_MENU_BAR_ITEM(gtk_event_controller_get_widget(controller));
+ vim_menu_bar_set_active_item(menubar, self, FALSE);
+}
+
+ static void
+vim_menu_bar_item_leave_cb(
+ GtkEventController *controller,
+ VimMenuBar *menubar)
+{
+ VimMenuBarItem *self;
+
+ self = VIM_MENU_BAR_ITEM(gtk_event_controller_get_widget(controller));
+
+ // If the item is the currently active item, then don't deselect it.
+ if (menubar->active_item != GTK_WIDGET(self))
+ gtk_widget_unset_state_flags(GTK_WIDGET(self),
+ GTK_STATE_FLAG_SELECTED);
+}
+
+ static void
+vim_menu_bar_item_clicked_cb(VimMenuBarItem *self, VimMenuBar *menubar)
+{
+ vim_menu_bar_set_active_item(menubar, self, TRUE);
+}
+
+ static void
+vim_menu_bar_item_menu_closed_cb(VimMenu *menu UNUSED, VimMenuBar *menubar)
+{
+ if (menubar->active_item != NULL)
+ gtk_widget_unset_state_flags(GTK_WIDGET(menubar->active_item),
+ GTK_STATE_FLAG_SELECTED);
+ vim_menu_bar_set_active_item(menubar, NULL, TRUE);
+ // Make sure to focus drawarea
+ gtk_widget_grab_focus(gui.drawarea);
+}
+
+/*
+ * Insert the menu item at the given index in the menu bar.
+ */
+ void
+vim_menu_bar_insert_item(VimMenuBar *self, VimMenuBarItem *item, int idx)
+{
+ GtkEventController *controller;
+ GList *next_sibling;
+
+ next_sibling = g_list_nth(self->items, idx);
+ gtk_widget_insert_before(GTK_WIDGET(item), GTK_WIDGET(self),
+ next_sibling == NULL ? NULL : next_sibling->data);
+
+ self->items = g_list_insert(self->items, item, idx);
+
+ controller = gtk_event_controller_motion_new();
+ g_signal_connect_object(controller, "enter",
+ G_CALLBACK(vim_menu_bar_item_enter_cb), self, G_CONNECT_DEFAULT);
+ g_signal_connect_object(controller, "leave",
+ G_CALLBACK(vim_menu_bar_item_leave_cb), self, G_CONNECT_DEFAULT);
+ gtk_widget_add_controller(GTK_WIDGET(item), controller);
+
+ g_signal_connect_object(item, "clicked",
+ G_CALLBACK(vim_menu_bar_item_clicked_cb), self, G_CONNECT_DEFAULT);
+
+ g_signal_connect_object(item->menu, "closed",
+ G_CALLBACK(vim_menu_bar_item_menu_closed_cb),
+ self, G_CONNECT_DEFAULT);
+}
+
+/*
+ * Remove the menu item or separator from the menu bar
+ */
+ void
+vim_menu_bar_remove(VimMenuBar *self, GtkWidget *item)
+{
+ self->items = g_list_remove(self->items, item);
+ gtk_widget_unparent(item);
+}
+
+/*
+ * Show the given menu in the menubar. If "item" is NULL, then show first menu.
+ */
+ void
+vim_menu_bar_show(VimMenuBar *self, VimMenuBarItem *item)
+{
+ if (item == NULL)
+ item = g_list_nth_data(self->items, 0);
+
+ vim_menu_bar_set_active_item(self, item, TRUE);
+}
+
+/*
+ * If "dir" is negative, then move to the item previous of currently the active
+ * item. If "dir" is positive, then move to the next item. Return the resulting
+ * item or NULL if there are no suitable ones.
+ */
+ static GtkWidget *
+vim_menu_bar_move_active_item(VimMenuBar *self, int dir)
+{
+ GtkWidget *(*func)(GtkWidget *);
+ GtkWidget *(*null_func)(GtkWidget *);
+ GtkWidget *widget = self->active_item;
+
+ if (widget == NULL)
+ return gtk_widget_get_first_child(GTK_WIDGET(self));
+
+ if (dir > 0)
+ {
+ func = gtk_widget_get_next_sibling;
+ null_func = gtk_widget_get_first_child;
+ }
+ else
+ {
+ func = gtk_widget_get_prev_sibling;
+ null_func = gtk_widget_get_last_child;
+ }
+
+ while (TRUE)
+ {
+ widget = func(widget);
+ if (widget == NULL)
+ {
+ if (null_func == NULL)
+ break;
+ widget = null_func(GTK_WIDGET(self));
+ null_func = NULL;
+ if (widget == NULL)
+ break;
+ }
+ break;
+ }
+ return widget;
+}
+
+/*
+ * Menu button that can be used to perform actions, or if there is a submenu,
+ * toggle the state of the submenu popover. CSS name is "modelbutton" to make it
+ * styled like GtkPopoverMenu
+ */
+struct _VimMenuItem
+{
+ GtkButton parent;
+
+ GtkWidget *label; // Displays text for button.
+ GtkWidget *aux_widget; // Either an icon or a label showing the accelerator
+ // text.
+
+ GtkWidget *submenu; // Submenu popover if any (VimMenu)
+
+ // Callback called when clicked or selected, we store this so that copying a
+ // menu item works properly.
+ VimMenuItemFunc func;
+ void *func_udata;
+};
+
+G_DEFINE_TYPE(VimMenuItem, vim_menu_item, GTK_TYPE_BUTTON)
+
+ static void
+vim_menu_item_dispose(GObject *object)
+{
+ VimMenuItem *self = VIM_MENU_ITEM(object);
+
+ g_clear_pointer(&self->label, gtk_widget_unparent);
+ g_clear_pointer(&self->aux_widget, gtk_widget_unparent);
+ g_clear_pointer((GtkWidget **)&self->submenu, gtk_widget_unparent);
+
+ G_OBJECT_CLASS(vim_menu_item_parent_class)->dispose(object);
+}
+
+ static void
+vim_menu_item_class_init(VimMenuItemClass *class)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(class);
+ GObjectClass *obj_class = G_OBJECT_CLASS(class);
+
+ obj_class->dispose = vim_menu_item_dispose;
+
+ gtk_widget_class_set_layout_manager_type(widget_class, GTK_TYPE_BOX_LAYOUT);
+ gtk_widget_class_set_css_name(widget_class, "modelbutton");
+}
+
+ static void
+vim_menu_item_init(VimMenuItem *self)
+{
+ GtkLayoutManager *lm = gtk_widget_get_layout_manager(GTK_WIDGET(self));
+
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(lm),
+ GTK_ORIENTATION_HORIZONTAL);
+ gtk_box_layout_set_spacing(GTK_BOX_LAYOUT(lm), 0);
+}
+
+/*
+ * Create a new menu item with the given text to display. "func" may be NULL if
+ * not needed.
+ */
+ GtkWidget *
+vim_menu_item_new(const char *text, VimMenuItemFunc func, void *udata)
+{
+ VimMenuItem *item = g_object_new(VIM_TYPE_MENU_ITEM, NULL);
+
+ item->func = func;
+ item->func_udata = udata;
+ item->label = gtk_label_new_with_mnemonic(text);
+
+ // Make sure label is on the right and pushes everything to the left
+ gtk_widget_set_halign(item->label, GTK_ALIGN_START);
+ gtk_widget_set_hexpand(item->label, TRUE);
+ gtk_widget_set_parent(item->label, GTK_WIDGET(item));
+
+ return GTK_WIDGET(item);
+}
+
+/*
+ * Update displayed text for menu item
+ */
+ void
+vim_menu_item_set_text(VimMenuItem *self, const char *text)
+{
+ gtk_label_set_text_with_mnemonic(GTK_LABEL(self->label), text);
+}
+
+ static void
+vim_menu_item_set_aux_widget(VimMenuItem *self, GtkWidget *aux)
+{
+ self->aux_widget = aux;
+ gtk_widget_set_halign(self->aux_widget, GTK_ALIGN_END);
+ gtk_widget_set_hexpand(self->aux_widget, FALSE);
+ gtk_widget_set_margin_start(self->aux_widget, 50);
+}
+
+/*
+ * Set the accelerator text for the menu item.
+ */
+ void
+vim_menu_item_set_accel(VimMenuItem *self, const char *accel_text)
+{
+ assert(self->aux_widget == NULL);
+
+ vim_menu_item_set_aux_widget(self, gtk_label_new(accel_text));
+ gtk_widget_insert_after(self->aux_widget, GTK_WIDGET(self), self->label);
+}
+
+/*
+ * Set the submenu popover for the menu item
+ */
+ void
+vim_menu_item_set_submenu(VimMenuItem *self, VimMenu *submenu)
+{
+ GtkWidget *icon;
+
+ assert(self->submenu == NULL);
+ assert(self->aux_widget == NULL);
+
+ // Add arrow icon pointing to right
+ icon = gtk_image_new_from_icon_name("pan-end-symbolic");
+ vim_menu_item_set_aux_widget(self, icon);
+ gtk_widget_insert_after(self->aux_widget, GTK_WIDGET(self), self->label);
+
+ gtk_popover_set_position(GTK_POPOVER(submenu), GTK_POS_RIGHT);
+ // Make top of popover be aligned with button.
+ gtk_widget_set_valign(GTK_WIDGET(submenu), GTK_ALIGN_START);
+
+ self->submenu = GTK_WIDGET(submenu);
+ gtk_widget_set_parent(GTK_WIDGET(submenu), GTK_WIDGET(self));
+}
+
+/*
+ * Create a deep copy of the menu item
+ */
+ static GtkWidget *
+vim_menu_item_copy(VimMenuItem *self)
+{
+ GtkWidget *copy;
+
+ copy = vim_menu_item_new(
+ gtk_label_get_text(GTK_LABEL(self->label)),
+ self->func, self->func_udata);
+
+ if (self->submenu != NULL)
+ vim_menu_item_set_submenu(VIM_MENU_ITEM(copy),
+ VIM_MENU(vim_menu_copy(VIM_MENU(self->submenu))));
+ else if (self->aux_widget != NULL)
+ vim_menu_item_set_accel(VIM_MENU_ITEM(copy),
+ gtk_label_get_text(GTK_LABEL(self->aux_widget)));
+ return copy;
+}
+
+/*
+ * Similar to GtkPopoverMenu, except uses GtkWidgets directly like GTK3, instead
+ * of abstracting it into GMenuModel.
+ */
+struct _VimMenu
+{
+ GtkPopover parent;
+
+ GtkWidget *box;
+ GtkWidget *scr;
+
+ GList *items;
+
+ // Currently active item showing submenu popover, or being hovered on, or
+ // NULL. Note that item may have submenu but not have it open, when
+ // navigating via keyboard.
+ GtkWidget *active_item;
+
+ // Used when mouse is hovering over an item and user is navigating with
+ // keyboard. When the scrolled window scrolls down or up, this causes a
+ // mouse enter event, causing the active item to go to the item that the
+ // mouse is hovered on, instead of the next item (from keyboard navigation).
+ gboolean ignore_hover;
+ double prev_x;
+ double prev_y;
+};
+
+G_DEFINE_TYPE(VimMenu, vim_menu, GTK_TYPE_POPOVER)
+
+ static void
+vim_menu_dispose(GObject *object)
+{
+ VimMenu *self = VIM_MENU(object);
+
+ g_clear_list(&self->items, (GDestroyNotify)gtk_widget_unparent);
+ g_clear_pointer(&self->scr, gtk_widget_unparent);
+
+ G_OBJECT_CLASS(vim_menu_parent_class)->dispose(object);
+}
+
+ static void
+vim_menu_class_init(VimMenuClass *class)
+{
+ GObjectClass *obj_class = G_OBJECT_CLASS(class);
+
+ obj_class->dispose = vim_menu_dispose;
+}
+
+ static gboolean
+vim_menu_select_active_item(VimMenu *self, gboolean open)
+{
+ VimMenuItem *item;
+
+ gtk_widget_set_state_flags(self->active_item,
+ GTK_STATE_FLAG_SELECTED, FALSE);
+
+ // Make sure to focus item, so that scrolled window knows what to do.
+ gtk_widget_grab_focus(GTK_WIDGET(self->active_item));
+
+ item = VIM_MENU_ITEM(self->active_item);
+ if (item->func != NULL)
+ item->func(item, VIM_MENU_ITEM_SELECTED, item->func_udata);
+
+ if (open && VIM_MENU_ITEM(self->active_item)->submenu != NULL)
+ {
+ GtkWidget *submenu = VIM_MENU_ITEM(self->active_item)->submenu;
+ gtk_popover_popup(GTK_POPOVER(submenu));
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/*
+ * Set the active item of the menu to "item". If "item" is NULL, then close any
+ * submenus. If "open" is FALSE, then don't open the submenu if any.
+ */
+ static void
+vim_menu_set_active_item(VimMenu *self, VimMenuItem *item, gboolean open)
+{
+ if (self->active_item == GTK_WIDGET(item))
+ return;
+
+ if (self->active_item != NULL)
+ {
+ if (VIM_MENU_ITEM(self->active_item)->submenu != NULL)
+ gtk_popover_popdown(GTK_POPOVER(
+ VIM_MENU_ITEM(self->active_item)->submenu
+ ));
+ gtk_widget_unset_state_flags(GTK_WIDGET(self->active_item),
+ GTK_STATE_FLAG_SELECTED);
+ }
+
+ self->active_item = GTK_WIDGET(item);
+ if (item != NULL)
+ (void)vim_menu_select_active_item(self, open);
+}
+
+ static void
+vim_menu_closed_cb(VimMenu *self, void *udata UNUSED)
+{
+ vim_menu_set_active_item(self, NULL, FALSE);
+}
+
+/*
+ * If "dir" is negative, then move to the item previous of currently the active
+ * item. If "dir" is positive, then move to the next item. Return the resulting
+ * item or NULL if there are no suitable ones.
+ */
+ static GtkWidget *
+vim_menu_move_active_item(VimMenu *self, int dir)
+{
+ GtkWidget *(*func)(GtkWidget *);
+ GtkWidget *(*null_func)(GtkWidget *);
+ GtkWidget *widget = self->active_item;
+
+ // If there is no currently active item, then just use the first one
+ if (widget == NULL)
+ {
+ widget = gtk_widget_get_first_child(self->box);
+ while (widget != NULL && !VIM_IS_MENU_ITEM(widget))
+ widget = gtk_widget_get_next_sibling(widget);
+ return widget;
+ }
+
+ // Could also just use GList functions, but this seems simpler (no
+ // difference anyways).
+ if (dir > 0)
+ {
+ func = gtk_widget_get_next_sibling;
+ null_func = gtk_widget_get_first_child;
+ }
+ else
+ {
+ func = gtk_widget_get_prev_sibling;
+ null_func = gtk_widget_get_last_child;
+ }
+
+ while (TRUE)
+ {
+ widget = func(widget);
+ if (widget == NULL)
+ {
+ if (null_func == NULL)
+ break;
+ widget = null_func(self->box);
+ null_func = NULL;
+ if (widget == NULL)
+ break;
+ }
+ if (VIM_IS_MENU_ITEM(widget))
+ break;
+ }
+ return widget;
+}
+
+ static void
+vim_menu_reset_parent_prelight(VimMenu *self)
+{
+ GtkWidget *parent = gtk_widget_get_parent(GTK_WIDGET(self));
+ VimMenu *parent_menu;
+
+ // gtk_widget_get_ancestor assumes the widget itself is also an ancestor, so
+ // we must get parent of menu first.
+ parent_menu = VIM_MENU(gtk_widget_get_ancestor(parent, VIM_TYPE_MENU));
+
+ if (parent_menu == NULL)
+ // TRUE for popup menus
+ return;
+
+ if (parent_menu->active_item != NULL)
+ gtk_widget_unset_state_flags(GTK_WIDGET(parent_menu->active_item),
+ GTK_STATE_FLAG_PRELIGHT);
+}
+
+/*
+ * Close all submenus in the menubar given a menu widget
+ */
+ static void
+vim_menu_close_all(VimMenu *self)
+{
+ VimMenuBar *menubar = GET_MENU_BAR(self);
+
+ // Must check if NULL, because popup menus don't have a parent.
+ if (menubar != NULL)
+ vim_menu_bar_set_active_item(menubar, NULL, TRUE);
+ else
+ gtk_popover_popdown(GTK_POPOVER(self));
+
+ // Grab focus after popup menus without a menubar are closed
+ gtk_widget_grab_focus(gui.drawarea);
+}
+
+ static gboolean
+vim_menu_key_pressed_cb(
+ GtkEventController *controller UNUSED,
+ guint keyval,
+ guint keycode UNUSED,
+ GdkModifierType state,
+ VimMenu *self)
+{
+ GtkWidget *widget;
+
+ switch (keyval)
+ {
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_Tab:
+ case GDK_KEY_KP_Tab:
+ case GDK_KEY_ISO_Left_Tab:
+ // Go to the previous or next item if any
+ widget = vim_menu_move_active_item(self,
+ (state & GDK_SHIFT_MASK)
+ || keyval == GDK_KEY_Up ? -1 : 1);
+ vim_menu_set_active_item(self, VIM_MENU_ITEM(widget), FALSE);
+ self->ignore_hover = TRUE;
+ return TRUE;
+ case GDK_KEY_Left:
+ // Pressing control switches menu bar item.
+ if (state & GDK_CONTROL_MASK)
+ {
+ VimMenuBar *menubar = GET_MENU_BAR(self);
+
+ if (menubar != NULL)
+ {
+ widget = vim_menu_bar_move_active_item(menubar, -1);
+ vim_menu_bar_set_active_item(menubar,
+ VIM_MENU_BAR_ITEM(widget), TRUE);
+ }
+ return TRUE;
+ }
+ // Go to parent menu (if any). We can do this by just closing the
+ // popover.
+ gtk_popover_popdown(GTK_POPOVER(self));
+ // For some reason when pointer is hovered over draw area, the
+ // active item in the parent menu will stay prelighted even when the
+ // active item is moved.
+ vim_menu_reset_parent_prelight(self);
+ return TRUE;
+ case GDK_KEY_Right:
+ if (state & GDK_CONTROL_MASK)
+ {
+ VimMenuBar *menubar = GET_MENU_BAR(self);
+
+ if (menubar != NULL)
+ {
+ widget = vim_menu_bar_move_active_item(menubar, 1);
+ vim_menu_bar_set_active_item(menubar,
+ VIM_MENU_BAR_ITEM(widget), TRUE);
+ }
+ return TRUE;
+ }
+ // Open submenu if active item has one
+ if (self->active_item != NULL
+ && vim_menu_select_active_item(self, TRUE))
+ {
+ // Select first item in opened submenu
+ VimMenu *submenu = VIM_MENU(
+ VIM_MENU_ITEM(self->active_item)->submenu
+ );
+
+ vim_menu_set_active_item(submenu,
+ VIM_MENU_ITEM(
+ gtk_widget_get_first_child(submenu->box)),
+ FALSE);
+ }
+ self->ignore_hover = TRUE;
+ return TRUE;
+ case GDK_KEY_Escape:
+ // Close all popover menus
+ vim_menu_close_all(self);
+ return TRUE;
+ case GDK_KEY_ISO_Enter:
+ case GDK_KEY_3270_Enter:
+ case GDK_KEY_KP_Enter:
+ case GDK_KEY_Return:
+ if (self->active_item != NULL)
+ g_signal_emit_by_name(self->active_item, "clicked");
+ return TRUE;
+ default:
+ break;
+ }
+ return FALSE;
+}
+
+
+ static void
+vim_menu_motion_cb(
+ GtkEventController *controller UNUSED,
+ double x,
+ double y,
+ VimMenu *self)
+{
+ if (self->prev_x == -1 || self->prev_y == -1 ||
+ (fabs(self->prev_x - x) > 0.05 && fabs(self->prev_y - y) > 0.05))
+ self->ignore_hover = FALSE;
+ self->prev_x = x;
+ self->prev_y = y;
+}
+
+ static void
+vim_menu_focus_cb(GtkEventController *controller UNUSED, VimMenu *self)
+{
+ gtk_popover_set_mnemonics_visible(GTK_POPOVER(self), TRUE);
+}
+
+ static void
+vim_menu_init(VimMenu *self)
+{
+ GtkEventController *controller;
+ GtkWidget *stack;
+ GtkWidget *parent_box;
+ GListModel *controllers;
+
+ gtk_popover_set_has_arrow(GTK_POPOVER(self), FALSE);
+ gtk_popover_set_autohide(GTK_POPOVER(self), TRUE);
+
+ // Do not make child popovers close parent popovers when they are closed.
+ gtk_popover_set_cascade_popdown(GTK_POPOVER(self), FALSE);
+
+ stack = gtk_stack_new();
+
+ // "stack" and "parent_box" have no use other than to make the css structure
+ // of the popup menu be exactly like GtkPopoverMenu. This is so that GTK
+ // themes style VimMenu exactly like GtkPopoverMenu.
+ parent_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_stack_add_child(GTK_STACK(stack), parent_box);
+ gtk_stack_set_visible_child(GTK_STACK(stack), parent_box);
+
+ self->box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_hexpand(self->box, TRUE);
+ gtk_widget_set_vexpand(self->box, TRUE);
+ gtk_box_append(GTK_BOX(parent_box), self->box);
+
+ self->scr = gtk_scrolled_window_new();
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->scr),
+ GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+ gtk_scrolled_window_set_propagate_natural_width(
+ GTK_SCROLLED_WINDOW(self->scr), TRUE);
+ gtk_scrolled_window_set_propagate_natural_height(
+ GTK_SCROLLED_WINDOW(self->scr), TRUE);
+
+ gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->scr), stack);
+ gtk_popover_set_child(GTK_POPOVER(self), self->scr);
+
+ gtk_widget_add_css_class(GTK_WIDGET(self), "menu");
+
+ // Add key controller for basic movement
+ controller = gtk_event_controller_key_new();
+ // Make sure we get the key presses first and handle them if possible
+ gtk_event_controller_set_propagation_phase(controller,
+ GTK_PHASE_CAPTURE);
+ g_signal_connect_object(controller, "key-pressed",
+ G_CALLBACK(vim_menu_key_pressed_cb),
+ self, G_CONNECT_DEFAULT);
+ gtk_widget_add_controller(GTK_WIDGET(self), controller);
+
+ // Show mnemonic underline always
+ controller = gtk_event_controller_focus_new();
+ g_signal_connect_object(controller, "enter",
+ G_CALLBACK(vim_menu_focus_cb), self, G_CONNECT_DEFAULT);
+ gtk_widget_add_controller(GTK_WIDGET(self), controller);
+
+ controller = gtk_event_controller_motion_new();
+ g_signal_connect_object(controller, "motion",
+ G_CALLBACK(vim_menu_motion_cb), self, G_CONNECT_DEFAULT);
+ gtk_widget_add_controller(GTK_WIDGET(self), controller);
+
+ g_signal_connect(self, "closed", G_CALLBACK(vim_menu_closed_cb), NULL);
+
+ // Set all shortcut controllers in the window to not require a modifier for
+ // mnemonics.
+ controllers = gtk_widget_observe_controllers(GTK_WIDGET(self));
+ for (int i = 0; i < g_list_model_get_n_items(controllers); i++)
+ {
+ controller = g_list_model_get_item(controllers, i);
+ if (GTK_IS_SHORTCUT_CONTROLLER(controller))
+ gtk_shortcut_controller_set_mnemonics_modifiers(
+ GTK_SHORTCUT_CONTROLLER(controller), 0);
+ }
+ g_object_unref(controllers);
+
+ self->prev_x = self->prev_y = -1;
+}
+
+ GtkWidget *
+vim_menu_new(void)
+{
+ return g_object_new(VIM_TYPE_MENU, NULL);
+}
+
+ static GtkWidget *
+vim_menu_insert(VimMenu *self, GtkWidget *item, int idx)
+{
+ if (idx > 0)
+ {
+ GList *prev = g_list_nth(self->items, idx - 1);
+ if (prev == NULL)
+ gtk_box_append(GTK_BOX(self->box), item);
+ else
+ gtk_box_insert_child_after(GTK_BOX(self->box), item,
+ prev->data);
+ }
+ else if (idx == 0)
+ gtk_box_prepend(GTK_BOX(self->box), item);
+ else
+ gtk_box_append(GTK_BOX(self->box), item);
+
+ self->items = g_list_insert(self->items, item, idx);
+ return item;
+}
+
+ static void
+vim_menu_item_clicked_cb(VimMenuItem *self, VimMenu *menu)
+{
+ // Only close all menus if item is a regular button (no submenu). If item
+ // has a submenu, then just toggle it on and off.
+ if (self->submenu != NULL)
+ {
+ if (gtk_widget_is_visible(self->submenu))
+ gtk_popover_popdown(GTK_POPOVER(self->submenu));
+ else
+ gtk_popover_popup(GTK_POPOVER(self->submenu));
+ return;
+ }
+
+ // Since we set the "cascade-popdown" property to FALSE, we must popdown the
+ // toplevel menu/popover, so that all submenus are closed.
+ vim_menu_close_all(menu);
+ if (self->func != NULL)
+ self->func(self, VIM_MENU_ITEM_CLICKED, self->func_udata);
+}
+
+ static void
+vim_menu_item_enter_cb(
+ GtkEventController *controller,
+ double x UNUSED,
+ double y UNUSED,
+ VimMenu *menu)
+{
+ VimMenuItem *self;
+
+ if (menu->ignore_hover || !gtk_event_controller_motion_contains_pointer(
+ GTK_EVENT_CONTROLLER_MOTION(controller)))
+ return;
+
+ self = VIM_MENU_ITEM(gtk_event_controller_get_widget(controller));
+ vim_menu_set_active_item(menu, self, TRUE);
+}
+
+ static void
+vim_menu_item_leave_cb(GtkEventController *controller, VimMenu *menu)
+{
+ VimMenuItem *self;
+
+ if (gtk_event_controller_motion_contains_pointer(
+ GTK_EVENT_CONTROLLER_MOTION(controller)))
+ return;
+
+ self = VIM_MENU_ITEM(gtk_event_controller_get_widget(controller));
+ if (menu->active_item == GTK_WIDGET(self))
+ vim_menu_set_active_item(menu, NULL, FALSE);
+}
+
+/*
+ * Insert the menu item at the given index in the menu. If "idx" is negative,
+ * then append the menu item.
+ */
+ void
+vim_menu_insert_item(VimMenu *self, VimMenuItem *item, int idx)
+{
+ GtkEventController *controller;
+
+ vim_menu_insert(self, GTK_WIDGET(item), idx);
+
+ controller = gtk_event_controller_motion_new();
+ g_signal_connect_object(controller, "enter",
+ G_CALLBACK(vim_menu_item_enter_cb), self, G_CONNECT_DEFAULT);
+ g_signal_connect_object(controller, "leave",
+ G_CALLBACK(vim_menu_item_leave_cb), self, G_CONNECT_DEFAULT);
+ gtk_widget_add_controller(GTK_WIDGET(item), controller);
+
+ g_signal_connect_object(item, "clicked",
+ G_CALLBACK(vim_menu_item_clicked_cb),
+ self, G_CONNECT_DEFAULT);
+}
+
+/*
+ * Insert a separator at the given position and return it.
+ */
+ GtkWidget *
+vim_menu_insert_separator(VimMenu *self, int idx)
+{
+ return vim_menu_insert(self,
+ gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), idx);
+}
+
+/*
+ * Remove the menu item or separator from the menu
+ */
+ void
+vim_menu_remove(VimMenu *self, GtkWidget *item)
+{
+ self->items = g_list_remove(self->items, item);
+ gtk_box_remove(GTK_BOX(self->box), item);
+}
+
+/*
+ * Create a deep copy of the menu
+ */
+ GtkWidget *
+vim_menu_copy(VimMenu *self)
+{
+ GtkWidget *copy = vim_menu_new();
+ int i = 0;
+
+ for (GList *l = self->items; l != NULL; l = l->next, i++)
+ {
+ VimMenuItem *item;
+ GtkWidget *item_copy;
+
+ if (!VIM_IS_MENU_ITEM(l->data))
+ {
+ assert(GTK_IS_SEPARATOR(l->data));
+ vim_menu_insert_separator(VIM_MENU(copy), i);
+ continue;
+ }
+
+ item = l->data;
+ item_copy = vim_menu_item_copy(item);
+
+ vim_menu_insert_item(VIM_MENU(copy), VIM_MENU_ITEM(item_copy), i);
+ }
+ return copy;
+}
+
+#endif // FEAT_MENU
diff --git a/src/gui_gtk4_menu.h b/src/gui_gtk4_menu.h
new file mode 100644
index 000000000..f6965e6dc
--- /dev/null
+++ b/src/gui_gtk4_menu.h
@@ -0,0 +1,61 @@
+/* vi:set ts=8 sts=4 sw=4 noet:
+ *
+ * VIM - Vi IMproved by Bram Moolenaar
+ *
+ * Do ":help uganda" in Vim to read copying and usage conditions.
+ * Do ":help credits" in Vim to see a list of people who contributed.
+ * See README.txt for an overview of the Vim source code.
+ */
+
+#ifndef GUI_GTK4_MENU_H
+#define GUI_GTK4_MENU_H
+
+#include "vim.h"
+
+#ifdef FEAT_MENU
+
+# include <gtk/gtk.h>
+
+# define VIM_TYPE_MENU_BAR_ITEM (vim_menu_bar_item_get_type())
+G_DECLARE_FINAL_TYPE(VimMenuBarItem, vim_menu_bar_item, VIM, MENU_BAR_ITEM, GtkButton)
+
+# define VIM_TYPE_MENU_BAR (vim_menu_bar_get_type())
+G_DECLARE_FINAL_TYPE(VimMenuBar, vim_menu_bar, VIM, MENU_BAR, GtkWidget)
+
+# define VIM_TYPE_MENU_ITEM (vim_menu_item_get_type())
+G_DECLARE_FINAL_TYPE(VimMenuItem, vim_menu_item, VIM, MENU_ITEM, GtkButton)
+
+# define VIM_TYPE_MENU (vim_menu_get_type())
+G_DECLARE_FINAL_TYPE(VimMenu, vim_menu, VIM, MENU, GtkPopover)
+
+typedef enum
+{
+ VIM_MENU_ITEM_CLICKED,
+ VIM_MENU_ITEM_SELECTED
+} VimMenuItemEvent;
+
+typedef void (*VimMenuItemFunc)(VimMenuItem *item, VimMenuItemEvent event, void *udata);
+
+GtkWidget *vim_menu_bar_item_new(const char *text, VimMenu *menu);
+void vim_menu_bar_item_set_text(VimMenuBarItem *self, const char *text);
+
+GtkWidget *vim_menu_bar_new(void);
+GtkWidget *vim_menu_bar_to_menu(VimMenuBar *self);
+void vim_menu_bar_insert_item(VimMenuBar *self, VimMenuBarItem *item, int idx);
+void vim_menu_bar_remove(VimMenuBar *self, GtkWidget *item);
+void vim_menu_bar_show(VimMenuBar *self, VimMenuBarItem *item);
+
+GtkWidget *vim_menu_item_new(const char *text, VimMenuItemFunc func, void *udata);
+void vim_menu_item_set_text(VimMenuItem *self, const char *text);
+void vim_menu_item_set_accel(VimMenuItem *self, const char *accel_text);
+void vim_menu_item_set_submenu(VimMenuItem *self, VimMenu *submenu);
+
+GtkWidget *vim_menu_new(void);
+void vim_menu_insert_item(VimMenu *self, VimMenuItem *item, int idx);
+GtkWidget *vim_menu_insert_separator(VimMenu *self, int idx);
+void vim_menu_remove(VimMenu *self, GtkWidget *item);
+GtkWidget *vim_menu_copy(VimMenu *self);
+
+#endif
+
+#endif
diff --git a/src/gui_gtk4_tb.c b/src/gui_gtk4_tb.c
index 79682cfb1..1971a4728 100644
--- a/src/gui_gtk4_tb.c
+++ b/src/gui_gtk4_tb.c
@@ -92,6 +92,8 @@ vim_toolbar_init(VimToolbar *self)
self->overflow_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
gtk_popover_set_child(GTK_POPOVER(popover), self->overflow_box);
gtk_popover_set_has_arrow(GTK_POPOVER(popover), FALSE);
+ // Make sure popover aligns to top right of button
+ gtk_widget_set_halign(popover, GTK_ALIGN_END);

gtk_menu_button_set_popover(GTK_MENU_BUTTON(self->overflow_btn), popover);
}
diff --git a/src/menu.c b/src/menu.c
index 5fd83d80b..861d78711 100644
--- a/src/menu.c
+++ b/src/menu.c
@@ -1074,10 +1074,6 @@ free_menu(vimmenu_T **menup)
// Also may rebuild a tearoff'ed menu
if (gui.in_use)
gui_mch_destroy_menu(menu);
-# ifdef USE_GTK4
- // GTK4 uses "menu->label" for action name
- vim_free((char_u *)menu->label);
-# endif
#endif

// Don't change *menup until after calling gui_mch_destroy_menu(). The
diff --git a/src/structs.h b/src/structs.h
index 70010567d..b112c6d18 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -4733,7 +4733,9 @@ struct VimMenu
# if defined(GTK_CHECK_VERSION) && !GTK_CHECK_VERSION(3,4,0)
GtkWidget *tearoff_handle;
# endif
+# ifndef USE_GTK4
GtkWidget *label; // Used by "set wak=" code.
+# endif
# endif
# ifdef FEAT_GUI_MOTIF
int sensitive; // turn button on/off
diff --git a/src/version.c b/src/version.c
index 7fd7b6b80..8d517c22f 100644
--- a/src/version.c
+++ b/src/version.c
@@ -759,6 +759,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 719,
/**/
718,
/**/
Reply all
Reply to author
Forward
0 new messages