Skip to content

LT-22820: Let Ctrl+Z and Ctrl+Y reach FieldWorks from Avalonia fields - #1242

Open
thejambi wants to merge 5 commits into
mainfrom
LT-22820-avalonia-undo-redo-keys
Open

thejambi wants to merge 5 commits into
mainfrom
LT-22820-avalonia-undo-redo-keys

Conversation

@thejambi

@thejambi thejambi commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

In the New UI detail view, Ctrl+Z and Ctrl+Y now run FieldWorks' Undo and Redo, as they do in the WinForms view. After typing in a field, the first Ctrl+Z saves the typing as its own undo step, and each further Ctrl+Z works back through the undo stack; Ctrl+Y redoes. Until now each Avalonia text box kept its own undo: Ctrl+Z took back one typed letter at a time, or cleared a field it had just shown, and Ctrl+Y did nothing.

Reviewer notes

Root cause. Two things kept the keys from FieldWorks. Each Avalonia TextBox handled Ctrl+Z itself, so the key stopped there. And even a key Avalonia leaves unhandled never reaches FieldWorks' shortcuts reliably. XWindow matches keys to commands in its KeyDown handler (XWindow_KeyDown, checking each command's enabled state at that moment), which it receives through KeyPreview. Only keys sent to WinForms controls go through KeyPreview, never keys typed into the Avalonia window. The only other route is the Edit menu item's own ShortcutKeys, which WinForms fires only if the item was enabled when the menu was last built. That is why an early version of this fix, which only left the key unhandled, worked only some of the time in manual testing.

Deliberate rules.

  • DataTree.OnUndoRedoKeyDown tunnels, so it sees Ctrl+Z and Ctrl+Y before any editor. With an edit open, it saves the edit (validation-gated, through the same OnSave as autosave) and stops, so the first press after typing never undoes. This matches the DetailEditContextHolder undo guard that already handles Undo from the toolbar and menu.
  • A reference row's add slot and item boxes (FwReferenceVectorField) hold typed text with no edit session open. StageUnstagedText (the counterpart of the existing DiscardUnstagedText) stages that text without saving it. It runs on both Ctrl+Z routes. In the view, OnUndoRedoKeyDown stages it, saves, and stops, even when the domain refuses the text. On the Edit-menu shortcut and the toolbar, the row has registered it with the edit context through the new IPendingEditFlushRegistry, which DetailEditContextBase implements, so the undo guard flushes it and saves it as one step, as it already did for the reversal entries field. Settle runs the same flush, so moving to another record with text still in a reference box now saves it too, as it already did for reversal entries.
  • An edit that fails validation is handled differently on the two routes, and that is left as is. In the view, Ctrl+Z shows the validation message and keeps the edit open, as Enter and autosave do. Through the undo guard, Settle rolls the edit back and says why. This already differed between Enter and toolbar Undo. Reachable by clearing both a list item's name and abbreviation.
  • With no edit open, the key goes out through DataTree.UndoRedoKeyUnhandled → AvaloniaHostControlBase.UndoRedoKeyUnhandled (as WinForms Keys) → RecordEditView.OnAvaloniaUndoRedoKey, which calls XWindow.HandleKeyDown. So the CmdUndo/CmdRedo bindings in Main.xml (Ctrl+Z, Ctrl+Y) stay the only definition of which key means Undo or Redo.
  • The hand-off is posted with ScheduleOnUiThread, so an undo that re-shows the detail view does not rebuild it inside its own key event.
  • Keys typed in a popup are left alone: the handler acts only when the key's source is in the DataTree's own visual tree. Flyout content routes its key events through the placement target, so without this check the FwOptionChooser dropdown's filter box would lose its own undo, and its Ctrl+Z would undo a FieldWorks change while the picker is open.
  • Only Ctrl+Z and Ctrl+Y count, matching FieldWorks' bindings; Ctrl+Shift+Z is not treated as Redo.
  • No double handling: when the Edit menu's shortcut does fire in WinForms' ProcessCmdKey, the key never reaches Avalonia.
  • A host that does not subscribe (the preview host) leaves the key to the focused editor, as before. The Avalonia dialogs (Insert Entry, Add New Sense) do not host a DataTree and are unaffected.

Tests. New in DetailEditingTests:

  • CtrlZ_WhileEditing_SavesTheEdit_AndStopsAtTheView
  • CtrlZAndCtrlY_WithNoOpenEdit_AreOfferedToTheHost: repeated presses after the save are each offered.
  • OnlyCtrlZAndCtrlY_AreOfferedToTheHost
  • CtrlZ_InAPopupEditor_IsLeftToThatEditor
  • CtrlZ_InAReferenceRowHoldingText_StopsOnce: add slot and item box, accepted and refused text, and the next press going on to Undo.
  • AReferenceRow_RegistersAFlush_ThatStagesWithoutSaving and AReferenceRowWithNothingToType_RegistersNoFlush: the flush the undo guard runs.

DetailHostControlTests covers the key conversion and the host event round trip.

Results:

  • FwAvaloniaTests: 989 total, 988 passed, 1 skipped.
  • xWorksTests: 1,931 total, 1,929 passed, 2 skipped (run before the popup and reference-text commits, which touch only FwAvalonia).
  • Build, comment hygiene and token hygiene are clean.
  • RecordEditView.OnAvaloniaUndoRedoKey has no automated test, since it needs a live XWindow.

Manual tests. Done in FLEx with the New UI, on the ticket's three scenarios:

  • Type in a field, then press Ctrl+Z repeatedly.
  • Press Ctrl+Z in a field showing text.
  • Edit a field, undo it from the toolbar, then press Ctrl+Y.

That run was before the popup and reference-text commits. Retested at 335f29a:

  • Ctrl+Z in a chooser's dropdown filter box undoes only the filter text; the record is unchanged.
  • Text typed into an allomorph's Environments slot is saved by the first Ctrl+Z, both in the view and through the Edit menu's shortcut, and by toolbar Undo. Nothing earlier is undone, and the next press undoes the environment.

Out of scope. Other FieldWorks shortcuts typed in Avalonia fields (for example F-keys and Ctrl+letter commands) still depend on the Edit menu item's enabled state; extending the hand-off beyond Undo and Redo is a separate change.

🤖 Generated with Claude Code


This change is Reviewable

Zachary Burnham and others added 3 commits November 11, 2026 07:45
In the Avalonia detail view, each text box ran its own undo: Ctrl+Z took
back one typed letter at a time, or cleared a field it had just shown,
and Ctrl+Y did nothing. The box marked both keys handled, so FieldWorks'
Undo and Redo never saw them.

The detail view turns off its text boxes' own undo, so with no edit open
both keys go on to FieldWorks' Undo and Redo. With an edit open, the
first Ctrl+Z or Ctrl+Y saves it as its own undo step and stops, as
moving to another field does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After the first Ctrl+Z saved an edit, later presses in an Avalonia field
usually did nothing. Leaving the key unhandled was not enough: FieldWorks
runs its shortcuts from the main window's KeyDown, which never sees keys
typed into Avalonia. The Edit menu's own Ctrl+Z shortcut fired only when
the menu item happened to be enabled the last time the menu was built.

The detail view now raises an event for an undo or redo key it leaves
unhandled, and the record edit view hands that key to the main window's
shortcut handling, which checks whether Undo or Redo is available now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Turning off every text box's own undo also reached the option chooser's
dropdown filter box, and a Ctrl+Z typed there ran FieldWorks' Undo while
the picker was still open. The detail view now takes Ctrl+Z and Ctrl+Y
before its editors do, but only for keys typed in the view itself, so a
popup's editor keeps its own undo. Ctrl+Shift+Z is no longer treated as
redo, since FieldWorks binds Redo only to Ctrl+Y.

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

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

NUnit Tests

    1 files  ± 0      1 suites  ±0   13m 16s ⏱️ + 5m 2s
7 254 tests +91  7 169 ✅ +91  85 💤 ±0  0 ❌ ±0 
7 263 runs  +91  7 178 ✅ +91  85 💤 ±0  0 ❌ ±0 

Results for commit 335f29a. ± Comparison against base commit c6cd25f.

This pull request removes 9 and adds 100 tests. Note that renamed tests count towards both.
SIL.FieldWorks.XWorks.LexiconMenuAuthorityTests ‑ InsertLeaves_OutsideTheScope_LeaveTheirIdsWithTheAdapter
SIL.FieldWorks.XWorks.MorphologyEditor.RuleFormulaControlNaturalClassNameTests ‑ ClassNamedForARuleBehindAPrefix_IsNotUserDefined
SIL.FieldWorks.XWorks.MorphologyEditor.RuleFormulaControlNaturalClassNameTests ‑ ClassWithAnEmptyName_IsNotUserDefined
SIL.FieldWorks.XWorks.MorphologyEditor.RuleFormulaControlNaturalClassNameTests ‑ ClassWithNoName_IsNotUserDefined
SIL.FieldWorks.XWorks.MorphologyEditor.RuleFormulaVcNaturalClassDisplayTests ‑ RuleNamedClass_AbbreviatedC_DrawsFeature
SIL.FieldWorks.XWorks.MorphologyEditor.RuleFormulaVcNaturalClassDisplayTests ‑ RuleNamedClass_OnceNamed_DrawsAbbreviation
SIL.FieldWorks.XWorks.MorphologyEditor.RuleFormulaVcNaturalClassDisplayTests ‑ RuleNamedClass_WithNoFeatures_DrawsQuestions
SIL.FieldWorks.XWorks.MorphologyEditor.RuleFormulaVcNaturalClassDisplayTests ‑ RuleNamedClass_WithOneFeature_DrawsFeature
SIL.FieldWorks.XWorks.MorphologyEditor.RuleFormulaVcNaturalClassDisplayTests ‑ RuleNamedClass_WithThreeFeatures_DrawsEveryFeatureInAPile
FwAvaloniaTests.DetailEditingViewTests ‑ AReferenceItem_RegistersAFlush_ThatStagesWithoutSaving
FwAvaloniaTests.DetailEditingViewTests ‑ AReferenceRowWithNothingToType_RegistersNoFlush
FwAvaloniaTests.DetailEditingViewTests ‑ AReferenceSlot_RegistersAFlush_ThatStagesWithoutSaving
FwAvaloniaTests.DetailEditingViewTests ‑ CtrlZAndCtrlY_WithNoOpenEdit_AreOfferedToTheHost
FwAvaloniaTests.DetailEditingViewTests ‑ CtrlZ_InAPopupEditor_IsLeftToThatEditor
FwAvaloniaTests.DetailEditingViewTests ‑ CtrlZ_InAReferenceItemHoldingRefusedText_StopsOnce
FwAvaloniaTests.DetailEditingViewTests ‑ CtrlZ_InAReferenceItemHoldingText_SavesIt_ThenUndoes
FwAvaloniaTests.DetailEditingViewTests ‑ CtrlZ_InAReferenceSlotHoldingRefusedText_StopsOnce
FwAvaloniaTests.DetailEditingViewTests ‑ CtrlZ_InAReferenceSlotHoldingText_SavesIt_ThenUndoes
FwAvaloniaTests.DetailEditingViewTests ‑ CtrlZ_WhileEditing_SavesTheEdit_AndStopsAtTheView
…

♻️ This comment has been updated with latest results.

@codecov-commenter

codecov-commenter commented Nov 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.54839% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 40.83%. Comparing base (36f0082) to head (335f29a).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
Src/Common/FwAvalonia/Detail/DataTree.cs 84.21% 0 Missing and 3 partials ⚠️
Src/Common/FwAvalonia/Detail/FwFieldControls.cs 95.55% 1 Missing and 1 partial ⚠️
Src/Common/FwAvalonia/AvaloniaHostControlBase.cs 94.11% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1242      +/-   ##
==========================================
+ Coverage   40.60%   40.83%   +0.23%     
==========================================
  Files        1561     1573      +12     
  Lines      356495   357244     +749     
  Branches    41666    41830     +164     
==========================================
+ Hits       144744   145870    +1126     
+ Misses     182435   182059     -376     
+ Partials    29316    29315       -1     
Files with missing lines Coverage Δ
Src/Common/FwAvalonia/DetailHostControl.cs 60.78% <100.00%> (-9.49%) ⬇️
Src/xWorks/Avalonia/DetailEditContextBase.cs 76.92% <ø> (ø)
...xWorks/Avalonia/Hosting/RecordEditView.Avalonia.cs 63.15% <100.00%> (+0.99%) ⬆️
Src/Common/FwAvalonia/AvaloniaHostControlBase.cs 51.61% <94.11%> (+6.75%) ⬆️
Src/Common/FwAvalonia/Detail/FwFieldControls.cs 88.32% <95.55%> (+0.28%) ⬆️
Src/Common/FwAvalonia/Detail/DataTree.cs 92.27% <84.21%> (-0.24%) ⬇️

... and 58 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.

Zachary Burnham and others added 2 commits November 11, 2026 17:13
A reference row's add slot and item boxes hold typed text themselves and
open no edit session until focus leaves them or Enter is pressed. Ctrl+Z
there went straight to FieldWorks' Undo, which undid an earlier change
while the typed text waited to be saved later. The detail view now stages
that text first, as leaving the box would, and the key stops there. Text
the domain refuses still takes the key, so the step before it is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ctrl+Z sometimes runs through the Edit menu's own shortcut and never
reaches the detail view, and toolbar Undo never does. Both go to the undo
guard, which saved only the editors registered with the edit context, and
reference rows had not registered. So typed reference text still waited
while an earlier change was undone.

Reference rows now register with the edit context through a new
IPendingEditFlushRegistry. Their flush stages the typed text without
saving it, and the guard saves it as one step. The detail view's own
Ctrl+Z handling stages the same way and then saves. Tests now cover item
boxes as well as the add slot, and check that the press after refused
text goes on to Undo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thejambi
thejambi marked this pull request as ready for review November 12, 2026 20:15
// An undo started outside this view, from the menu or toolbar, stages typed text
// through the context first; it would otherwise undo an earlier change instead.
var registry = editContext as IPendingEditFlushRegistry;
if (registry != null && _stageUnstaged.Count > 0)

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.

Opening a dialog while typing in a reference box now saves the half-typed text. In Environments, using Insert Natural Class partway through an environment adds the unfinished environment to the allomorph and to the project's environment list. The natural class then either ends up in a second environment or is lost.

To reproduce: in an allomorph's Environments add slot, type / _, then right-click and choose Insert Natural Class. The chooser is a modal dialog, so opening it deactivates the main window, and the deactivate hook (AttachDeactivateHook) calls Settle(). With this registration, Settle() now runs the row's flush, which calls TryCreateAndAddReferenceItem("/ _"). FindOrCreateEnvironment accepts any non-blank text, so the partial environment is created, added and committed. Describe Error (a message box) and Alt+Tab take the same path.

The flush ignores _menuGestureActive, which CommitUnlessMenuGesture respects for this same reason. I traced this through the code but haven't run it in FLEx. Could the flush skip a row while a menu gesture, or a dialog it opened, is in progress? Or could the deactivate settle leave reference rows out?

foreach (var vector in VectorRows)
heldText |= vector.StageUnstagedText();
if (_editContext != null && _editContext.IsOpen)
OnSave();

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.

When the open edit fails validation, Ctrl+Z here calls OnSave(), which shows the message and keeps the edit open. Every later press does the same, so nothing is ever restored. For example, clear an entry's Lexeme Form and press Ctrl+Z: the user wants the form back, and only Escape gives it to them. Before this PR the text box's own undo restored it, and WinForms restores it too. Should Ctrl+Z cancel an invalid edit, as Escape does, instead of repeating the error?

var heldText = false;
foreach (var vector in VectorRows)
heldText |= vector.StageUnstagedText();
if (_editContext != null && _editContext.IsOpen)

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.

Did you know that after typing in a field, it takes two presses of Ctrl+Z to undo the typing? The first press saves the edit and stops, so nothing visible happens, and only the second press reverts it. In WinForms one press reverts the typing. I can see this matches the toolbar Undo guard (DetailEditContextHolder.OnDoingUndoOrRedo), which also stops after settling, but it doesn't seem like the behavior the user would expect. Should it be one press to revert the typing?

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