patch 9.2.0659: GTK4: no balloon support in GUI

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

    patch 9.2.0659: GTK4: no balloon support in GUI
    
    Problem:  GTK4: no balloon support in GUI
    Solution: Implement balloon_eval() (Foxe Chen).
    
    closes: #20527
    
    Signed-off-by: Foxe Chen <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/src/Makefile b/src/Makefile
index f66d94f1a..a5f23612c 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -1241,12 +1241,14 @@ GTK_BUNDLE      =
 ### GTK4 GUI
 GTK4_SRC       = gui.c gui_gtk4.c gui_gtk4_f.c \
                        gui_gtk4_cb.c \
-                       gui_gtk4_da.c \
+                       gui_gtk4_da.c \
+                       gui_beval.o \
                        $(GRESOURCE_SRC)
 GTK4_OBJ       = objects/gui.o objects/gui_gtk4.o \
                        objects/gui_gtk4_f.o \
                        objects/gui_gtk4_cb.o \
                        objects/gui_gtk4_da.o \
+                       objects/gui_beval.o \
                        $(GRESOURCE_OBJ)
 GTK4_DEFS      = -DFEAT_GUI_GTK $(NARROW_PROTO)
 GTK4_IPATH     = $(GUI_INC_LOC)
diff --git a/src/beval.c b/src/beval.c
index 14288e8af..0f1af845e 100644
--- a/src/beval.c
+++ b/src/beval.c
@@ -335,6 +335,8 @@ general_beval_cb(BalloonEval *beval, int state UNUSED)
            recursive = FALSE;
            return;
        }
+       else
+           vim_free(text);
     }
 # endif
 # ifdef FEAT_NETBEANS_INTG
diff --git a/src/gui_beval.c b/src/gui_beval.c
index c928d2568..fc653ba4d 100644
--- a/src/gui_beval.c
+++ b/src/gui_beval.c
@@ -48,15 +48,21 @@ static BalloonEval *current_beval = NULL;
 # ifdef FEAT_GUI_GTK
 static void addEventHandler(GtkWidget *, BalloonEval *);
 static void removeEventHandler(BalloonEval *);
+#  ifdef USE_GTK4
+static gboolean event_cb(GtkEventController *, GdkEvent *, BalloonEval *);
+#  else
 static gint target_event_cb(GtkWidget *, GdkEvent *, gpointer);
 static gint mainwin_event_cb(GtkWidget *, GdkEvent *, gpointer);
+#  endif
 static void pointer_event(BalloonEval *, int, int, unsigned);
 static void key_event(BalloonEval *, unsigned, int);
 static gboolean timeout_cb(gpointer);
-#  if GTK_CHECK_VERSION(3,0,0)
+#  ifndef USE_GTK4
+#   if GTK_CHECK_VERSION(3,0,0)
 static gboolean balloon_draw_event_cb (GtkWidget *, cairo_t *, gpointer);
-#  else
+#   else
 static gint balloon_expose_event_cb (GtkWidget *, GdkEventExpose *, gpointer);
+#   endif
 #  endif
 # else
 static void addEventHandler(Widget, BalloonEval *);
@@ -140,7 +146,7 @@ gui_mch_create_beval_area(
     return beval;
 }
 
-# if defined(FEAT_BEVAL_TIP)
+# if defined(FEAT_BEVAL_TIP) || defined(USE_GTK4)
 /*
  * Destroy a balloon-eval and free its associated memory.
  */
@@ -151,7 +157,11 @@ gui_mch_destroy_beval_area(BalloonEval *beval)
     removeEventHandler(beval);
     // Children will automatically be destroyed
 #  ifdef FEAT_GUI_GTK
+#   ifdef USE_GTK4
+    gtk_widget_unparent(beval->balloonShell);
+#   else
     gtk_widget_destroy(beval->balloonShell);
+#   endif
 #  else
     XtDestroyWidget(beval->balloonShell);
 #  endif
@@ -228,6 +238,14 @@ gui_mch_unpost_balloon(BalloonEval *beval)
     static void
 addEventHandler(GtkWidget *target, BalloonEval *beval)
 {
+#  ifdef USE_GTK4
+    GtkEventController *controller = gtk_event_controller_legacy_new();
+
+    g_signal_connect(controller, "event", G_CALLBACK(event_cb), beval);
+
+    g_object_set_data(G_OBJECT(target), "beval", controller);
+    gtk_widget_add_controller(target, controller);
+#  else
     /*
      * Connect to the generic "event" signal instead of the individual
      * signals for each event type, because the former is emitted earlier.
@@ -252,11 +270,19 @@ addEventHandler(GtkWidget *target, BalloonEval *beval)
                         G_CALLBACK(mainwin_event_cb),
                         beval);
     }
+#  endif
 }
 
     static void
 removeEventHandler(BalloonEval *beval)
 {
+#  ifdef USE_GTK4
+    GtkWidget          *target = beval->target;
+    GtkEventController *controller;
+
+    controller = g_object_get_data(G_OBJECT(target), "beval");
+    gtk_widget_remove_controller(target, controller);
+#  else
     g_signal_handlers_disconnect_by_func(G_OBJECT(beval->target),
                                         FUNC2GENERIC(target_event_cb),
                                         beval);
@@ -268,8 +294,64 @@ removeEventHandler(BalloonEval *beval)
                                             FUNC2GENERIC(mainwin_event_cb),
                                             beval);
     }
+#  endif
 }
 
+#  ifdef USE_GTK4
+    static gboolean
+event_cb(GtkEventController *controller, GdkEvent *event, BalloonEval *beval)
+{
+    int type = gdk_event_get_event_type(event);
+
+    switch (type)
+    {
+       case GDK_ENTER_NOTIFY:
+       case GDK_MOTION_NOTIFY:
+       {
+           GtkWidget           *root;
+           graphene_point_t    p;
+           graphene_point_t    result;
+           double              x = 0, y = 0;
+
+           // x and y coords returned by gdk_event_get_position are relative to
+           // the surface, not the widget, must convert them into widget local
+           // coordinates.
+           root = GTK_WIDGET(gtk_widget_get_root(beval->target));
+           gdk_event_get_position(event, &x, &y);
+           p.x = x;
+           p.y = y;
+
+           if (gtk_widget_compute_point(root, beval->target, &p, &result))
+           {
+               x = result.x;
+               y = result.y;
+           }
+
+           pointer_event(beval, (int)x, (int)y,
+                   gdk_event_get_modifier_state(event));
+           break;
+       }
+       case GDK_LEAVE_NOTIFY:
+           if (gdk_crossing_event_get_mode(event) == GDK_CROSSING_NORMAL)
+               cancelBalloon(beval);
+           break;
+       case GDK_BUTTON_PRESS:
+       case GDK_SCROLL:
+           cancelBalloon(beval);
+           break;
+       case GDK_KEY_PRESS:
+       case GDK_KEY_RELEASE:
+           key_event(beval, gdk_key_event_get_keyval(event),
+                   type == GDK_KEY_PRESS);
+           break;
+       default:
+           break;
+    }
+
+    // Always pass through events?
+    return FALSE;
+}
+#  else
     static gint
 target_event_cb(GtkWidget *widget, GdkEvent *event, gpointer data)
 {
@@ -292,22 +374,22 @@ target_event_cb(GtkWidget *widget, GdkEvent *event, 
gpointer data)
                 * GDK_POINTER_MOTION_HINT_MASK is set, thus we cannot obtain
                 * the coordinates from the GdkEventMotion struct directly.
                 */
-#  if GTK_CHECK_VERSION(3,0,0)
+#   if GTK_CHECK_VERSION(3,0,0)
                {
                    GdkWindow * const win = gtk_widget_get_window(widget);
                    GdkDisplay * const dpy = gdk_window_get_display(win);
-#   if GTK_CHECK_VERSION(3,20,0)
+#    if GTK_CHECK_VERSION(3,20,0)
                    GdkSeat * const seat = gdk_display_get_default_seat(dpy);
                    GdkDevice * const dev = gdk_seat_get_pointer(seat);
-#   else
+#    else
                    GdkDeviceManager * const mngr = 
gdk_display_get_device_manager(dpy);
                    GdkDevice * const dev = 
gdk_device_manager_get_client_pointer(mngr);
-#   endif
+#    endif
                    gdk_window_get_device_position(win, dev , &x, &y, &state);
                }
-#  else
+#   else
                gdk_window_get_pointer(widget->window, &x, &y, &state);
-#  endif
+#   endif
                pointer_event(beval, x, y, (unsigned int)state);
            }
            else
@@ -367,6 +449,7 @@ mainwin_event_cb(GtkWidget *widget UNUSED, GdkEvent *event, 
gpointer data)
 
     return FALSE; // continue emission
 }
+#  endif
 
     static void
 pointer_event(BalloonEval *beval, int x, int y, unsigned state)
@@ -472,7 +555,8 @@ timeout_cb(gpointer data)
     return FALSE; // don't call me again
 }
 
-#  if GTK_CHECK_VERSION(3,0,0)
+#  ifndef USE_GTK4
+#   if GTK_CHECK_VERSION(3,0,0)
     static gboolean
 balloon_draw_event_cb(GtkWidget *widget,
                      cairo_t   *cr,
@@ -502,7 +586,7 @@ balloon_draw_event_cb(GtkWidget *widget,
 
     return FALSE;
 }
-#  else
+#   else
     static gint
 balloon_expose_event_cb(GtkWidget *widget,
                        GdkEventExpose *event,
@@ -515,7 +599,8 @@ balloon_expose_event_cb(GtkWidget *widget,
 
     return FALSE; // continue emission
 }
-#  endif // !GTK_CHECK_VERSION(3,0,0)
+#   endif // !GTK_CHECK_VERSION(3,0,0)
+#  endif
 
 # else // !FEAT_GUI_GTK
 
@@ -913,6 +998,39 @@ set_printable_label_text(GtkLabel *label, char_u *text)
 /*
  * Draw a balloon.
  */
+#  ifdef USE_GTK4
+    static void
+drawBalloon(BalloonEval *beval)
+{
+    GdkRectangle    rect;
+    int                    natural_width = 0;
+
+    if (beval->msg == NULL)
+       return;
+
+    set_printable_label_text(GTK_LABEL(beval->balloonLabel), beval->msg);
+
+    rect.x = beval->x;
+    rect.y = beval->y;
+
+    // Make popover appear at the bottom right of the pointer. Also add an
+    // offset (gui.char_width * 3, gui.char_height), so that popover doesn't
+    // block the text. This also prevents the popover from stealing motion
+    // events, making pressing alt (to force show beval) and moving cursor
+    // around feel smooth.
+    gtk_widget_measure(beval->balloonLabel, GTK_ORIENTATION_HORIZONTAL, -1,
+           NULL, &natural_width, NULL, NULL);
+
+    rect.width = (natural_width > 0 ? natural_width : 1) + gui.char_width * 3;
+    rect.height = gui.char_height;
+
+    gtk_popover_set_pointing_to(GTK_POPOVER(beval->balloonShell), &rect);
+    gtk_popover_popup(GTK_POPOVER(beval->balloonShell));
+
+    beval->showState = ShS_SHOWING;
+    gui_mch_update();
+}
+#  else
     static void
 drawBalloon(BalloonEval *beval)
 {
@@ -930,18 +1048,18 @@ drawBalloon(BalloonEval *beval)
     int                y_offset = EVAL_OFFSET_Y;
     PangoLayout        *layout;
 
-#  if !GTK_CHECK_VERSION(3,22,2)
+#   if !GTK_CHECK_VERSION(3,22,2)
     GdkScreen  *screen;
 
     screen = gtk_widget_get_screen(beval->target);
     gtk_window_set_screen(GTK_WINDOW(beval->balloonShell), screen);
-#  endif
+#   endif
     gui_gtk_get_screen_geom_of_win(beval->target, 0, 0,
            &screen_x, &screen_y, &screen_w, &screen_h);
-#  if !GTK_CHECK_VERSION(3,0,0)
+#   if !GTK_CHECK_VERSION(3,0,0)
     gtk_widget_ensure_style(beval->balloonShell);
     gtk_widget_ensure_style(beval->balloonLabel);
-#  endif
+#   endif
 
     set_printable_label_text(GTK_LABEL(beval->balloonLabel), beval->msg);
     /*
@@ -950,11 +1068,11 @@ drawBalloon(BalloonEval *beval)
      * builtin maximum value of about 65 Latin characters.
      */
     layout = gtk_label_get_layout(GTK_LABEL(beval->balloonLabel));
-#  ifdef PANGO_WRAP_WORD_CHAR
+#   ifdef PANGO_WRAP_WORD_CHAR
     pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
-#  else
+#   else
     pango_layout_set_wrap(layout, PANGO_WRAP_WORD);
-#  endif
+#   endif
     pango_layout_set_width(layout,
            // try to come up with some reasonable width
            PANGO_SCALE * CLAMP(gui.num_cols * gui.char_width,
@@ -962,11 +1080,11 @@ drawBalloon(BalloonEval *beval)
                MAX(20, screen_w - 20)));
 
     // Calculate the balloon's width and height.
-#  if GTK_CHECK_VERSION(3,0,0)
+#   if GTK_CHECK_VERSION(3,0,0)
     gtk_widget_get_preferred_size(beval->balloonShell, &requisition, NULL);
-#  else
+#   else
     gtk_widget_size_request(beval->balloonShell, &requisition);
-#  endif
+#   endif
 
     // Compute position of the balloon area
     gdk_window_get_origin(gtk_widget_get_window(beval->target), &x, &y);
@@ -986,16 +1104,17 @@ drawBalloon(BalloonEval *beval)
            MAX(0, screen_y + screen_h - requisition.height));
 
     // Show the balloon
-#  if GTK_CHECK_VERSION(3,0,0)
+#   if GTK_CHECK_VERSION(3,0,0)
     gtk_window_move(GTK_WINDOW(beval->balloonShell), x, y);
-#  else
+#   else
     gtk_widget_set_uposition(beval->balloonShell, x, y);
-#  endif
+#   endif
     gtk_widget_show(beval->balloonShell);
 
     beval->showState = ShS_SHOWING;
     gui_mch_update();
 }
+#  endif
 
 /*
  * Undraw a balloon.
@@ -1004,7 +1123,11 @@ drawBalloon(BalloonEval *beval)
 undrawBalloon(BalloonEval *beval)
 {
     if (beval->balloonShell != NULL)
+#  ifdef USE_GTK4
+       gtk_popover_popdown(GTK_POPOVER(beval->balloonShell));
+#  else
        gtk_widget_hide(beval->balloonShell);
+#  endif
     beval->showState = ShS_NEUTRAL;
 }
 
@@ -1023,6 +1146,29 @@ cancelBalloon(BalloonEval *beval)
     beval->showState = ShS_NEUTRAL;
 }
 
+#  ifdef USE_GTK4
+    static void
+createBalloonEvalWindow(BalloonEval *beval)
+{
+    GtkWidget *popover = gtk_popover_new();
+    beval->balloonShell = popover;
+
+    gtk_widget_set_parent(beval->balloonShell, beval->target);
+    gtk_popover_set_has_arrow(GTK_POPOVER(popover), FALSE);
+    gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
+    // Don't make popover grab focus
+    gtk_popover_set_autohide(GTK_POPOVER(popover), FALSE);
+
+    beval->balloonLabel = gtk_label_new(NULL);
+
+    gtk_label_set_wrap(GTK_LABEL(beval->balloonLabel), FALSE);
+    gtk_label_set_justify(GTK_LABEL(beval->balloonLabel), GTK_JUSTIFY_LEFT);
+    gtk_label_set_xalign(GTK_LABEL(beval->balloonLabel), 0.5);
+    gtk_label_set_yalign(GTK_LABEL(beval->balloonLabel), 0.5);
+
+    gtk_popover_set_child(GTK_POPOVER(popover), beval->balloonLabel);
+}
+#  else
     static void
 createBalloonEvalWindow(BalloonEval *beval)
 {
@@ -1034,35 +1180,36 @@ createBalloonEvalWindow(BalloonEval *beval)
     gtk_widget_set_name(beval->balloonShell, "gtk-tooltips");
     gtk_container_set_border_width(GTK_CONTAINER(beval->balloonShell), 4);
 
-#  if GTK_CHECK_VERSION(3,0,0)
+#   if GTK_CHECK_VERSION(3,0,0)
     g_signal_connect(G_OBJECT(beval->balloonShell), "draw",
-                    G_CALLBACK(balloon_draw_event_cb), NULL);
-#  else
+           G_CALLBACK(balloon_draw_event_cb), NULL);
+#   else
     gtk_signal_connect((GtkObject*)(beval->balloonShell), "expose_event",
-                      GTK_SIGNAL_FUNC(balloon_expose_event_cb), NULL);
-#  endif
+           GTK_SIGNAL_FUNC(balloon_expose_event_cb), NULL);
+#   endif
     beval->balloonLabel = gtk_label_new(NULL);
 
     gtk_label_set_line_wrap(GTK_LABEL(beval->balloonLabel), FALSE);
     gtk_label_set_justify(GTK_LABEL(beval->balloonLabel), GTK_JUSTIFY_LEFT);
-#  if GTK_CHECK_VERSION(3,16,0)
+#   if GTK_CHECK_VERSION(3,16,0)
     gtk_label_set_xalign(GTK_LABEL(beval->balloonLabel), 0.5);
     gtk_label_set_yalign(GTK_LABEL(beval->balloonLabel), 0.5);
-#  elif GTK_CHECK_VERSION(3,14,0)
+#   elif GTK_CHECK_VERSION(3,14,0)
     GValue align_val = G_VALUE_INIT;
     g_value_init(&align_val, G_TYPE_FLOAT);
     g_value_set_float(&align_val, 0.5);
     g_object_set_property(G_OBJECT(beval->balloonLabel), "xalign", &align_val);
     g_object_set_property(G_OBJECT(beval->balloonLabel), "yalign", &align_val);
     g_value_unset(&align_val);
-#  else
+#   else
     gtk_misc_set_alignment(GTK_MISC(beval->balloonLabel), 0.5f, 0.5f);
-#  endif
+#   endif
     gtk_widget_set_name(beval->balloonLabel, "vim-balloon-label");
     gtk_widget_show(beval->balloonLabel);
 
     gtk_container_add(GTK_CONTAINER(beval->balloonShell), beval->balloonLabel);
 }
+#  endif
 
 # else // !FEAT_GUI_GTK
 
diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c
index 90cd7aa3c..e87122bad 100644
--- a/src/gui_gtk4.c
+++ b/src/gui_gtk4.c
@@ -791,6 +791,11 @@ gui_mch_exit(int rc UNUSED)
 #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
+#ifdef FEAT_BEVAL_GUI
+       // 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
        gtk_window_destroy(GTK_WINDOW(gui.mainwin));
     }
@@ -4133,37 +4138,6 @@ clip_mch_lose_selection(Clipboard_T *cbd)
        gdk_clipboard_set_content(clipboard, NULL);
 }
 
-// Balloon eval - use GTK4 tooltip
-    void
-gui_mch_post_balloon(BalloonEval *beval UNUSED, char_u *mesg)
-{
-    if (mesg != NULL && gui.drawarea != NULL)
-    {
-       char_u *text = CONVERT_TO_UTF8(mesg);
-       gtk_widget_set_tooltip_text(gui.drawarea, (const char *)text);
-       CONVERT_TO_UTF8_FREE(text);
-    }
-    else if (gui.drawarea != NULL)
-       gtk_widget_set_tooltip_text(gui.drawarea, NULL);
-}
-
-    BalloonEval *
-gui_mch_create_beval_area(void *target UNUSED, char_u *mesg UNUSED,
-       void (*mesgCB)(BalloonEval *, int) UNUSED, void *clientData UNUSED)
-{
-    return NULL;
-}
-
-    void
-gui_mch_enable_beval_area(BalloonEval *beval UNUSED)
-{
-}
-
-    void
-gui_mch_disable_beval_area(BalloonEval *beval UNUSED)
-{
-}
-
 // GTK4 does not have gtk_main_level/gtk_main_quit.
 // Provide compatibility stubs using a simple flag.
     guint
@@ -5198,7 +5172,8 @@ gui_mch_dialog(
     // mnemonics without <Alt> key, but that behaviour comes from GTK+ 1.2 
(from
     // 1999!), so most users probably don't care...
     key_controller = gtk_event_controller_key_new();
-    g_signal_connect(key_controller, "key-pressed", 
G_CALLBACK(dialog_key_pressed_cb), &done);
+    g_signal_connect(key_controller, "key-pressed",
+           G_CALLBACK(dialog_key_pressed_cb), &done);
     gtk_widget_add_controller(GTK_WIDGET(win), key_controller);
 
     if (textfield != NULL)
diff --git a/src/proto/gui_gtk4.pro b/src/proto/gui_gtk4.pro
index 8c4c2ac41..acfb13c9a 100644
--- a/src/proto/gui_gtk4.pro
+++ b/src/proto/gui_gtk4.pro
@@ -86,10 +86,6 @@ void clip_mch_request_selection(Clipboard_T *cbd);
 void clip_mch_set_selection(Clipboard_T *cbd);
 int clip_mch_own_selection(Clipboard_T *cbd);
 void clip_mch_lose_selection(Clipboard_T *cbd);
-void gui_mch_post_balloon(BalloonEval *beval, char_u *mesg);
-BalloonEval *gui_mch_create_beval_area(void *target, char_u *mesg, void 
(*mesgCB)(BalloonEval *, int), void *clientData);
-void gui_mch_enable_beval_area(BalloonEval *beval);
-void gui_mch_disable_beval_area(BalloonEval *beval);
 guint gtk_main_level(void);
 void gtk_main_quit(void);
 void mch_set_mouse_shape(int shape);
diff --git a/src/version.c b/src/version.c
index 1dc1267b2..96dd5ff8f 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 */
+/**/
+    659,
 /**/
     658,
 /**/

-- 
-- 
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/E1wZZic-0010Bz-4z%40256bit.org.

Raspunde prin e-mail lui