]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0719: GTK4: default menu is lacking v9.2.0719
authorFoxe Chen <chen.foxe@gmail.com>
Wed, 24 Jun 2026 18:19:53 +0000 (18:19 +0000)
committerChristian Brabandt <cb@256bit.org>
Wed, 24 Jun 2026 18:19:53 +0000 (18:19 +0000)
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.foxe@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
12 files changed:
Filelist
runtime/doc/gui_x11.txt
runtime/doc/tags
src/Makefile
src/gui_gtk4.c
src/gui_gtk4_da.c
src/gui_gtk4_menu.c [new file with mode: 0644]
src/gui_gtk4_menu.h [new file with mode: 0644]
src/gui_gtk4_tb.c
src/menu.c
src/structs.h
src/version.c

index 019fa1aeeac8f70657cde4bce7eaf70f9be9b0f8..c073e1ad6d19ea88c7371b1c992d6341d3c01173 100644 (file)
--- 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 \
index 2e558d8a0a4f2477f37a958495007a1d286d1c43..8a084505e81b209cecded153e911901d41791339 100644 (file)
@@ -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:
index 11ae058bbcfc75724cfe5bdfab9433a9ce9bf54c..3abd3e035e885efcd4a4eb5f1b94c23aad229e05 100644 (file)
@@ -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*
index 89e023c9d05adfa1d4d77e8cd9f8fe716c44bd0c..e0f7057b8323cbacdeb27bb1428bc70410104fc8 100644 (file)
@@ -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 \
index fe9381f2f6e4c11ac8fc862c5effcf9abef7cbe1..b5f1f93b406a2a8541bc84b35df15fb8c360d576 100644 (file)
@@ -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));
 }
 
 /*
index 11b7463892ae024904440b4224dc16d5ce9db75d..8db224ea67868ed4b71853b302f9011d46f6ff3f 100644 (file)
@@ -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 (file)
index 0000000..174f2f7
--- /dev/null
@@ -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 (file)
index 0000000..f6965e6
--- /dev/null
@@ -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
index 79682cfb13a10f7f7acd54fce39bc01726ca4aca..1971a4728eea941350b85e783ef53d550d43c920 100644 (file)
@@ -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);
 }
index 5fd83d80b5ff1fdaccffbe22cd56da4bd6a8d8e0..861d787112ec66256fedeeb4ce7f6b1d0c91d6cf 100644 (file)
@@ -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
index 70010567d2abd9e7a990db88fd5f03a313df7b77..b112c6d182892b8bdb91e49d948801d6c37d0e84 100644 (file)
@@ -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
index 7fd7b6b8013710865811f3229174bba22e26852e..8d517c22f80999ae5289355dee981d77c43146a8 100644 (file)
@@ -759,6 +759,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    719,
 /**/
     718,
 /**/