patch 9.2.0657: GTK4: missing menu when right-clicking in tabline

Commit: 
https://github.com/vim/vim/commit/2a149bf34fd6713983f1d2cc589d00c6757a6f30
Author: Foxe Chen <[email protected]>
Date:   Tue Jun 16 19:19:33 2026 +0000

    patch 9.2.0657: GTK4: missing menu when right-clicking in tabline
    
    Problem:  GTK4: missing menu when right-clicking in tabline
    Solution: Add tabline menu for GTK4 GUI (similar to GTK4 GUI)
              (Foxe Chen).
    
    closes: #20518
    
    Signed-off-by: Foxe Chen <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c
index 82c9248a7..90cd7aa3c 100644
--- a/src/gui_gtk4.c
+++ b/src/gui_gtk4.c
@@ -284,8 +284,11 @@ static void focus_out_event(GtkEventControllerFocus 
*controller, gpointer data);
 static gboolean drop_cb(GtkDropTarget *target, const GValue *value, double x, 
double y, gpointer data);
 #endif
 #ifdef FEAT_GUI_TABLINE
+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 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);
 static gboolean delete_event_cb(GtkWindow *window, gpointer data);
@@ -506,6 +509,47 @@ gui_mch_init(void)
                     G_CALLBACK(on_select_tab), NULL);
     g_signal_connect(G_OBJECT(gui.tabline), "page-reordered",
                     G_CALLBACK(on_tab_reordered), NULL);
+
+    {
+       GtkEventController *mcontroller = gtk_event_controller_motion_new();
+
+       g_signal_connect(mcontroller, "enter",
+               G_CALLBACK(tabline_enter_cb), NULL);
+
+       // Make sure that tabline_enter_cb() is always called before
+       // tabpage_enter_cb().
+       gtk_event_controller_set_propagation_phase(
+               mcontroller, GTK_PHASE_CAPTURE);
+       gtk_widget_add_controller(gui.tabline, mcontroller);
+    }
+
+    // Create right click popup menu for tabline
+    {
+       GtkGesture      *click;
+       GActionGroup    *agroup;
+       GMenu           *menu;
+       GtkWidget       *popover;
+
+       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);
+
+       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_popover_set_has_arrow(GTK_POPOVER(popover), FALSE);
+       gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
+
+       // 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);
+       gtk_widget_add_controller(gui.tabline, GTK_EVENT_CONTROLLER(click));
+    }
 #endif
 
     // The form widget manages absolute positioning of scrollbars and the draw
@@ -743,7 +787,13 @@ gui_mch_open(void)
 gui_mch_exit(int rc UNUSED)
 {
     if (gui.mainwin != NULL)
+    {
+#ifdef FEAT_GUI_TABLINE
+       // Must unparent popover menu for tabline or we will get warning
+       gtk_widget_unparent(g_object_get_data(G_OBJECT(gui.tabline), "menu"));
+#endif
        gtk_window_destroy(GTK_WINDOW(gui.mainwin));
+    }
 }
 
     int
@@ -2737,6 +2787,28 @@ gui_mch_showing_tabline(void)
 }
 
 static int ignore_tabline_evt = FALSE;
+// Page hovered over in tab line, zero indicates none
+static int tabpage_hover = 0;
+
+    static void
+tabpage_enter_cb(
+       GtkEventController  *controller UNUSED,
+       double              x UNUSED,
+       double              y UNUSED,
+       void                *udata)
+{
+    tabpage_hover = GPOINTER_TO_INT(udata);
+}
+
+    static void
+tabline_enter_cb(
+       GtkEventController  *controller UNUSED,
+       double              x UNUSED,
+       double              y UNUSED,
+       void                *udata UNUSED)
+{
+    tabpage_hover = 0;
+}
 
     void
 gui_mch_update_tabline(void)
@@ -2757,6 +2829,10 @@ gui_mch_update_tabline(void)
 
     for (tp = first_tabpage; tp != NULL; tp = tp->tp_next, ++nr)
     {
+       GtkNotebookPage     *page_widget;
+       GtkWidget           *tab_widget;
+       GtkEventController  *mcontroller;
+
        if (tp == curtab)
            curtabidx = nr;
 
@@ -2778,9 +2854,18 @@ gui_mch_update_tabline(void)
                    page, TRUE);
        }
 
-       event_box = gtk_notebook_get_tab_label(GTK_NOTEBOOK(gui.tabline), page);
-       g_object_set_data(G_OBJECT(event_box), "tab_num",
+       // Attach motion event controller to detect which tab page is currently
+       // hovered over.
+       page_widget = gtk_notebook_get_page(GTK_NOTEBOOK(gui.tabline), page);
+       mcontroller = gtk_event_controller_motion_new();
+       g_signal_connect(mcontroller, "enter", G_CALLBACK(tabpage_enter_cb),
                GINT_TO_POINTER(tab_num));
+       gtk_event_controller_set_propagation_phase(
+               mcontroller, GTK_PHASE_CAPTURE);
+       g_object_get(page_widget, "tab", &tab_widget, NULL);
+       gtk_widget_add_controller(tab_widget, mcontroller);
+
+       event_box = gtk_notebook_get_tab_label(GTK_NOTEBOOK(gui.tabline), page);
        label = gtk_widget_get_first_child(event_box);
        get_tabline_label(tp, FALSE);
        labeltext = CONVERT_TO_UTF8(NameBuff);
@@ -2842,6 +2927,96 @@ on_tab_reordered(
     else
        tabpage_move(idx);
 }
+
+/*
+ * Handle selecting an item in the tab line popup menu.
+ */
+    static void
+tabline_menu_action_cb(
+       GSimpleAction   *action UNUSED,
+       GVariant        *parameter UNUSED,
+       void            *udata)
+{
+    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)
+{
+    GSimpleAction *act = g_simple_action_new(action, NULL);
+    char detailed[32];
+
+    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);
+}
+
+/*
+ * Create a menu for the tab line.
+ */
+    static GMenu *
+create_tabline_popup_menu(GActionGroup **agroup_store)
+{
+    GMenu              *gmenu = g_menu_new();
+    GSimpleActionGroup *agroup = g_simple_action_group_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);
+
+    *agroup_store = G_ACTION_GROUP(agroup);
+    return gmenu;
+}
+
+    static void
+tabline_menu_press_event(
+       GtkGestureClick *gesture,
+       int             n_press UNUSED,
+       double          x,
+       double          y,
+       GtkWidget       *popover)
+{
+    guint but;
+
+    but = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture));
+    if (but == GDK_BUTTON_SECONDARY)
+    {
+       // Right mouse button pressed, popup menu
+       GdkRectangle rect;
+       rect.x = x;
+       rect.y = y;
+       rect.width = 1;
+       rect.height = 1;
+
+       gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect);
+       gtk_popover_popup(GTK_POPOVER(popover));
+    }
+    else if (but == GDK_BUTTON_PRIMARY)
+    {
+       if (tabpage_hover == 0)
+           // Click after all tabs moves to next tab page.  When "x" is
+           // small guess it's the left button.
+           send_tabline_event(x < 50 ? -1 : 0);
+    }
+    else if (but == GDK_BUTTON_MIDDLE)
+    {
+       if (tabpage_hover != 0)
+           // Middle mouse click on tabpage label closes that tab.
+           send_tabline_menu_event(tabpage_hover, TABLINE_MENU_CLOSE);
+    }
+}
 #endif
 
 /*
diff --git a/src/po/vim.pot b/src/po/vim.pot
index fa6d37d69..b185a5144 100644
--- a/src/po/vim.pot
+++ b/src/po/vim.pot
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: Vim
"
 "Report-Msgid-Bugs-To: [email protected]
"
-"POT-Creation-Date: 2026-06-13 19:24+0000
"
+"POT-Creation-Date: 2026-06-16 19:21+0000
"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>
"
 "Language-Team: LANGUAGE <[email protected]>
"
@@ -956,6 +956,15 @@ msgstr ""
 msgid "_Close"
 msgstr ""
 
+msgid "Close Tab"
+msgstr ""
+
+msgid "New Tab"
+msgstr ""
+
+msgid "Open Tab..."
+msgstr ""
+
 msgid "Direction:"
 msgstr ""
 
@@ -971,9 +980,6 @@ msgstr ""
 msgid "New tab"
 msgstr ""
 
-msgid "Open Tab..."
-msgstr ""
-
 msgid "Vim: Main window unexpectedly destroyed
"
 msgstr ""
 
diff --git a/src/version.c b/src/version.c
index 864688f60..fef84f283 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 */
+/**/
+    657,
 /**/
     656,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1wZZU6-000z3Z-Vj%40256bit.org.

Raspunde prin e-mail lui