From: Foxe Chen Date: Wed, 24 Jun 2026 18:19:53 +0000 (+0000) Subject: patch 9.2.0719: GTK4: default menu is lacking X-Git-Tag: v9.2.0719^0 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=037a19e1c1f737abc560ffc1d2edf2b535194816;p=thirdparty%2Fvim.git 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 Signed-off-by: Christian Brabandt --- diff --git a/Filelist b/Filelist index 019fa1aeea..c073e1ad6d 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 2e558d8a0a..8a084505e8 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 ~ + Go to next item + Go to previous item + Go to parent submenu + Go to current item's submenu + Go to next menu bar item + 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 11ae058bbc..3abd3e035e 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 89e023c9d0..e0f7057b83 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 fe9381f2f6..b5f1f93b40 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(¶m_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 + * 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." 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 11b7463892..8db224ea67 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 0000000000..174f2f772b --- /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 +#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 0000000000..f6965e6dc0 --- /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 + +# 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 79682cfb13..1971a4728e 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 5fd83d80b5..861d787112 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 70010567d2..b112c6d182 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 7fd7b6b801..8d517c22f8 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, /**/