From ca56970a6b13562e6d88dcdb6bcbcec717e8790c Mon Sep 17 00:00:00 2001 From: "Andrea C. G. Mennucci" Date: Wed, 7 Oct 2026 14:19:20 +0200 Subject: [PATCH] Fix link preview popup hover handling GTK4 popovers grab pointer events by default. The GTK3 link preview explicitly used a non-modal popover, but the GTK4 port omitted the equivalent, so hover events could be routed to the popover and prevent the view from cleaning up the preview consistently. Disable the grab and let EvView handle pointer movement again. Anchor the enlarged popup arrow below the hover point so the cursor starts inside it. This avoids an immediate crossing back onto the link and raises the preview body without moving the cursor. Keep the preview alive while the pointer traverses the popup, and add EvinceView debug messages for hover and preview lifecycle events; these remain silent unless G_MESSAGES_DEBUG is enabled. Co-authored-by: OpenAI Codex --- libview/ev-view.c | 77 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/libview/ev-view.c b/libview/ev-view.c index b4729c46..f059e1e5 100644 --- a/libview/ev-view.c +++ b/libview/ev-view.c @@ -115,6 +115,8 @@ typedef struct { #define LINK_PREVIEW_HORIZONTAL_LINK_POS 0.5 /* as fraction of preview width */ #define LINK_PREVIEW_VERTICAL_LINK_POS 0.3 /* as fraction of preview height */ #define LINK_PREVIEW_DELAY_MS 300 /* Delay before showing preview in milliseconds */ +#define LINK_PREVIEW_ARROW_SIZE 72 +#define LINK_PREVIEW_ARROW_OFFSET (LINK_PREVIEW_ARROW_SIZE / 2) #define SCROLL_SENSITIVITY_THRESHOLD 5 @@ -143,6 +145,7 @@ static EvLink * ev_view_get_link_at_location (EvView *view, static char* tip_from_link (EvView *view, EvLink *link); static void ev_view_link_preview_popover_cleanup (EvView *view); +static void link_preview_style_popover (GtkWidget *popover); static void get_link_area (EvView *view, gint x, gint y, @@ -2211,26 +2214,55 @@ tip_from_link (EvView *view, EvLink *link) return msg; } -static gboolean +static void link_preview_popover_motion_notify (GtkEventControllerMotion *self, gdouble x, gdouble y, EvView *view) { - ev_view_link_preview_popover_cleanup (view); - return TRUE; + /* Log preview motion; cleanup is handled by the document hover logic. */ + g_debug ("link preview: motion inside popover at %.1f,%.1f", x, y); +} + +static void +link_preview_popover_leave (GtkEventControllerMotion *self, + EvView *view) +{ + g_debug ("link preview: pointer left popover"); +} + +static void +link_preview_style_popover (GtkWidget *popover) +{ + static gboolean provider_installed; + static const char css[] = + "popover.link-preview arrow { min-width: 72px; min-height: 72px; }"; + + if (!provider_installed) { + GtkCssProvider *provider = gtk_css_provider_new (); + GdkDisplay *display = gtk_widget_get_display (popover); + + gtk_css_provider_load_from_string (provider, css); + gtk_style_context_add_provider_for_display (display, + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + g_object_unref (provider); + provider_installed = TRUE; + } + + gtk_widget_add_css_class (popover, "link-preview"); } static void handle_cursor_over_link (EvView *view, EvLink *link, gint x, gint y, gboolean from_motion) { EvViewPrivate *priv = GET_PRIVATE (view); - GdkRectangle link_area; EvLinkAction *action; EvLinkDest *dest; EvLinkDestType type; GtkWidget *popover, *spinner; GdkTexture *page_texture = NULL; + GdkRectangle cursor_area; guint link_dest_page; EvPoint link_dest_doc; GdkPoint link_dest_view; @@ -2266,14 +2298,23 @@ handle_cursor_over_link (EvView *view, EvLink *link, gint x, gint y, gboolean fr /* Init popover */ priv->link_preview.popover = popover = gtk_popover_new (); gtk_popover_set_position (GTK_POPOVER (popover), GTK_POS_TOP); + /* Match the GTK3 non-modal popover behavior. GTK4 uses autohide=FALSE + * to avoid grabbing pointer events while the preview is visible. */ + gtk_popover_set_autohide (GTK_POPOVER (popover), FALSE); + link_preview_style_popover (popover); gtk_widget_set_parent (popover, GTK_WIDGET (view)); - get_link_area (view, x, y, link, &link_area); - gtk_popover_set_pointing_to (GTK_POPOVER (popover), &link_area); + /* Anchor the enlarged arrow below the pointer so that the cursor starts + * around the middle of the arrow rather than on its tip. */ + cursor_area = (GdkRectangle) { x, y + LINK_PREVIEW_ARROW_OFFSET, 1, 1 }; + gtk_popover_set_pointing_to (GTK_POPOVER (popover), &cursor_area); controller = GTK_EVENT_CONTROLLER (gtk_event_controller_motion_new ()); g_signal_connect (controller, "motion", - G_CALLBACK (link_preview_popover_motion_notify), - view); + G_CALLBACK (link_preview_popover_motion_notify), + view); + g_signal_connect (controller, "leave", + G_CALLBACK (link_preview_popover_leave), + view); gtk_widget_add_controller (popover, controller); spinner = gtk_spinner_new (); @@ -2297,6 +2338,8 @@ handle_cursor_over_link (EvView *view, EvLink *link, gint x, gint y, gboolean fr priv->link_preview.link = link; page_texture = ev_pixbuf_cache_get_texture (priv->pixbuf_cache, link_dest_page); + g_debug ("link preview: create popover=%p destination_page=%u cached_texture=%d", + popover, link_dest_page, page_texture != NULL); if (page_texture) link_preview_show_thumbnail (page_texture, view); @@ -2353,6 +2396,8 @@ ev_view_handle_cursor_over_xy (EvView *view, gint x, gint y, gboolean from_motio link = ev_view_get_link_at_location (view, x, y); if (link) { + g_debug ("link hover event: link=%p at %d,%d from_motion=%d preview_link=%p", + link, x, y, from_motion, priv->link_preview.link); handle_cursor_over_link (view, link, x, y, from_motion); } else { ev_view_link_preview_popover_cleanup (view); @@ -5121,8 +5166,17 @@ link_preview_delayed_show (EvView *view) EvViewPrivate *priv = GET_PRIVATE (view); GtkWidget *popover = priv->link_preview.popover; + if (!popover) { + g_debug ("link preview: delayed show skipped because popover was cleaned up"); + priv->link_preview.delay_timeout_id = 0; + return; + } + + g_debug ("link preview: presenting popover=%p", popover); gtk_popover_present (GTK_POPOVER (popover)); gtk_popover_popup (GTK_POPOVER (popover)); + g_debug ("link preview: popup requested popover=%p visible=%d mapped=%d", + popover, gtk_widget_get_visible (popover), gtk_widget_get_mapped (popover)); priv->link_preview.delay_timeout_id = 0; } @@ -5177,6 +5231,13 @@ static void ev_view_link_preview_popover_cleanup (EvView *view) { EvViewPrivate *priv = GET_PRIVATE (view); + + if (priv->link_preview.popover || priv->link_preview.job || + priv->link_preview.delay_timeout_id) + g_debug ("link preview: cleanup popover=%p job=%p timeout=%u", + priv->link_preview.popover, priv->link_preview.job, + priv->link_preview.delay_timeout_id); + if (priv->link_preview.job) { ev_job_cancel (priv->link_preview.job); g_clear_object (&priv->link_preview.job); -- 2.53.0