[gtksourceview] hoverassistant: implement dismissal without grabs



commit a0f168f886283181ede6c517596b913b0799b928
Author: Christian Hergert <chergert redhat com>
Date:   Wed Aug 31 12:21:34 2022 -0700

    hoverassistant: implement dismissal without grabs
    
    When using gtk_popover_set_autohide(..., TRUE), we incur device grabs
    implicitely by GDK via GdkPopup:autohide. Those grabs can be quite
    difficult to break simply using motion/crossing events.
    
    Instead, this manages a few motion controllers (including a shared motion
    controller on the GtkRoot) to track when pointer motion has left in such
    a way that we want to dismiss the popover.
    
    Fixes https://gitlab.gnome.org/GNOME/gnome-builder/-/issues/1749

 gtksourceview/gtksourcehoverassistant.c | 349 ++++++++++++++++++++++----------
 1 file changed, 247 insertions(+), 102 deletions(-)
---
diff --git a/gtksourceview/gtksourcehoverassistant.c b/gtksourceview/gtksourcehoverassistant.c
index 150b8455..7de6cb81 100644
--- a/gtksourceview/gtksourcehoverassistant.c
+++ b/gtksourceview/gtksourcehoverassistant.c
@@ -21,104 +21,267 @@
 
 #include "config.h"
 
+#include <string.h>
+
 #include "gtksourceassistant-private.h"
 #include "gtksourcehoverassistant-private.h"
 #include "gtksourcehovercontext-private.h"
 #include "gtksourcehoverdisplay-private.h"
+#include "gtksourcehover-private.h"
 #include "gtksourceview.h"
 
-#define GRACE_X 20
-#define GRACE_Y 20
+#define GTK_SOURCE_HOVER_ASSISTANT_MOTION "GTK_SOURCE_HOVER_ASSISTANT_MOTION"
 
 struct _GtkSourceHoverAssistant
 {
        GtkSourceAssistant parent_instance;
+
+       GtkEventController *popover_motion;
+       GtkEventController *root_motion;
+
        GtkSourceHoverDisplay *display;
+
        GCancellable *cancellable;
+
        GdkRectangle hovered_at;
+
+       double root_x;
+       double root_y;
+
+       gulong root_motion_handler;
+       gulong root_leave_handler;
+
+       GSource *dismiss_source;
 };
 
 G_DEFINE_TYPE (GtkSourceHoverAssistant, gtk_source_hover_assistant, GTK_SOURCE_TYPE_ASSISTANT)
 
+static gboolean
+gtk_source_hover_assistant_should_dismiss (GtkSourceHoverAssistant *self)
+{
+       GdkSurface *surface;
+
+       g_assert (GTK_SOURCE_IS_HOVER_ASSISTANT (self));
+
+       if (gtk_event_controller_motion_contains_pointer (GTK_EVENT_CONTROLLER_MOTION (self->popover_motion)))
+       {
+               return FALSE;
+       }
+
+       if ((surface = gtk_native_get_surface (GTK_NATIVE (self))))
+       {
+               GdkRectangle popup_area, root_area;
+               double popup_x, popup_y;
+               double transform_x, transform_y;
+               GtkRoot *root;
+
+               popup_x = gdk_popup_get_position_x (GDK_POPUP (surface));
+               popup_y = gdk_popup_get_position_y (GDK_POPUP (surface));
+               gtk_native_get_surface_transform (GTK_NATIVE (self), &transform_x, &transform_y);
+
+               popup_area.x = popup_x - transform_x;
+               popup_area.y = popup_y - transform_y;
+               popup_area.width = gdk_surface_get_width (surface);
+               popup_area.height = gdk_surface_get_height (surface);
+
+               root = gtk_widget_get_root (GTK_WIDGET (self));
+               root_area.x = 0;
+               root_area.y = 0;
+               root_area.width = gtk_widget_get_width (GTK_WIDGET (root));
+               root_area.height = gtk_widget_get_height (GTK_WIDGET (root));
+
+               if (gdk_rectangle_intersect (&root_area, &popup_area, &popup_area) &&
+                   gdk_rectangle_contains_point (&popup_area, self->root_x, self->root_y))
+               {
+                       return FALSE;
+               }
+       }
+
+       return TRUE;
+}
+
+static gboolean
+gtk_source_hover_assistant_dismiss_cb (GtkSourceHoverAssistant *self)
+{
+       g_assert (GTK_SOURCE_IS_HOVER_ASSISTANT (self));
+
+       g_clear_pointer (&self->dismiss_source, g_source_destroy);
+
+       if (gtk_source_hover_assistant_should_dismiss (self))
+       {
+               _gtk_source_hover_assistant_dismiss (self);
+       }
+
+       return G_SOURCE_REMOVE;
+}
+
 static void
-gtk_source_hover_assistant_get_target_location (GtkSourceAssistant *assistant,
-                                                GdkRectangle       *rect)
+gtk_source_hover_assistant_queue_dismiss (GtkSourceHoverAssistant *self)
 {
-       *rect = GTK_SOURCE_HOVER_ASSISTANT (assistant)->hovered_at;
+       g_assert (GTK_SOURCE_IS_HOVER_ASSISTANT (self));
+
+       if (self->dismiss_source != NULL)
+       {
+               return;
+       }
+
+       self->dismiss_source = g_idle_source_new ();
+       g_source_set_name (self->dismiss_source, "gtk-source-hover-assistant-timeout");
+       g_source_set_callback (self->dismiss_source, (GSourceFunc)gtk_source_hover_assistant_dismiss_cb, 
self, NULL);
+       g_source_attach (self->dismiss_source, NULL);
+       g_source_unref (self->dismiss_source);
 }
 
 static void
-gtk_source_hover_assistant_motion_cb (GtkSourceHoverAssistant  *self,
-                                      double                    x,
-                                      double                    y,
-                                      GtkEventControllerMotion *controller)
+gtk_source_hover_assistant_popover_leave_cb (GtkSourceHoverAssistant  *self,
+                                             GtkEventControllerMotion *controller)
 {
-       GdkSurface *assistant_surface;
-       GtkWidget *parent;
-       GtkRoot *root;
-       double tx, ty;
-       int width, height;
+       g_assert (GTK_SOURCE_IS_HOVER_ASSISTANT (self));
+       g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (controller));
+
+       gtk_source_hover_assistant_queue_dismiss (self);
+}
 
+static void
+gtk_source_hover_assistant_root_leave_cb (GtkSourceHoverAssistant  *self,
+                                          GtkEventControllerMotion *controller)
+{
+       g_assert (GTK_SOURCE_IS_HOVER_ASSISTANT (self));
+       g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (controller));
+
+       gtk_source_hover_assistant_queue_dismiss (self);
+}
+
+static void
+gtk_source_hover_assistant_root_motion_cb (GtkSourceHoverAssistant  *self,
+                                           double                    x,
+                                           double                    y,
+                                           GtkEventControllerMotion *controller)
+{
        g_assert (GTK_SOURCE_IS_HOVER_ASSISTANT (self));
        g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (controller));
 
-       if (gtk_event_controller_motion_contains_pointer (controller))
+       self->root_x = x;
+       self->root_y = y;
+
+       gtk_source_hover_assistant_queue_dismiss (self);
+}
+
+static void
+gtk_source_hover_assistant_root (GtkWidget *widget)
+{
+       GtkSourceHoverAssistant *self = (GtkSourceHoverAssistant *)widget;
+       GtkRoot *root;
+
+       GTK_WIDGET_CLASS (gtk_source_hover_assistant_parent_class)->root (widget);
+
+       if ((root = gtk_widget_get_root (widget)))
        {
-               return;
+               GtkEventController *motion;
+
+               if (!(motion = g_object_get_data (G_OBJECT (root), GTK_SOURCE_HOVER_ASSISTANT_MOTION)))
+               {
+                       motion = gtk_event_controller_motion_new ();
+                       gtk_event_controller_set_name (motion, "gtk-source-hover-assistant-motion");
+                       g_object_set_data (G_OBJECT (root), GTK_SOURCE_HOVER_ASSISTANT_MOTION, motion);
+                       gtk_widget_add_controller (GTK_WIDGET (root), motion);
+               }
+
+               self->root_motion = g_object_ref (motion);
+               self->root_motion_handler =
+                       g_signal_connect_object (self->root_motion,
+                                                "motion",
+                                                G_CALLBACK (gtk_source_hover_assistant_root_motion_cb),
+                                                self,
+                                                G_CONNECT_SWAPPED);
+               self->root_leave_handler =
+                       g_signal_connect_object (self->root_motion,
+                                                "leave",
+                                                G_CALLBACK (gtk_source_hover_assistant_root_leave_cb),
+                                                self,
+                                                G_CONNECT_SWAPPED);
+
+               if (!gtk_widget_get_visible (GTK_WIDGET (self)))
+               {
+                       g_signal_handler_block (self->root_motion, self->root_motion_handler);
+                       g_signal_handler_block (self->root_motion, self->root_leave_handler);
+               }
        }
+}
 
-       if (!(parent = gtk_widget_get_parent (GTK_WIDGET (self))) ||
-           !(root = gtk_widget_get_root (parent)) ||
-           !(assistant_surface = gtk_native_get_surface (GTK_NATIVE (self))))
+static void
+gtk_source_hover_assistant_unroot (GtkWidget *widget)
+{
+       GtkSourceHoverAssistant *self = (GtkSourceHoverAssistant *)widget;
+       GtkRoot *root;
+
+       if ((root = gtk_widget_get_root (widget)))
        {
-               return;
+               GtkEventController *motion;
+
+               if ((motion = g_object_get_data (G_OBJECT (root), GTK_SOURCE_HOVER_ASSISTANT_MOTION)))
+               {
+                       g_clear_signal_handler (&self->root_motion_handler, motion);
+                       g_clear_signal_handler (&self->root_leave_handler, motion);
+               }
+               else
+               {
+                       self->root_motion_handler = 0;
+                       self->root_leave_handler = 0;
+               }
        }
 
-       gtk_widget_translate_coordinates (parent, GTK_WIDGET (root), x, y, &x, &y);
-       x -= gdk_popup_get_position_x (GDK_POPUP (assistant_surface));
-       y -= gdk_popup_get_position_y (GDK_POPUP (assistant_surface));
+       GTK_WIDGET_CLASS (gtk_source_hover_assistant_parent_class)->unroot (widget);
+}
 
-       gtk_native_get_surface_transform (GTK_NATIVE (root), &tx, &ty);
-       x += tx;
-       y += ty;
+static void
+gtk_source_hover_assistant_show (GtkWidget *widget)
+{
+       GtkSourceHoverAssistant *self = GTK_SOURCE_HOVER_ASSISTANT (widget);
+       GtkRoot *root;
 
-       width = gdk_surface_get_width (assistant_surface);
-       height = gdk_surface_get_height (assistant_surface);
+       GTK_WIDGET_CLASS (gtk_source_hover_assistant_parent_class)->show (widget);
 
-       if (x < -GRACE_X ||
-           x > width + GRACE_Y ||
-           y < -GRACE_Y ||
-           y > height + GRACE_Y)
+       if ((root = gtk_widget_get_root (widget)))
        {
-               GtkWidget *focus = gtk_root_get_focus (root);
-               GtkWidget *view = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_SOURCE_TYPE_VIEW);
-               gboolean return_focus;
-
-               return_focus = focus != NULL &&
-                       (focus == GTK_WIDGET (self) ||
-                        gtk_widget_is_ancestor (focus, GTK_WIDGET (self)));
+               GtkEventController *motion;
 
-               gtk_widget_hide (GTK_WIDGET (self));
-
-               if (return_focus && view != NULL)
+               if ((motion = g_object_get_data (G_OBJECT (root), GTK_SOURCE_HOVER_ASSISTANT_MOTION)))
                {
-                       gtk_widget_grab_focus (GTK_WIDGET (view));
+                       g_signal_handler_unblock (motion, self->root_motion_handler);
+                       g_signal_handler_unblock (motion, self->root_leave_handler);
                }
        }
 }
 
-static gboolean
-gtk_source_hover_assistant_scroll_cb (GtkSourceHoverAssistant  *self,
-                                      double                    dx,
-                                      double                    dy,
-                                      GtkEventControllerScroll *controller)
+static void
+gtk_source_hover_assistant_hide (GtkWidget *widget)
 {
-       g_assert (GTK_SOURCE_IS_HOVER_ASSISTANT (self));
-       g_assert (GTK_IS_EVENT_CONTROLLER_SCROLL (controller));
+       GtkSourceHoverAssistant *self = GTK_SOURCE_HOVER_ASSISTANT (widget);
+       GtkRoot *root;
 
-       gtk_widget_hide (GTK_WIDGET (self));
+       GTK_WIDGET_CLASS (gtk_source_hover_assistant_parent_class)->hide (widget);
+
+       gtk_popover_set_pointing_to (GTK_POPOVER (widget), NULL);
+       gtk_popover_set_offset (GTK_POPOVER (widget), 0, 0);
+
+       if ((root = gtk_widget_get_root (widget)))
+       {
+               GtkEventController *motion;
+
+               if ((motion = g_object_get_data (G_OBJECT (root), GTK_SOURCE_HOVER_ASSISTANT_MOTION)))
+               {
+                       g_signal_handler_block (motion, self->root_motion_handler);
+                       g_signal_handler_block (motion, self->root_leave_handler);
+               }
+       }
+}
 
-       return GDK_EVENT_PROPAGATE;
+static void
+gtk_source_hover_assistant_get_target_location (GtkSourceAssistant *assistant,
+                                                GdkRectangle       *rect)
+{
+       *rect = GTK_SOURCE_HOVER_ASSISTANT (assistant)->hovered_at;
 }
 
 static void
@@ -128,6 +291,11 @@ gtk_source_hover_assistant_dispose (GObject *object)
 
        self->display = NULL;
 
+       g_clear_pointer (&self->dismiss_source, g_source_destroy);
+
+       g_clear_object (&self->root_motion);
+       g_clear_object (&self->popover_motion);
+
        g_clear_object (&self->cancellable);
 
        G_OBJECT_CLASS (gtk_source_hover_assistant_parent_class)->dispose (object);
@@ -136,43 +304,39 @@ gtk_source_hover_assistant_dispose (GObject *object)
 static void
 gtk_source_hover_assistant_class_init (GtkSourceHoverAssistantClass *klass)
 {
-       GtkSourceAssistantClass *assistant_class = GTK_SOURCE_ASSISTANT_CLASS (klass);
        GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+       GtkSourceAssistantClass *assistant_class = GTK_SOURCE_ASSISTANT_CLASS (klass);
 
        object_class->dispose = gtk_source_hover_assistant_dispose;
 
+       widget_class->root = gtk_source_hover_assistant_root;
+       widget_class->unroot = gtk_source_hover_assistant_unroot;
+       widget_class->show = gtk_source_hover_assistant_show;
+       widget_class->hide = gtk_source_hover_assistant_hide;
+
        assistant_class->get_target_location = gtk_source_hover_assistant_get_target_location;
 }
 
 static void
 gtk_source_hover_assistant_init (GtkSourceHoverAssistant *self)
 {
-       GtkEventController *motion;
-       GtkEventController *scroll;
-
        gtk_widget_add_css_class (GTK_WIDGET (self), "hover-assistant");
 
-       gtk_popover_set_autohide (GTK_POPOVER (self), TRUE);
+       gtk_popover_set_autohide (GTK_POPOVER (self), FALSE);
+       gtk_popover_set_position (GTK_POPOVER (self), GTK_POS_TOP);
 
        self->display = g_object_new (GTK_SOURCE_TYPE_HOVER_DISPLAY, NULL);
        _gtk_source_assistant_set_child (GTK_SOURCE_ASSISTANT (self), GTK_WIDGET (self->display));
 
-       motion = gtk_event_controller_motion_new ();
-       gtk_event_controller_set_propagation_phase (motion, GTK_PHASE_CAPTURE);
-       g_signal_connect_object (motion,
-                                "motion",
-                                G_CALLBACK (gtk_source_hover_assistant_motion_cb),
+       self->popover_motion = gtk_event_controller_motion_new ();
+       gtk_event_controller_set_name (self->popover_motion, "gkt-source-hover-assistant-motion");
+       g_signal_connect_object (self->popover_motion,
+                                "leave",
+                                G_CALLBACK (gtk_source_hover_assistant_popover_leave_cb),
                                 self,
                                 G_CONNECT_SWAPPED);
-       gtk_widget_add_controller (GTK_WIDGET (self), motion);
-
-       scroll = gtk_event_controller_scroll_new (GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES);
-       g_signal_connect_object (scroll,
-                                "scroll",
-                                G_CALLBACK (gtk_source_hover_assistant_scroll_cb),
-                                self,
-                                G_CONNECT_SWAPPED);
-       gtk_widget_add_controller (GTK_WIDGET (self), scroll);
+       gtk_widget_add_controller (GTK_WIDGET (self), g_object_ref (self->popover_motion));
 }
 
 GtkSourceAssistant *
@@ -225,20 +389,15 @@ _gtk_source_hover_assistant_display (GtkSourceHoverAssistant  *self,
        g_return_if_fail (end != NULL);
        g_return_if_fail (location != NULL);
 
-       if (n_providers == 0)
-       {
-               if (gtk_widget_get_visible (GTK_WIDGET (self)))
-               {
-                       gtk_widget_hide (GTK_WIDGET (self));
-               }
+       memset (&self->hovered_at, 0, sizeof self->hovered_at);
 
-               return;
-       }
+       g_cancellable_cancel (self->cancellable);
+       g_clear_object (&self->cancellable);
 
-       if (self->cancellable != NULL)
+       if (n_providers == 0)
        {
-               g_cancellable_cancel (self->cancellable);
-               g_clear_object (&self->cancellable);
+               gtk_widget_hide (GTK_WIDGET (self));
+               return;
        }
 
        view = GTK_SOURCE_VIEW (gtk_widget_get_parent (GTK_WIDGET (self)));
@@ -247,31 +406,14 @@ _gtk_source_hover_assistant_display (GtkSourceHoverAssistant  *self,
        gtk_text_view_get_iter_location (GTK_TEXT_VIEW (view), begin, &begin_rect);
        gtk_text_view_get_iter_location (GTK_TEXT_VIEW (view), end, &end_rect);
        gtk_text_view_get_iter_location (GTK_TEXT_VIEW (view), location, &location_rect);
-
        gdk_rectangle_union (&begin_rect, &end_rect, &location_rect);
 
        if (!gdk_rectangle_intersect (&location_rect, &visible_rect, &location_rect))
        {
-               if (gtk_widget_get_visible (GTK_WIDGET (self)))
-               {
-                       gtk_widget_hide (GTK_WIDGET (self));
-               }
-
-               memset (&self->hovered_at, 0, sizeof self->hovered_at);
-
+               gtk_widget_hide (GTK_WIDGET (self));
                return;
        }
 
-       if (gtk_text_iter_equal (begin, end) && gtk_text_iter_starts_line (begin))
-       {
-               location_rect.width = 1;
-               gtk_popover_set_position (GTK_POPOVER (self), GTK_POS_RIGHT);
-       }
-       else
-       {
-               gtk_popover_set_position (GTK_POPOVER (self), GTK_POS_TOP);
-       }
-
        self->hovered_at = location_rect;
 
        context = _gtk_source_hover_context_new (view, begin, end, location);
@@ -281,10 +423,10 @@ _gtk_source_hover_assistant_display (GtkSourceHoverAssistant  *self,
                _gtk_source_hover_context_add_provider (context, providers[i]);
        }
 
-       self->cancellable = g_cancellable_new ();
-
        _gtk_source_hover_display_clear (self->display);
 
+       self->cancellable = g_cancellable_new ();
+
        _gtk_source_hover_context_populate_async (context,
                                                  self->display,
                                                  self->cancellable,
@@ -300,6 +442,9 @@ _gtk_source_hover_assistant_dismiss (GtkSourceHoverAssistant *self)
        g_return_if_fail (GTK_SOURCE_IS_HOVER_ASSISTANT (self));
 
        g_cancellable_cancel (self->cancellable);
+       g_clear_object (&self->cancellable);
+
        gtk_widget_hide (GTK_WIDGET (self));
+
        _gtk_source_hover_display_clear (self->display);
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]