Skip to content

LT-22676: Stop browse views dragging the selected row back on scroll - #1156

Open
jasonleenaylor wants to merge 2 commits into
mainfrom
LT-22676-browse-scroll-yank
Open

jasonleenaylor wants to merge 2 commits into
mainfrom
LT-22676-browse-scroll-yank

Conversation

@jasonleenaylor

@jasonleenaylor jasonleenaylor commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Clear the row rectangle XmlBrowseViewBase saves for AdjustScrollRange1
as soon as MakeSelectionVisible returns, so a lazy-box expansion caused
by the user scrolling no longer forces the selected row back on screen.
Guard the idle selection cleanup so installing its replacement insertion
point does not queue another cleanup pass; that loop re-scrolled the row
on every idle tick whenever the current row had no editable cell.
Re-asserting the same index now scrolls only when the row shows a
different object, and PostLayoutInit goes through ScrollSelectedRowIntoView.

Why it failed

Three things in XmlBrowseViewBase cooperated, all confirmed with runtime
tracing in Bulk Edit Phoneme Features on Sena 3:

  1. SaveSelectionInfo (called from MakeSelectionVisible) left the selected
    row's rectangle armed after the call finished. The Views engine raises
    AdjustScrollRange on every lazy-box expansion or contraction, so the first
    user scroll that reached unexpanded rows hit AdjustScrollRange1 with the
    stale anchor and it snapped the view back to the row (the LT-3607 code from
    2006, meant only for the expansions inside MakeSelectionVisible itself).
  2. RemoveRootBoxSelectionOnIdle destroyed a non-editable insertion point in
    the current row and re-created it through SetDefaultInsertionPointInRow,
    which also force-scrolls the row into view. Installing the new selection
    raised SelectionChanged, which queued the idle pass again. In any browse view
    whose current row has no editable cell this ran about 80 times a second
    (about 1,800 MakeSelectionVisible calls in seven idle seconds) and undid a
    page-down within 12 ms. It was also a permanent idle CPU drain.
  3. The same-index branch of the SelectedIndex setter always scrolled the row
    into view, re-arming (1) each time a clerk re-asserted the current record.

The editing selection is never touched, so LT-12092 behaviour is unchanged.

Verification

  • XMLViewsTests: 119 pass with -CommentHygiene -TokenHygiene. Two of the
    five new tests fail on main (anchor survives MakeSelectionVisible;
    same-row re-assert scrolls) and pass on the branch; a third guards the
    LT-3607 behaviour and passes on both.
  • Live on Sena 3: idle MakeSelectionVisible calls in startup + 10 s dropped
    from about 1,800 to 7. Page-down and page-up in Bulk Edit Phoneme Features
    and Bulk Edit Wordforms (about 4,400 rows) leave the view where it was
    scrolled with record 1 still current.
  • Not exercised by me: arrow keys, typing in an editable bulk-edit cell,
    filter/sort changes, Find/Jump (need keyboard focus).

Fixes LT-22676; same defect as LT-20523 (kept open per Beth's request, to be
tested against this build).

Next: approve, or ask for the idle-loop unit test (needs a Mediator in the
fake browse view) before merge.

Preflight review details

Code Review Summary

Branch: LT-22676-browse-scroll-yank
Base: origin/main
Date: 2026-09-23
Review model: Claude Fable 5.1 (Claude Code)
Files changed: 2

Overview

Purpose (author): in every XmlBrowseViewBase browse or bulk-edit view, once a
row is selected the user cannot scroll it out of view; the viewport is dragged
back (LT-22676, duplicate of LT-20523). The branch changes only when the view
scrolls; the editing selection is never touched (LT-12092 constraint).

Analysis found three cooperating mechanisms, all confirmed with runtime tracing
in Bulk Edit Phoneme Features on Sena 3 before and after the change: a stale
SaveSelectionInfo anchor consumed by AdjustScrollRange1 on the next lazy-box
expansion; an infinite idle loop in RemoveRootBoxSelectionOnIdle (about 80
passes a second, each force-scrolling the row) whenever the current row has no
editable cell; and the same-index branch of the SelectedIndex setter always
scrolling. The fixes are scoped to XmlBrowseViewBase; SimpleRootSite is not
edited.

Contract/API Changes

  • XmlBrowseViewBase.MakeSelectionVisible(IVwSelection, bool, bool, bool) is
    now overridden (base is protected virtual in SimpleRootSite); no subclass
    in the repository overrides it.
  • New internal void ScrollSelectedRowIntoView(); PostLayoutInit delegates
    to it. No public surface change.
  • Behaviour: assigning SelectedIndex to the current index with the same
    object no longer scrolls the row into view. Callers that re-assert the current
    index (RecordBrowseView.OnRecordNavigation, ShowRecord) keep scrolling when
    the object at that index changed (filter/sort reload).

Findings

Critical - Must address before merge

None.

Important - Should address before merge

  • No unit test covers the idle-loop guard (m_fReplacingIdleSelection).
    (author: the fake browse view has no Mediator/IdleQueue; adding one is an
    explicit decision deferred to a follow-up. Covered live: idle
    MakeSelectionVisible calls in startup + 10 s dropped from about 1,800 to 7.)

Minor - Consider

  • Same-index re-assert no longer scrolls an off-screen current row back
    into view when the object is unchanged.
    (author: intended; approved in the
    plan as the distinction between "selection changed" and "selection
    re-asserted". Window-size changes are already handled by
    SimpleRootSite.OnSizeChanged.)

Required Validation / Evidence

  • .\test.ps1 -SkipNative -TestProject XMLViewsTests -CommentHygiene -TokenHygiene:
    119 passed; comment and token hygiene clean. Two of the five new tests fail
    on main and pass on the branch; a third guards the LT-3607 behaviour.
  • Live on Sena 3 (WinForms UI): page-down and page-up in Bulk Edit Phoneme
    Features and Bulk Edit Wordforms leave the view where it was scrolled with
    record 1 still current; idle loop gone.
  • Author manual checks on the built branch: arrow keys in browse views (found
    the separate 3 px nudge, now LT-22489); Bulk Edit Phoneme Features scrolling.
  • Not exercised: typing in an editable bulk-edit cell (LT-12092 path),
    filter/sort change, Find/Jump. The editing selection code is untouched.
  • Pre-existing uncommitted files in the working tree (.mcp.json, Docs/,
    Installer/) are not part of this branch and were left alone.

Positive Observations

  • Each mechanism has a one-line guard or scoping change rather than a rewrite
    of the 2006/2011 scrolling code, and the LT-3607 and LT-12092 behaviours are
    preserved and tested.
  • Tests target the class that changed through the existing
    FakeXmlBrowseViewBase, with an ObjectListPublisher for the row list.

Interview Notes

  • Author chose plan B (explicit scroll intent) over a base-class hook, and asked
    for ScrollSelectedRowIntoView to be internal, since its only caller is
    PostLayoutInit.
  • Author confirmed the idle-loop guard's unit test is deferred rather than
    forgotten (needs a Mediator in the fake; ask first).
  • Ticket: LT-22676 (user-visible; FW 9.3). LT-20523 stays open per Beth Bryson,
    to be tested against this build.

Suggested Review Focus

  • RemoveRootBoxSelectionOnIdle guard: confirm the replacement insertion
    point should not queue a second cleanup pass.
  • Same-index branch of SelectedIndex: agree that re-asserting the same
    object should not scroll.
  • MakeSelectionVisible override: anchor cleared in finally covers the
    early-return paths in the base method.

🤖 Generated with Claude Code


This change is Reviewable

Clear the row rectangle XmlBrowseViewBase saves for AdjustScrollRange1
as soon as MakeSelectionVisible returns, so a lazy-box expansion caused
by the user scrolling no longer forces the selected row back on screen.
Guard the idle selection cleanup so installing its replacement insertion
point does not queue another cleanup pass.
Re-asserting the same index now scrolls only when the row shows a
different object, and PostLayoutInit goes through ScrollSelectedRowIntoView.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.00000% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.95%. Comparing base (2f1d9db) to head (70f69da).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
Src/Common/Controls/XMLViews/XmlBrowseViewBase.cs 70.00% 9 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1156      +/-   ##
==========================================
+ Coverage   38.89%   38.95%   +0.06%     
==========================================
  Files        1518     1520       +2     
  Lines      352262   352514     +252     
  Branches    40597    40638      +41     
==========================================
+ Hits       137005   137329     +324     
+ Misses     185976   185904      -72     
  Partials    29281    29281              
Files with missing lines Coverage Δ
Src/Common/Controls/XMLViews/BrowseViewer.cs 50.89% <100.00%> (+0.09%) ⬆️
Src/xWorks/RecordBrowseView.cs 52.92% <100.00%> (+0.20%) ⬆️
Src/xWorks/RecordClerk.cs 43.55% <100.00%> (+0.27%) ⬆️
Src/Common/Controls/XMLViews/XmlBrowseViewBase.cs 50.23% <70.00%> (+1.00%) ⬆️

... and 35 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

NUnit Tests

    1 files  ±0      1 suites  ±0   13m 22s ⏱️ + 2m 57s
6 238 tests +6  6 153 ✅ +6  85 💤 ±0  0 ❌ ±0 
6 247 runs  +6  6 162 ✅ +6  85 💤 ±0  0 ❌ ±0 

Results for commit 70f69da. ± Comparison against base commit 0ceeb22.

♻️ This comment has been updated with latest results.

if (value >= 0 && m_wantScrollIntoView)
// Never touch the editing selection here (LT-12092). Scroll only when the
// row now shows a different object, e.g. after a re-sort (LT-22676).
if (value >= 0 && m_wantScrollIntoView && GetNewSelectionObject(value) != m_hvoOldSel)

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.

Jumping to the record that's already current no longer brings it into view. Now that the user can scroll the current row off screen, Find Lexical Entry (or any JumpToRecord) on that same record goes through RecordClerk.JumpToIndex. Because the index hasn't changed, it just re-sends RecordNavigation. RecordBrowseView.OnRecordNavigation then sets the same index again, and this branch skips the scroll because the object is the same. The list stays where the user left it, and the entry they just chose isn't visible.

Can an explicit jump still scroll the row into view, while other re-asserts of the current record keep leaving the scroll position alone? ScrollSelectedRowIntoView looks like the right hook if the jump path can reach it.

JumpToRecord now tells the RecordNavigation payload when it asked for the
record that was already current, and RecordBrowseView scrolls the selected
row into view for that case. JumpToIndex on the current index still leaves
the scroll position alone, so refreshes do not drag the row back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants