dear all

I have used an AI to prepare a patch, it seems fine to me

regards

a.

Attachment: 0001-Fix-link-preview-popup-hover-handling.patch.gpg
Description: application/pgp-encrypted

From ca56970a6b13562e6d88dcdb6bcbcec717e8790c Mon Sep 17 00:00:00 2001
From: "Andrea C. G. Mennucci" <[email protected]>
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 <[email protected]>
---
 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

Reply via email to