Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ It is intentionally not a full architecture manual. It should stay biased toward
- **Seam**: A framework-neutral substitution point that lets the Avalonia layer consume product behavior without referencing LCModel or WinForms — `IEditSession`, `IUiScheduler`, `IDetailLifetime`, `IDetailRefreshCoordinator`, `IXCoreCommandBridge`, `IRecordNavigationContext` in `FwAvalonia/Seams`, alongside boundary data contracts such as `IFwClipboard`. Product implementations live at the xWorks edge. Deliberately narrow: direct Avalonia APIs remain allowed at the UI edge, and `Seams/` holds only real substitution points — not helpers that merely live near one.
- **Fenced edit session**: One `IEditSession` wrapping one LCModel undo task, opened lazily on the first staged edit and ended exactly once by Commit or Cancel. Prevents orphaned undo tasks; implemented by `LcmDetailEditSession`.
- **Settle**: The single auto-save policy for an open edit session — commit if valid, otherwise roll back and notify. Invoked on navigation, window deactivate, undo, and teardown (`DetailEditContextHolder.Settle`).
- **Parity**: Behavioral/visual equivalence between a legacy WinForms view or dialog and its Avalonia replacement. Gaps are recorded as `// TODO` with the blocking facts named, or as approved divergences -- never left implicit. (The older `// PARITY` marker is retired.)
- **Parity**: Behavioral/visual equivalence between a legacy WinForms view or dialog and its Avalonia replacement. WinForms behavior is the default contract. Any intentional difference requires explicit human approval and an entry in the relevant parity document; an unlisted difference is a defect. Gaps are recorded as `// TODO` with the blocking facts named, never left implicit. (The older `// PARITY` marker is retired.)
- **Approved divergence**: An Avalonia behavior that intentionally differs from WinForms, has explicit human approval, and is listed in the relevant parity document. For `.fwlayout` behavior, that document is `Docs/architecture/avalonia-fwlayout-parity.md`.
- **Preview vs POC**: In the Avalonia migration, **preview** means a lightweight sample or design-time path that reuses the shared detail renderer — including the standalone preview host, which runs a view or dialog without a language project; **POC** refers only to the retired spike/evidence vocabulary and should not name live runtime code paths.
- **StringTable localization**: The singleton `StringTable` (FwUtils) loads `strings-<locale>.xml` from the Language Explorer configuration folder and localizes text that originates in XML configuration files — layout/part labels, browse column headings, and XCore menu/command labels. `StringTable.Table.LocalizeAttributeValue(...)` (and `XmlUtils.GetLocalizedAttributeValue(...)`) look the attribute value up in the file's `LocalizedAttributes` group and return the English value unchanged when no localization exists.
- **`.resx` localization strategy**: FieldWorks-owned forms and controls keep their strings in project `.resx` resources, resolved through `ResourceManager` and shipped as satellite assemblies. New FieldWorks-owned UI — including the Avalonia views and dialogs (`FwAvaloniaStrings.resx`, `FwAvaloniaDialogsStrings.resx`) — belongs in this strategy.
Expand Down
186 changes: 186 additions & 0 deletions Docs/architecture/avalonia-fwlayout-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# Avalonia `.fwlayout` Parity

## Intention

Avalonia detail views use the same project `.fwlayout` files as WinForms and
must reproduce WinForms layout selection, fallback, mutation, persistence, and
transient behavior. WinForms is the default behavioral contract. A difference
is a defect unless it has been explicitly approved and recorded in the
`Divergences` section of this document.

This work does not convert Avalonia's pre-alpha `.viewoverride.json` files.
Existing JSON overrides are ignored. They are not imported or automatically
deleted.

This plan covers `.fwlayout` selection, command targeting, mutation,
persistence, and related transient writing-system state. It does not claim that
Avalonia already implements every WinForms editor or layout construct. Such
gaps remain defects to implement and must stay visible as unsupported rows,
with their blocking facts recorded in named TODOs. They are not divergences.
Content that WinForms itself omits (an unresolved part ref, part content
`DataTree.ProcessSubpartNode` does not recognize) is omitted the same way and
reported only through import diagnostics; an unsupported row there would be a
divergence.

## Persistent layout identity

An `.fwlayout` layout is identified by all four attributes used by the WinForms
layout inventory:

| Attribute | Meaning |
| --- | --- |
| `class` | Value of `layout/@class` on the selected XML layout; it can name a base class after fallback. |
| `type` | Value of `layout/@type` on the selected XML layout, normally `detail`. |
| `name` | Value of `layout/@name` on the selected XML layout; it can be `default` after fallback. |
| `choiceGuid` | Value of `layout/@choiceGuid` on the selected XML layout, or absence of that attribute. |

The values are matched case-insensitively, as they are by the WinForms layout
inventory. An absent `choiceGuid` is distinct from a present-empty `choiceGuid`
and from any other present value. Code that clones or persists a layout preserves
the selected XML key spelling so a case-sensitive physical-file replacement does
not append a duplicate.

Layout selection has two identities. The requested identity contains the
object's concrete class, requested layout name (or `default`), `detail` type,
and GUID obtained through `layoutChoiceField`. The resolved identity contains
the four attributes of the XML layout that WinForms actually selected after
fallback. Composed fields and commands carry the resolved identity. A command
never repeats layout fallback; it targets that already-resolved layout or fails
closed.

The caller path is also retained when a field is composed because the same part
can appear through different callers. It is relative to the resolved layout
that WinForms treats as the persistence root after promoting the layout beyond
the final `sublayout`. Each path segment contains the XML element name and its
zero-based ordinal among same-named element siblings. It locates the XML node
to mutate, but it is not a fifth layout-inventory key.

Runtime occurrence identity also carries the ordered four-field identities of
every selected layout crossed through object or sequence descent. Persistence
remains rooted in the outer layout, while this chain prevents a command or
transient state from matching the same caller path under a different nested
choice or fallback result. A `sublayout` resets both the persistence root and
the runtime layout chain.

Object HVO and field name are runtime safety checks. They prevent a command
from reaching a stale or ambiguous row, but they are not persistent layout
identity. Avalonia-generated stable IDs are likewise runtime identities and
must not become `.fwlayout` persistence keys.

## Design

1. Keep the `XCoreMenuBridge` interceptor contract, its disabled-item
normalization, and the `RecordEditView.ResolveShownRecord` refresh.
`OnDetailMenuRequested` renders the native menu only, with no WinForms
adapter-menu fallback: when native menu construction throws, the error is
logged and no menu is shown; when the menu ids resolve to no items, no menu
is shown and nothing is logged. In both cases the post-menu refresh still
runs (see Divergences). JSON-backed persistence and its Show-all code give
way to the parity behavior defined here.
2. Carry `class`, `type`, `name`, and optional `choiceGuid` through layout
loading, view-definition models, composed fields, command identities, menu
bindings, and nested object or sequence composition. Retain the caller path
for the exact composed occurrence.
3. Load the same shipped inventories as the legacy `Inventory`:
`DistFiles/Parts` (`StandardParts.xml`, `GeneratedParts.xml`,
`Standard.fwlayout`, `Generated.fwlayout`) and then
`Language Explorer/Configuration/Parts`, with the later hand-authored files
replacing same-id parts and same-key layouts. Without the first directory
`CmObject-Detail-HeavySummary`, the generated `default` layouts, and the
`autoCustom` part do not exist and every sense subtree vanishes.
4. Match WinForms layout selection and fallback order exactly. For the
requested name, try the concrete class and then each base class with the
requested choice. For an `RnGenericRec`, a missing choice-specific layout is
first cloned from the no-choice layout of the class currently being tried.
If the named search reaches `CmObject` without a match, WinForms changes the
name to `default` and resets the class variable to the concrete class, but
immediately advances to that class's base before the next lookup; it does
not check the concrete class's default. Thus the default search begins at the
concrete class's base. Throw if that search also reaches `CmObject` without a
match. Apply this algorithm to root layouts, nested `sublayout` elements,
and nested object and sequence layouts.
5. Match WinForms handling of a new Notebook record type. If an
`RnGenericRec` choice layout does not exist, clone the no-choice layout, add
the requested `choiceGuid`, add it to the layout inventory, and persist it
to the project `.fwlayout` file before composing against it.
6. Resolve every persistent layout command to the exact hidden WinForms slice
and use the existing WinForms mutation path. Missing or ambiguous targets
fail closed and are logged instead of changing a nearby layout occurrence.
7. Remove the obsolete Avalonia JSON override path. Do not add conversion,
deletion, or dual-write behavior.
8. Port and extend tests around the shared layout inventory, production command
route, and project `.fwlayout` files. Cover root and nested choice layouts,
repeated callers, new Notebook record types, fallback order, hidden-slice
targeting, field-to-field current-state transitions, record and Type
changes, and missing or ambiguous command targets.

## Behavior mapping

| Avalonia action | WinForms behavior to reuse |
| --- | --- |
| Hide or show a field | Update the `visibility` attribute on the caller part through the matching `Slice`. |
| Move a field | Change physical sibling order through `Slice.MoveField`. |
| Configure visible writing systems | Update `visibleWritingSystems` through `MultiStringSlice`, retaining its pronunciation-writing-system side effects. |
| Show all writing systems temporarily | Use WinForms transient state. Do not write the layout file. Reload configured writing systems when the target slice changes from current to not current. |
| Sense or record item header | WinForms renders the HeavySummary SummarySlice as the item's header. Avalonia synthesizes the item header from LexSenseOutline + ShortName and folds a HeavySummary that directly follows it into that header (no repeated row, children at the item's depth). |

For nested edits, WinForms promotes the selected layout after the final
`sublayout` to the persistence root. Avalonia must therefore keep the selected
choice variant and caller path intact so the existing override machinery writes
the intended nested layout rather than a same-named neighbor.

## Acceptance criteria

- Given the same project, tool, object, and `.fwlayout` files, Avalonia and
WinForms resolve the same four-field layout identity at the root and at every
nested layout boundary.
- Choice and `sublayout` resolution does not omit or duplicate any field that
Avalonia supports from the selected layout. Constructs WinForms renders but
Avalonia does not yet support remain visibly represented and tracked rather
than being silently dropped; constructs WinForms omits are omitted.
- Every persistent Avalonia layout command changes the same XML node and uses
the same legacy writer that the equivalent WinForms command uses.
- Moving current selection from the target field to another slice reloads the
configured writing systems and ends temporary Show-all. Recomposition after
a Type or record change cannot revive the old transient reveal.
- A new Notebook record type creates and persists the same choice-specific
layout that WinForms creates.
- No command falls back from an exact choice or caller occurrence to a broader
target merely to make the command succeed.
- Old `.viewoverride.json` files have no effect and require no migration.

## Retirement

After FieldWorks fully switches to Avalonia and no supported WinForms path
depends on `.fwlayout`, retire `.fwlayout` as the project layout-customization
format. Replace it with an Avalonia-owned JSON format and migrate the project
customizations that must survive the cutover. That future migration must define
the JSON schema, conversion and rollback strategy, validation, and removal of
the legacy layout inventory, hidden WinForms slices, and related command bridge.

Until that migration is designed, implemented, and validated, `.fwlayout`
remains the sole source of truth. The obsolete pre-alpha `.viewoverride.json`
format described above is not the future format and must not constrain its
design.

## Divergences

- **Behavior**: a detail-row menu request whose Avalonia-native menu
construction throws (`XCoreMenuBridge` conversion or the host interceptor).
**WinForms**: the legacy `DataTree` shows the xCore menu through the
WinForms adapter `ContextMenuStrip` (`XWindow.ShowContextMenu` ->
`MenuAdapter`).
**Avalonia**: `RecordEditView.OnDetailMenuRequested` logs the
error and shows no menu; the pending Show-all reveal still ends and the
detail view still recomposes. **Why accepted**: the adapter menu bypasses
the `XCoreMenuBridge` interceptor, so its commands would skip the
exact-slice re-targeting and the post-command Avalonia recompose and leave
the view stale. Such a failure is a defect to fix, made visible through the
log rather than masked by a second rendering.

- **Behavior**: a detail-row menu request whose ids resolve to no menu items.
**WinForms**: `MenuAdapter.ShowContextMenu` opens the `ContextMenuStrip`
even when it holds nothing, so an empty popup appears at the pointer.
**Avalonia**: no flyout opens and nothing is logged; the post-menu refresh
still runs. **Why accepted**: an empty popup tells the user nothing and
costs a click to dismiss.
27 changes: 27 additions & 0 deletions Src/Common/Controls/DetailControls/DataTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5415,6 +5415,33 @@ public override bool IsLazyPlaceholder
}
}

public override int LazySequenceFlid
{
get
{
CheckDisposed();
return m_flid;
}
}

public override int LazySequenceIndex
{
get
{
CheckDisposed();
return m_ihvoMin;
}
}

public override object[] LazySequencePath
{
get
{
CheckDisposed();
return m_path == null ? null : m_path.ToArray();
}
}

/// <summary>
/// Turn this dummy slice into whatever it stands for, replacing itself in the data tree's
/// slices (where it occupies slot index) with whatever is appropriate.
Expand Down
17 changes: 17 additions & 0 deletions Src/Common/Controls/DetailControls/Slice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,23 @@ public virtual bool IsLazyPlaceholder
}
}

/// <summary>
/// The sequence flid represented by a lazy placeholder. Real slices do not expose
/// lazy-sequence metadata.
/// </summary>
public virtual int LazySequenceFlid => 0;

/// <summary>
/// The first sequence item index represented by a lazy placeholder. Real slices do not
/// expose lazy-sequence metadata.
/// </summary>
public virtual int LazySequenceIndex => -1;

/// <summary>
/// Gets the XML/object path captured when this lazy placeholder was created.
/// </summary>
public virtual object[] LazySequencePath => null;

/// <summary>
/// In some contexts, we use a "ghost" slice to represent data that
/// has not yet been created. These are "real" slices, but they don't
Expand Down
8 changes: 8 additions & 0 deletions Src/Common/FwAvalonia/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) 2026 SIL International
// This software is licensed under the LGPL, version 2.1 or later
// (http://www.gnu.org/licenses/lgpl-2.1.html)

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("FwAvaloniaTests")]
[assembly: InternalsVisibleTo("xWorksTests")]
8 changes: 5 additions & 3 deletions Src/Common/FwAvalonia/AvaloniaHostControlBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,13 @@ private void UpdateCompanionStripHeight()
/// <param name="anchor">The control to anchor the menu to.</param>
/// <param name="atPointer">True: Open at the pointer location.
/// False: Open under the anchor control.</param>
public void ShowContextMenu(IReadOnlyList<DetailMenuItem> items,
Avalonia.Controls.Control anchor, bool atPointer)
/// <param name="closed">Optional action invoked after the menu closes.</param>
/// <returns>True when a menu was shown.</returns>
public bool ShowContextMenu(IReadOnlyList<DetailMenuItem> items,
Avalonia.Controls.Control anchor, bool atPointer, Action closed = null)
{
var target = anchor ?? Host.Content as Avalonia.Controls.Control;
DetailMenuFlyout.Show(items, target, atPointer);
return DetailMenuFlyout.Show(items, target, atPointer, closed) != null;
}

public void ShowMessage(string message)
Expand Down
Loading
Loading