Skip to content

LT-22691: Copy writing-system menu choices into the view override - #1108

Draft
mark-sil wants to merge 3 commits into
LT-22691dfrom
LT-22691e
Draft

LT-22691: Copy writing-system menu choices into the view override#1108
mark-sil wants to merge 3 commits into
LT-22691dfrom
LT-22691e

Conversation

@mark-sil

@mark-sil mark-sil commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

In the new UI, changing which writing systems a multi-writing-system field shows — unchecking a toggle, re-checking one, or using the Configure dialog — now updates the Avalonia detail view immediately and persists per project. This completes the field-menu family started with Field Visibility and Move Field, and fixes the long-standing bug where unchecking a writing system left the row visible.

Depends on #1097 (the storage layer this writes into); stacked on LT-22691d and draft until it merges. This PR also delivers two commitments from #1097's review discussion: the ordering canonicalization and the UI write path with auto-refresh.

Three commits, in reading order:

  1. Registry refactor — the interceptor's HelpId switch becomes an OverrideCommandRegistry. Behavior-identical; the commit message carries the rationale (natively-handled commands become enumerable data, which a later per-menu-group completion check can query — a switch cannot).
  2. The selection copy — writing-system items dispatch to the hidden adapter slice as before; the resulting selection is then copied into the project view override.
  3. Bridge leaf contract — the interceptor receives the leaf's already-computed display properties (no second Display* round trip), and every leaf, default or retargeted, carries no execute action when disabled.

Where to look:

  • The timing asymmetry is the load-bearing subtlety: toggles read the selection property (written before OnClick returns; the slice reacts later), Configure reads the slice (updated synchronously by the modal dialog). Reading the slice after a toggle stores the pre-click set — that was the bug. Locked by WritingSystemToggle_TwoVernaculars_StoresTheReducedThenRestoredSet, red under the old code.
  • Canonicalization: the stored set is filtered to the slice's options in option order via StringSliceUtils.GetVisibleWritingSystems, so uncheck+recheck never reorders rows and junk tokens never restrict.
  • Cancel writes nothing: the selection is snapshotted before dispatch; an unchanged Configure result is not copied.
  • Guards fail safe with a log trail: unreadable adapter slice, a slice that isn't the clicked row's, and empty sets all decline to write.
  • The bridge's disabled-no-execute rule applies to every tool's menus, not just the Lexicon.

Deliberately not here:

  • "Show all right now" is visibly inert in the Avalonia view — deliberate: it is a transient reveal in legacy, and persisting it would pin the full set. The native transient reveal is the recorded follow-up.
  • Reversal-entries rows (the plugin row neither raises this menu nor reads the restriction yet; the copy log-and-bails there).
  • Mutation-command conversion and adapter end-state (the open LT-22691: Fix command routing through the hidden DataTree adapter #1079 direction question).

Verified: build.ps1 -CommentHygiene clean; 23/23 targeted menu fixtures; 213/213 Detail-fixture sweep after the bridge change. Manual testing is complete — the 9-scenario Sena 3 plan, including the Configure dialog lanes that cannot run headlessly.


Reading this a year from now — start here

This is the write path ("4b") for the SetVisibleWritingSystems override operation whose storage layer landed via #1097 ("4a"). The split existed because a native command layer — one possible answer to the #1079 adapter-direction question — would delete this copy code while the storage survives. The working review record lives in this description; .review/ is gitignored by design.

Decisions, and why
  • A registry instead of the switch, honestly weighed. The switch would have worked (the id-less toggles only need one if). The registry is the deliberate "middle path" from the adapter end-state analysis: natively-handled commands become enumerable data, so a later per-menu-group completion check can ask what is covered and skip building the hidden adapter for a fully covered menu. If the direction ruling ends up plain per-command peeling forever, the cost is one 45-line class.
  • "Copy", not "mirror." This codebase already uses "mirrors legacy X" to mean reimplements the same algorithm (a parity claim). The operation here is one-directional, after-the-fact state copying — so the vocabulary is copy (the action) and stored (what tests assert), and "mirror" was retired from these additions to avoid two senses of one word in one file.
  • Copy-after-dispatch, not replacement. The hidden adapter slice owns the picker, the Configure dialog, and the legacy layout-inventory record; the Avalonia view composes from its own override store. Copying the outcome keeps both sides working without reimplementing either — and it is precisely the code a native command layer would later delete, which is why it is isolated to one registry entry and three private members.
  • Show-all stays visibly inert (chosen over graying it out or reinterpreting it as a persistent clear). Legacy's command is a temporary reveal that reverts when the slice loses currency; every cheap way to make the button "do something" persistent was rejected as data corruption or dishonest UI. Inert-and-visible makes the gap obvious until the native transient reveal is built.
  • The disabled-no-execute rule lives in the bridge, for all leaves. Real UI already blocked disabled clicks; the rule exists for programmatic invokers (tests, future keyboard/automation), and enforcing it once in the bridge deleted a ws-specific guard and made Execute != null a uniform invariant.
Deferred, and what would unblock it
  • Show-all transient reveal (the desired end state): intercept the click into host-level transient state, recompose, and have DetailComposer.ApplyVisibleWritingSystems consult it — it must be composer-level because the restriction can come from the shipped layout, not just the override. The open design decision is expiry (legacy expires on slice-currency loss; Avalonia analogs are field focus loss or record navigation). Roughly 100–150 lines with tests.
  • Reversal-entries rows: three missing pieces — the plugin row raising the standard menu, the row consuming VisibleWritingSystems, and the copy gaining a slice-agnostic options source (a small shared interface). Belongs to reversal-slice Avalonia parity; today the copy logs and declines on that slice type.
  • Adapter end-state: whether the registry grows group-completion checks and native mutation commands is the LT-22691: Fix command routing through the hidden DataTree adapter #1079 direction question; nothing here commits either way.
Preflight review details

The branch went through the 8-angle adversarial review (line-by-line, removed-behavior, cross-file tracing, reuse, simplification, efficiency, altitude, conventions): 34 raw candidates deduplicated to 10 findings, each verified against quoted source. All ten are dispositioned; the significant ones and their fixes:

  • Show-all was being persisted despite being a transient reveal in legacy (OnDataTreeWritingSystemsShowAll never persists; SetCurrentState(false) reverts) — excluded from the copy.
  • Configure-Cancel wrote an override pinning the current set — before/after snapshot comparison; no change, no write.
  • Raw property order and tokens were stored verbatim while legacy renders in options order — canonicalized through StringSliceUtils.GetVisibleWritingSystems; exact-order test assertions.
  • A stale adapter slice could write the wrong row's set — object-identity guard with a log entry.
  • A null override-target location silently disabled the copy — the ws entry now registers before locating.
  • The reversal slice drives the same menu property — both lanes require a MultiStringSlice and log-and-bail otherwise.
  • Bridge-level issues (double GetDisplayProperties per intercepted item; disabled leaves carrying execute) — the leaf-contract commit.
  • Plus an unreachable guard removed, a silent-bail log added, and comment-standard fixes.

Validation: build.ps1 -CommentHygiene clean at every step; 23/23 targeted menu fixtures; 213/213 Detail sweep after the bridge change. The author reviewed every change line-by-line, approving names individually ("copy"/"stored" vocabulary, WritingSystemItem, CopyWritingSystemSelectionToOverride). Manual testing: the 9-scenario Sena 3 plan has been executed by the author — it is the only coverage for the Configure dialog lanes, which cannot run headlessly (modal).

🤖 Generated with Claude Code


This change is Reviewable

mark-sil and others added 3 commits August 25, 2026 06:09
Promote BuildOverrideCommandInterceptor's HelpId switch into an
OverrideCommandRegistry of (command id, item builder) entries.
Behavior is identical; an unregistered command still falls through
to normal mediator dispatch.

A registry, unlike a switch, makes "which commands are handled
natively" enumerable data: a later per-menu-group completion check
can ask it what it covers and skip building the hidden adapter for
a fully covered menu. It also gives the next commit's
writing-system items, which carry no command id and must register
by matcher, the same shape as id-keyed commands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Writing-system toggles and the Configure dialog dispatch to the
hidden adapter slice as before; the resulting selection is then
copied into the project view override so the Avalonia detail view
recomposes with it.

Toggles read the selection property (written before OnClick
returns; the slice reacts later) and canonicalize it to the
slice's option order. Configure is copied only when the dialog
changed the selection, so Cancel writes nothing. The copy is
skipped, with a log entry, when the adapter slice is unreadable
or is not the clicked row's.

Show all right now is deliberately not copied: it is a transient
reveal in the slice, and persisting it would pin the full set. In
the Avalonia view it currently does nothing visible; the native
transient reveal is a planned follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pass each leaf's already-computed display properties through to
the interceptor, so retargeted item builders stop re-querying
GetDisplayProperties -- a second mediator Display* round trip per
intercepted item.

Normalize every leaf, default or retargeted, so a disabled item
carries no execute action. "Execute != null" now means invokable
for every consumer, including programmatic invokers; the
writing-system item's local guard is replaced by this invariant.

Co-Authored-By: Claude Fable 5 <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.

1 participant