Skip to content

Fix styled-text note hyperlink click detection#2448

Open
eduralph wants to merge 1 commit into
gramps-project:maintenance/gramps61from
eduralph:fix/bug-8841-note-link-click-hypersensitive
Open

Fix styled-text note hyperlink click detection#2448
eduralph wants to merge 1 commit into
gramps-project:maintenance/gramps61from
eduralph:fix/bug-8841-note-link-click-hypersensitive

Conversation

@eduralph

@eduralph eduralph commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

User impact: When you view a Note that contains a web link, clicking in the empty space beside the link or below the last line of text unexpectedly opens the link in your browser — even though you never clicked on the link itself. Any click near a link that ends a line or the note can launch the browser by mistake.

This makes Gramps confirm the pointer is really over the link's text before treating a click as a link.

Reported in Mantis #8841.

What to look at

The Note viewer decides whether a click hit a link by asking GTK for the "nearest" text position, which snaps empty-area clicks onto the end of the text and its trailing link. The fix adds a check that the pointer is genuinely inside a character's rendered rectangle before counting it as a link hit. To reproduce: on maintenance/gramps61, create a Note, paste a URL on its own line, save, and reopen in read-only view. Click in the empty area below or to the right of the URL — before the fix the browser opens; after the fix the empty-area click does nothing, while clicking on the URL still opens it.

Root cause

Gtk.TextView.get_iter_at_location(x, y) is a GTK3 API that returns the "nearest" text iterator for any point, including a click in the empty area beside/below text — it never returns "no iter". In styledtexteditor.py, on_motion_notify_event() (lines 424–456) sets iter_at_location from the click at line 435, then uses that position directly to look up URLs (via match_check()) and internal link tags. An empty-area click snaps to the end of the text and matches the link if one ends there; on_button_press_event() (lines 501–516) then opens the link when self.url_match is set, and do_match_changed() (line 404) keeps self.url_match in sync.

Fix

The fix (in gramps/gui/widgets/styledtexteditor.py) adds a new private method _pointer_over_iter(x, y, text_iter) that uses get_iter_location() to confirm the point (x, y) is genuinely inside a character's bounding rectangle. Because get_iter_at_location() advances past a glyph when the click lands in that glyph's trailing (right) half (it adds Pango's trailing count), the character under the pointer may be the one before text_iter; the helper therefore checks the rectangle of the char at the iter and, on a miss, the char immediately before it. Both the URL regexp match path (match_check()) and the internal link-tag loop are guarded by this check:

if self._pointer_over_iter(x, y, iter_at_location):
    self.match = self.textbuffer.match_check(...)
    ... link-tag loop ...
else:
    self.match = None

This ensures that a click to the right of the URL on its line is excluded (past the last glyph's rectangle); a click below the last line is excluded (below every line's rectangle); and a click anywhere on the URL glyph, left or right half, still matches (point inside the char's or its predecessor's rectangle → link still opens). Both URL matches and internal link tags are guarded, so neither hyperlink flavour can open on an empty-area click, and neither is lost on a real on-glyph click.

Verification

  • Claim: get_iter_at_location() snaps even out-of-bounds clicks; the fix gates both URL matches and internal link tags on a real glyph hit (checking the snapped char and its predecessor), clears the match otherwise, and leaves the match→open path intact.
  • Checked: on maintenance/gramps61gramps/gui/widgets/styledtexteditor.py:435 sets iter_at_location from the user click (nearest-iter semantics per GTK3 docs / match_check behaviour). Patched: the URL match_check() and the link-tag loop now sit inside an if self._pointer_over_iter() block, with an else clause setting self.match = None for out-of-bounds clicks; _pointer_over_iter() uses get_iter_location() to confirm the point is inside the char at the iter or, on a miss, the char before it (covering the trailing-half advance so a real click on a link glyph's right half still opens). The signal path is unbroken: styledtexteditor.py:450–451 still emits "match-changed" when self.match changes, do_match_changed() still updates self.url_match (line 404, preimage), and on_button_press_event() (lines 511–516, preimage) consumes self.url_match and calls _open_url_cb() only when set.
  • Test: No core unit test ships — the behaviour is a GUI hit-test, not headless-testable. Coverage is a committed AT-SPI/dogtail repro in the gramps-testbed harness (not part of this PR), engine/interface/test_bug_0008841_note_link_hittest.py, red without the fix and green with it: before an empty-area click snaps to the URL end, match_check() includes that offset, self.match is set, and the non-editable view calls _open_url_cb() so the "does not open" assertion fails; after, _pointer_over_iter() returns False, self.match stays None, no browser launch occurs, and on-glyph clicks still match so the positive assertion passes both legs.

Fixes #8841

@eduralph eduralph force-pushed the fix/bug-8841-note-link-click-hypersensitive branch from 169d5a9 to 88a10b5 Compare July 3, 2026 01:07
When a user clicks in empty space beside or below a hyperlink in a Note's
styled-text view, GTK's get_iter_at_location() snaps the click to the nearest
character position. The existing code treats that snapped position as a real
link match, so the link opens even though the user never clicked on it.

Add a new _pointer_over_iter() predicate to verify that the pointer is actually
within the rendered glyph of a character, not merely adjacent to it. Because
get_iter_at_location() advances past a glyph when the click lands in its
trailing (right) half, the predicate checks the bounding rectangle of the
character at the snapped position and, on a miss, the one immediately before it.
Gate both URL regexp matches and internal link tags on this check, so clicks in
the empty area beside/below a link no longer register as hits, while a click
anywhere on the link glyph -- left or right half -- still opens it.

This fixes the invariant that link activation requires the pointer to be over
the link's rendered glyph, not over the nearest position when a click falls
outside the text.

Fixes #8841

Signed-off-by: Eduard Ralph <eduard@ralphovi.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants