Skip to content

Make the package details text selectable across rows and links - #5580

Merged
Gabriel Dufresne (GabrielDuf) merged 7 commits into
mainfrom
fix/hyperlink-text-multiclick-copy-link
Oct 23, 2026
Merged

Gabriel Dufresne (GabrielDuf) merged 7 commits into
mainfrom
fix/hyperlink-text-multiclick-copy-link

Conversation

@GabrielDuf

@GabrielDuf Gabriel Dufresne (GabrielDuf) commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor
  • I have read the contributing guidelines, and I agree with the Code of Conduct.
  • Have you checked that there aren't other open pull requests for the same changes?
  • Have you tested that the committed code can be executed without errors?
  • Have you confirmed that this issue is caused by UniGetUI itself, and not by the package manager or the package involved?
    If the same issue can be reproduced outside UniGetUI with the relevant package manager or with the package itself, please report it there first. UniGetUI should only be used to track issues that are specific to UniGetUI's behavior or integration.

Bug report

From #5548 and the maintainer's report: in the Avalonia package details window, URLs are hard to select and copy. Double or triple clicking a link opened the browser instead of selecting it, a drag selected text with no visible highlight, there was no way to copy a whole link, and a selection could not cover a label and its URL (Homepage: https://…) or span several rows.

Root cause

Three separate problems:

  1. No highlight on links. HyperlinkText subclasses SelectableTextBlock, but Avalonia themes match by exact type, so the Fluent SelectableTextBlock theme never applied. Its SelectionBrush stayed null and SelectableTextBlock only draws a selection when it has a brush. Selection worked (a debug log in the running app showed the selected text) but was invisible.
  2. Multi-click opened the browser. A clean single-click release called Activate() at once, and the first click of a double or triple click is exactly that.
  3. Each row was several controls. Link rows were a label control and a link control side by side in a Grid, and every row was its own control. A selection cannot cross controls, and Avalonia 12 has no inline hyperlink element (Run, Span, LineBreak, InlineUIContainer… but no Hyperlink).

Fix

  • LinkTextBlock (new, Views/Controls/LinkTextBlock.cs): one SelectableTextBlock of inlines that records which character ranges are links. It shows the hand cursor over links only, opens a link on a single click deferred by the platform double-click time (a second press cancels it), lets drag and multi-click select, and offers a right-click menu with "Copy" (selection) and "Copy link" (the URL under the pointer, hidden elsewhere and for action links such as "Download installer"). Clipboard failures are logged. It also uses the SelectableTextBlock theme and a transparent background.
    • Keyboard: tabbing into the block selects its first link (Shift+Tab: the last), Tab and Shift+Tab step through the links and then let focus leave the block, and Enter or Space opens the selected link.
    • UI Automation: a LinkTextBlockAutomationPeer exposes one LinkAutomationPeer per link, as an invokable Hyperlink with the link text as its name and bounds from the text layout.
    • Rebuilding the text (Clear(), as DetailsLoaded does) and detaching reset any pending click, and a press stops being a click once the pointer moves past 4px, even if it comes back.
  • PackageDetailsWindow: the top info area and the details area are each built as a single LinkTextBlock, so selection runs across labels, values, links and rows. Group spacing is a blank line. The installed-version warning icon is now a ⚠ glyph after the version, with its tooltip shown while hovering the version.
  • HyperlinkText (still used by the telemetry dialog and General settings): gets the theme fix (StyleKeyOverride), a transparent background, the deferred single click, and the same 4px drag rule. The Url property and Copy link menu from the first commit are removed, as nothing uses them any more.
  • Tests: AvaloniaTestApp makes the test assembly's headless app use Skia, because the default headless text stub cannot lay out line breaks.

Avalonia behaviours worked around in LinkTextBlock (each one found by a test):

  • Changing the selection drops the text layout until the next layout pass, so the link hit-test runs on the tunnel phase of a press and before base.OnPointerMoved.
  • TextHitTestResult.IsInside is false on every line after the first, so the control checks the hit character's own box instead.
  • TextPosition is the nearest caret, so the control uses CharacterHit.FirstCharacterIndex to find the character under the pointer.

Verification

  • The Copilot review of e573cc5 (stale link state on rebuild, keyboard and screen-reader access, drag returning to its start) is addressed in 9098161.

  • Checked by the maintainer in the running app (macOS, Debug): selection works across rows, labels and links.

  • dotnet build src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj: succeeds, no new warnings.

  • dotnet format whitespace src --folder --verify-no-changes and dotnet format style on the changed UniGetUI.Avalonia files: exit 0. UniGetUI.Tests cannot load on macOS, so CI is the first style check for the test files.

  • Tests were run on macOS arm64 through a scratch net10.0 xUnit project compiling the test files against UniGetUI.Avalonia, as UniGetUI.Tests targets Windows: 19/19 pass. The full suite is left to CI.

  • NativeAOT publish not run locally. No reflection, dynamic code or serialization is added.

Regression tests

  • LinkTextBlockTests (14): a click opens the link and a link after line breaks opens the right one; a click on plain text opens nothing; double and triple click select without opening; drag selects across a label and its link, and across rows; a drag that returns to its start opens nothing; rebuilding the text cancels a pending click; right-click on a URL offers Copy link and copies the URL; right-click on plain text or an action link hides it; Tab, Shift+Tab, Enter and Space reach and open the links; each link is an invokable Hyperlink automation peer.
  • HyperlinkTextTests (5): single click opens; double and triple click select with a highlight brush set and do not open; drag selects; a drag that returns to its start does not open. The highlight assertions fail without StyleKeyOverride, and the multi-click ones fail with the immediate Activate().

Review notes

  • A single click opens a link after the double-click interval (about 500 ms) rather than at once.
  • Keyboard focus on a link is shown by selecting the link text, as there is no separate focus rectangle inside a text block. The automation peers were checked headlessly, not with a real screen reader.
  • A selection cannot run from the top info area into the details area: the action button and screenshots sit between them.
  • A wrapped URL continues at the left edge rather than under itself, and a copied selection includes the ⚠ glyph.
  • "Copy link" is only added to lang_en.json.
  • No separate copy button was added; [IMPROVEMENT] Allow copying install URL / software info text from the app details panel #5548 mentions one as optional.

Relates to #5548

🤖 Generated with Claude Code

HyperlinkText activated the link on the release of any clean left click.
The first press of a double or triple click always has ClickCount 1, so
the browser opened before the second press could select a word or the
whole text. A clean single click now starts a one-shot timer set to the
platform double-click time, and any further press cancels it, so
multi-clicks only select. Keyboard and UIA activation stay immediate.

The control also had no way to copy a link: its StyleKey is its own
type, so the SelectableTextBlock theme and its context flyout never
applied. A new Url property holds the link target, and when set the
control gets a context menu with Copy and Copy link. Copy link copies
the whole URL, even when the displayed text is trimmed. The package
details window sets Url on its link rows.

Relates to #5548

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Clipboard failures can escape the asynchronous menu handler and terminate the application.

1 open issue
What changed in this PR

This PR improves link selection and copying in UniGetUI’s package details window, addressing part of #5548.

Changes:

  • Defers pointer activation to distinguish single clicks from multi-click text selection.
  • Adds selection-copy and full-URL “Copy link” menu actions to links with a URL.
  • Adds regression tests and the English “Copy link” string.
File Description
src/​UniGetUI.Tests/​HyperlinkTextTests.cs Tests click activation, multi-click selection, and URL copying.
src/​UniGetUI.Avalonia/​Views/​DialogPages/​PackageDetailsWindow.axaml.cs Supplies URLs to package-detail links.
src/​UniGetUI.Avalonia/​Views/​Controls/​HyperlinkText.cs Adds deferred activation and the copy context menu.
src/​Languages/​lang_en.json Adds the English “Copy link” entry.

Review effort: Balanced

The Copy link menu item runs an async void Click handler that awaited
SetTextAsync unguarded. If the OS clipboard is busy or unavailable the
exception escaped the handler, and the dispatcher exception handler only
absorbs native-control-host and WebView failures, so it could terminate
the app instead of just failing to copy.

The write now sits in a try/catch inside CopyUrlToClipboardAsync, the one
awaited call on the menu path, and logs through Logger.Warn. Warn rather
than Error because a busy clipboard is transient and the user can retry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused changes have regression coverage, resolve the prior clipboard exception issue, and leave no identified blocking defects.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

randy-but-a-ro[bot]
randy-but-a-ro Bot previously approved these changes Oct 22, 2026

@randy-but-a-ro randy-but-a-ro Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Pull request was approved automatically: the AI review is complete and all its review threads are resolved. 🎉

Integration Details
{
	"deliveryId": "ee8e42a0-ce42-11f1-9a36-b7bc7031ccd4",
	"headSha": "2e9ab596851d208af4dc461707663dbb43dd5485",
	"reviewer": "copilot-pull-request-reviewer[bot]"
}

Links in the package details window could be selected but showed no
highlight: HyperlinkText subclasses SelectableTextBlock, and Avalonia
themes match by exact type, so it never got the theme's selection brush.
Each label and link was also its own control, so a selection could not
cover a label and its URL, or span rows.

Each details area is now a single LinkTextBlock. Links are ranges of
that text, with a hand cursor, a deferred single click, and a right-click
Copy link. HyperlinkText keeps the theme, background and multi-click
fixes for its remaining callers, and drops the Url and Copy link menu
that only the details window used.

The UI tests now run on a Skia headless app, since the default text
stub cannot lay out line breaks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@GabrielDuf Gabriel Dufresne (GabrielDuf) changed the title Stop links opening on multi-click and add a Copy link menu Make the package details text selectable across rows and links Oct 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Accessible link activation and interaction-state handling need correction before approval.

3 open issues

Review effort: Balanced

… UIA access

LinkTextBlock.Clear() left the deferred-click timer and the pressed,
pending and context-menu links in place, so rebuilding the details text
between a click and its activation could open a link that was no longer
shown. Clear() and detach now reset all of it.

A press on a link kept its click candidate however far the pointer
moved, so a drag that ended back near its start opened the link. Moving
past the 4px threshold now ends the candidate, in LinkTextBlock and in
HyperlinkText.

Links inside the text had no keyboard or screen-reader access. Tab and
Shift+Tab now step through them (selecting each one), Enter or Space
opens the selected link, and each link is exposed to UI Automation as an
invokable Hyperlink peer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Automation focus on a later link can leave inconsistent focus state and prevent keyboard activation.

1 open issue
3 resolved since last review

Review effort: Balanced

…block

FocusLink stored the link index and then called Focus(). Focus raises
GotFocus synchronously, and OnGotFocus, seeing no focused link yet,
selected the first link and overwrote the index. FocusLink then selected
the requested link without restoring it, so FocusedLink was null: Enter
and Space did nothing and the link's automation peer reported no
keyboard focus. FocusLink now focuses the block first and assigns the
index afterwards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Keyboard context-menu targeting, focused-link scrolling, and screen-reader focus notifications need correction before approval.

No open issues 🎉

3 previously missed issues

In code that hasn't changed since last review

Medium severity Use the current keyboard-focused link in the context menu

src/​UniGetUI.Avalonia/​Views/​Controls/​LinkTextBlock.cs:74

Resolve the context-menu target for keyboard openings rather than retaining the last right-clicked link. After right-clicking URL A, dismissing the menu, tabbing to URL B, and pressing Shift+F10, “Copy link” still copies A. The stale item can also appear for an action-only link; before any right-click, it is unavailable to keyboard users. Clear the target when the flyout closes and fall back to FocusedLink on opening. Pointer presses already reset _focusedLink, so this preserves the existing behavior for right-clicks on plain text. Add a regression test that switches from a right-clicked URL to a different keyboard-focused URL and then an action link.

Medium severity Notify automation when keyboard navigation focuses a link

src/​UniGetUI.Avalonia/​Views/​Controls/​LinkTextBlock.cs:158

Notify automation when keyboard navigation moves to an inline link. After the block acquires focus, Tab only changes _focusedLink and the selection; keyboard focus stays on the same control, and neither this method nor the virtual link peer raises an automation focus-change event. Screen readers therefore receive no focus notification identifying the hyperlink that Enter/Space will invoke. Publish focus-change notifications for the corresponding link peer on entry and when moving between links. Extend the tests to observe those notifications: querying HasKeyboardFocus() alone does not verify that assistive technology is notified.

Medium severity Cache link bounds before updating selection

src/​UniGetUI.Avalonia/​Views/​Controls/​LinkTextBlock.cs:160

Capture the link bounds before changing focus or selection. These selection writes invalidate the inline text layout, as the pointer handlers already account for by hit-testing before changing selection. The subsequent hit-test can therefore lose the rendered link's bounds, leaving a keyboard-selected installer or release-notes link off-screen in a long details block. Cache the bounds first, then update focus and selection and use the cached rectangle for BringIntoView. Add a regression test with a link below a ScrollViewer viewport.

1 resolved since last review

Review effort: Balanced

randy-but-a-ro[bot]
randy-but-a-ro Bot previously approved these changes Oct 22, 2026

@randy-but-a-ro randy-but-a-ro Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Pull request was approved automatically: the AI review is complete and all its review threads are resolved. 🎉

Integration Details
{
	"deliveryId": "8a5f5cc0-ce5a-11f1-9a13-98931acf6e55",
	"headSha": "7de27e4365812b100a2b71120155fae11e4d0d55",
	"reviewer": "copilot-pull-request-reviewer[bot]"
}

…lticlick-copy-link

# Conflicts:
#	src/Languages/lang_en.json

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Selection handling, clipboard failure handling, and automation focus notifications still need corrections.

3 open issues

Review effort: Balanced

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Unresolved keyboard interaction issues and missing real-platform accessibility validation require fixes and human verification.

No open issues 🎉

2 previously missed issues

In code that hasn't changed since last review

Medium severity Resolve context-menu target for each opening method

src/​UniGetUI.Avalonia/​Views/​Controls/​LinkTextBlock.cs:74

Resolve the target for each context-menu request rather than reusing the last right-click target. _contextLink is set on a right-button press but is not updated by keyboard navigation. After right-clicking URL A, dismissing the menu, and tabbing to URL B, opening the menu with Shift+F10 still makes “Copy link” copy A. On a fresh block, keyboard opening hides the item instead. Use the pointer position for pointer requests and FocusedLink for keyboard requests, or explicitly clear the target for keyboard requests. Add a regression test that opens the menu by keyboard after dismissing a different link's pointer menu.

Medium severity Cache link bounds before focus and selection changes

src/​UniGetUI.Avalonia/​Views/​Controls/​LinkTextBlock.cs:160

Capture the link bounds before changing focus or selection. Selection changes invalidate the inline text layout, as the pointer handlers in this control already account for. This lookup runs immediately after setting the selection, before another layout pass can restore the inline runs, so it can return no bounds and skip BringIntoView. In the package details ScrollViewer, keyboard navigation can therefore select an offscreen link without revealing it. Cache the bounds before Focus and the selection updates, and add a regression test that tabs to a link below the viewport.

3 resolved since last review

Review effort: Balanced

@randy-but-a-ro randy-but-a-ro Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Pull request was approved automatically: the AI review is complete and all its review threads are resolved. 🎉

Integration Details
{
	"deliveryId": "412e56d0-cee3-11f1-9c67-6b2834b48869",
	"headSha": "7d53e29583cba40bcf10ce7c825049abff7adeca",
	"reviewer": "copilot-pull-request-reviewer[bot]"
}

@GabrielDuf
Gabriel Dufresne (GabrielDuf) merged commit 42f84e1 into main Oct 23, 2026
7 checks passed
@GabrielDuf
Gabriel Dufresne (GabrielDuf) deleted the fix/hyperlink-text-multiclick-copy-link branch October 23, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants