Skip to content

Commit de9fe70

Browse files
Split IRegionEditContext into sub-capability interfaces
The core edit-context interface carried every specialized edit gesture, so adding a field kind's ops meant a widening edit that forced every implementer (base, in-memory, composed, preview, reversal, the test fake) to stub a new method. Factor the specialized clusters out. - Move the four StText paragraph-CRUD methods (TrySetParagraphText, TrySetParagraphStyle, TryInsertParagraph, TryDeleteParagraph) into a new optional IStructuredTextEditing interface. The one caller (FwStructuredTextField) queries `ctx as IStructuredTextEditing` and treats null as "unsupported" - the same rejection the core methods returned on a context with no StText rows. Only the composed context and the preview/test doubles implement it; the base and the in-memory/reversal contexts drop their stubs. Adding a future kind's ops is now a new sub-interface plus one implementer, not a shotgun edit across every context. - Remove the five picture methods (TryInsertPicture, TryReplacePictureFile, TryDeletePicture, TrySetPictureMetadata, TryInsertPictureOrc). Picture editing was removed from the Avalonia region (the picture slice renders the Unsupported row), no caller invoked them, and the composed context did not override them - they were dead stubs in every implementer. Wrapping dead surface in an IPictureEditing that nothing implements or consumes would be empty ceremony, so the cluster is removed instead. RegionPictureMetadata (used by the picture-properties dialog) is unaffected. Behavior-preserving: no gesture that succeeded before is rejected now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0bc3b9f commit de9fe70

10 files changed

Lines changed: 83 additions & 230 deletions

Src/Common/FwAvalonia/FwAvaloniaTests/RegionEditingTests.cs

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
namespace FwAvaloniaTests
2323
{
2424
/// <summary>Records edit-context traffic so view editing behavior can be asserted without LCModel.</summary>
25-
internal sealed class FakeRegionEditContext : IRegionEditContext
25+
internal sealed class FakeRegionEditContext : IRegionEditContext, IStructuredTextEditing
2626
{
2727
public readonly List<(string Field, string Ws, string Value)> TextEdits = new List<(string, string, string)>();
2828
public readonly List<(string Field, string Ws, RegionRichTextValue Value)> RichTextEdits
@@ -119,50 +119,6 @@ public bool TryDeleteParagraph(LexicalEditRegionField field, int paragraphIndex)
119119
return ParagraphGestureResult;
120120
}
121121

122-
// §19d: recorded picture traffic, so the picture field view can be asserted without LCModel.
123-
public readonly List<(string Field, string SourceFile, RegionPictureMetadata Metadata)> PictureInserts
124-
= new List<(string, string, RegionPictureMetadata)>();
125-
public readonly List<(string Field, string SourceFile)> PictureReplaces = new List<(string, string)>();
126-
public readonly List<string> PictureDeletes = new List<string>();
127-
public readonly List<(string Field, RegionPictureMetadata Metadata)> PictureMetadataEdits
128-
= new List<(string, RegionPictureMetadata)>();
129-
public readonly List<(string Field, string Ws, int Caret, string SourceFile)> PictureOrcInserts
130-
= new List<(string, string, int, string)>();
131-
132-
/// <summary>What the next picture gesture reports (false = rejected, e.g. a missing file).</summary>
133-
public bool PictureGestureResult = true;
134-
135-
public bool TryInsertPicture(LexicalEditRegionField field, string sourceFile, RegionPictureMetadata metadata)
136-
{
137-
PictureInserts.Add((field.Field, sourceFile, metadata));
138-
return PictureGestureResult;
139-
}
140-
141-
public bool TryReplacePictureFile(LexicalEditRegionField field, string sourceFile)
142-
{
143-
PictureReplaces.Add((field.Field, sourceFile));
144-
return PictureGestureResult;
145-
}
146-
147-
public bool TryDeletePicture(LexicalEditRegionField field)
148-
{
149-
PictureDeletes.Add(field.Field);
150-
return PictureGestureResult;
151-
}
152-
153-
public bool TrySetPictureMetadata(LexicalEditRegionField field, RegionPictureMetadata metadata)
154-
{
155-
PictureMetadataEdits.Add((field.Field, metadata));
156-
return PictureGestureResult;
157-
}
158-
159-
public bool TryInsertPictureOrc(LexicalEditRegionField field, string ws, int caretPosition,
160-
string sourceFile, RegionPictureMetadata metadata)
161-
{
162-
PictureOrcInserts.Add((field.Field, ws, caretPosition, sourceFile));
163-
return PictureGestureResult;
164-
}
165-
166122
public IReadOnlyList<string> Validate() => ValidateResult;
167123

168124
public void Commit()

Src/Common/FwAvalonia/Preview/LexicalEditPreviewSupport.cs

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ public LexicalEditPreviewScenario(LexicalEditRegionModel model, IRegionEditConte
130130
public IRegionEditContext EditContext { get; }
131131
}
132132

133-
internal sealed class PreviewRegionEditContext : IRegionEditContext
133+
internal sealed class PreviewRegionEditContext : IRegionEditContext, IStructuredTextEditing
134134
{
135135
public bool IsOpen { get; private set; }
136136

@@ -189,20 +189,6 @@ public bool TryDeleteParagraph(LexicalEditRegionField field, int paragraphIndex)
189189
return true;
190190
}
191191

192-
// §19d: the preview support context stages pictures/audio as "accepted" so the preview renders the
193-
// affordances; the preview never touches real LCModel/files (it is the detached preview path).
194-
public bool TryInsertPicture(LexicalEditRegionField field, string sourceFile, RegionPictureMetadata metadata)
195-
{ IsOpen = true; return true; }
196-
public bool TryReplacePictureFile(LexicalEditRegionField field, string sourceFile)
197-
{ IsOpen = true; return true; }
198-
public bool TryDeletePicture(LexicalEditRegionField field)
199-
{ IsOpen = true; return true; }
200-
public bool TrySetPictureMetadata(LexicalEditRegionField field, RegionPictureMetadata metadata)
201-
{ IsOpen = true; return true; }
202-
public bool TryInsertPictureOrc(LexicalEditRegionField field, string ws, int caretPosition,
203-
string sourceFile, RegionPictureMetadata metadata)
204-
{ IsOpen = true; return true; }
205-
206192
public IReadOnlyList<string> Validate()
207193
=> Array.Empty<string>();
208194

Src/Common/FwAvalonia/Region/FwStructuredTextField.cs

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ namespace SIL.FieldWorks.Common.FwAvalonia.Region
2020
/// replacement for the legacy <c>StTextSlice</c> RootSite rich editor. A vertical stack of one
2121
/// bordered, dense editor row per paragraph; each row carries a run-aware text editor (the SAME
2222
/// staging the single-WS <see cref="FwMultiWsTextField"/> uses — TextChanged replays the untouched
23-
/// runs around the edit and stages through <see cref="IRegionEditContext.TrySetParagraphText"/>),
23+
/// runs around the edit and stages through <see cref="IStructuredTextEditing.TrySetParagraphText"/>),
2424
/// a per-paragraph named-style picker (the shared <see cref="FwOptionPicker"/>), and add/delete
2525
/// paragraph affordances. Enter at a paragraph's end inserts a paragraph after it; Backspace in an
2626
/// empty paragraph (when more than one remains) deletes it.
@@ -53,11 +53,15 @@ public FwStructuredTextField(
5353
AutomationProperties.SetAutomationId(this, automationId);
5454
AutomationProperties.SetName(this, field.Label ?? field.Field ?? automationId);
5555

56+
// Paragraph CRUD is the optional IStructuredTextEditing capability; a context without it leaves
57+
// the rows displayed but inert (each gesture below no-ops), the same rejection the former core
58+
// IRegionEditContext paragraph methods returned on a context with no StText rows.
59+
var structuredText = editContext as IStructuredTextEditing;
5660
var editable = editContext != null && field.IsEditable;
5761
var paragraphs = field.Paragraphs;
5862
for (var i = 0; i < paragraphs.Count; i++)
5963
{
60-
Children.Add(BuildParagraphRow(field, automationId, editContext, writingSystemFocused,
64+
Children.Add(BuildParagraphRow(field, automationId, structuredText, writingSystemFocused,
6165
gestureCompleted, clipboard, paragraphs[i], i, paragraphs.Count, editable));
6266
}
6367

@@ -66,13 +70,13 @@ public FwStructuredTextField(
6670
// — the first keystroke materializes the StText through the edit-context setter (index 0).
6771
if (paragraphs.Count == 0)
6872
{
69-
Children.Add(BuildParagraphRow(field, automationId, editContext, writingSystemFocused,
73+
Children.Add(BuildParagraphRow(field, automationId, structuredText, writingSystemFocused,
7074
gestureCompleted, clipboard, new RegionParagraph(null), 0, 1, editable));
7175
}
7276
}
7377

7478
private Control BuildParagraphRow(
75-
LexicalEditRegionField field, string automationId, IRegionEditContext editContext,
79+
LexicalEditRegionField field, string automationId, IStructuredTextEditing editContext,
7680
Action<string> writingSystemFocused, Action gestureCompleted, IFwClipboard clipboard,
7781
RegionParagraph paragraph, int index, int paragraphCount, bool fieldEditable)
7882
{
@@ -129,7 +133,7 @@ private Control BuildParagraphRow(
129133
{
130134
if (e.Key == Key.Enter)
131135
{
132-
if (editContext.TryInsertParagraph(field, index))
136+
if (editContext != null && editContext.TryInsertParagraph(field, index))
133137
{
134138
gestureCompleted?.Invoke();
135139
e.Handled = true;
@@ -142,7 +146,7 @@ private Control BuildParagraphRow(
142146
&& box.CaretIndex == 0
143147
&& paragraphCount > 1)
144148
{
145-
if (editContext.TryDeleteParagraph(field, index))
149+
if (editContext != null && editContext.TryDeleteParagraph(field, index))
146150
{
147151
gestureCompleted?.Invoke();
148152
e.Handled = true;
@@ -158,7 +162,7 @@ private Control BuildParagraphRow(
158162
addAffordance = BuildIconButton(automationId + ".Para." + index + ".Add", "+",
159163
FwAvaloniaStrings.AddParagraph, () =>
160164
{
161-
if (editContext.TryInsertParagraph(field, index))
165+
if (editContext != null && editContext.TryInsertParagraph(field, index))
162166
gestureCompleted?.Invoke();
163167
});
164168

@@ -169,7 +173,7 @@ private Control BuildParagraphRow(
169173
deleteAffordance = BuildIconButton(automationId + ".Para." + index + ".Delete", "×",
170174
FwAvaloniaStrings.DeleteParagraph, () =>
171175
{
172-
if (editContext.TryDeleteParagraph(field, index))
176+
if (editContext != null && editContext.TryDeleteParagraph(field, index))
173177
gestureCompleted?.Invoke();
174178
});
175179
}
@@ -241,7 +245,7 @@ private Control BuildParagraphRow(
241245
// a TextChanged stages a plain-text-over-preserved-runs edit through the paragraph seam. A
242246
// last-staged guard keeps the template's initial set and no-op events from staging; the guard
243247
// advances only on a successful stage so a failed write re-attempts.
244-
private void WireParagraphTextEditing(LexicalEditRegionField field, IRegionEditContext editContext,
248+
private void WireParagraphTextEditing(LexicalEditRegionField field, IStructuredTextEditing editContext,
245249
TextBox box, RegionRichTextValue currentRich, int index, Action<RegionRichTextValue> onStaged)
246250
{
247251
var lastStaged = currentRich?.PlainText ?? string.Empty;
@@ -253,7 +257,7 @@ private void WireParagraphTextEditing(LexicalEditRegionField field, IRegionEditC
253257
var updatedRich = RegionRichTextEditAlgorithms.ApplyPlainTextEdit(
254258
currentRich ?? RegionRichTextEditAlgorithms.FromRuns(string.Empty, Array.Empty<RegionTextRun>()),
255259
text);
256-
if (editContext.TrySetParagraphText(field, index, updatedRich))
260+
if (editContext != null && editContext.TrySetParagraphText(field, index, updatedRich))
257261
{
258262
lastStaged = text;
259263
currentRich = updatedRich;
@@ -270,7 +274,7 @@ private void WireParagraphTextEditing(LexicalEditRegionField field, IRegionEditC
270274
// seam and completes the gesture (structural: commit immediately + re-show). Built only when the
271275
// field carries available paragraph styles.
272276
private Control BuildStyleAffordance(LexicalEditRegionField field, string automationId,
273-
IRegionEditContext editContext, Action gestureCompleted, RegionParagraph paragraph, int index)
277+
IStructuredTextEditing editContext, Action gestureCompleted, RegionParagraph paragraph, int index)
274278
{
275279
if (field.AvailableParagraphStyles == null || field.AvailableParagraphStyles.Count == 0)
276280
return null;
@@ -312,7 +316,7 @@ private Control BuildStyleAffordance(LexicalEditRegionField field, string automa
312316
{
313317
styleFlyout.Hide();
314318
var styleName = string.IsNullOrEmpty(option?.Key) ? null : option.Key;
315-
if (editContext.TrySetParagraphStyle(field, index, styleName))
319+
if (editContext != null && editContext.TrySetParagraphStyle(field, index, styleName))
316320
gestureCompleted?.Invoke();
317321
};
318322
stylePicker.OptionCommitted += styleCommitted;
@@ -401,7 +405,7 @@ private Control BuildValueContentWithFontSwap(LexicalEditRegionField field, stri
401405
// clear entry plus the project's character styles, acting on the box's current selection and
402406
// staging ApplySpanNamedStyle through the paragraph-text seam. Null when no character styles.
403407
private Control BuildCharStyleAffordance(LexicalEditRegionField field, string automationId,
404-
IRegionEditContext editContext, Action gestureCompleted, TextBox box, int index,
408+
IStructuredTextEditing editContext, Action gestureCompleted, TextBox box, int index,
405409
Func<RegionRichTextValue> getRich, Action<RegionRichTextValue> setRich)
406410
{
407411
if (field.AvailableNamedStyles == null || field.AvailableNamedStyles.Count == 0)
@@ -449,7 +453,7 @@ private Control BuildCharStyleAffordance(LexicalEditRegionField field, string au
449453
var styleName = string.IsNullOrEmpty(option?.Key) ? null : option.Key;
450454
var restyled = RegionRichTextEditAlgorithms.ApplySpanNamedStyle(rich, lo, hi, styleName);
451455
if (!ReferenceEquals(restyled, rich)
452-
&& editContext.TrySetParagraphText(field, index, restyled))
456+
&& editContext != null && editContext.TrySetParagraphText(field, index, restyled))
453457
{
454458
setRich(restyled);
455459
gestureCompleted?.Invoke();
@@ -463,7 +467,7 @@ private Control BuildCharStyleAffordance(LexicalEditRegionField field, string au
463467
// writing systems (no clear entry), acting on the box's current selection and staging
464468
// RetagSpanWritingSystem through the paragraph-text seam. Null when no writing systems.
465469
private Control BuildWsRetagAffordance(LexicalEditRegionField field, string automationId,
466-
IRegionEditContext editContext, Action gestureCompleted, TextBox box, int index,
470+
IStructuredTextEditing editContext, Action gestureCompleted, TextBox box, int index,
467471
Func<RegionRichTextValue> getRich, Action<RegionRichTextValue> setRich)
468472
{
469473
if (field.AvailableWritingSystems == null || field.AvailableWritingSystems.Count == 0)
@@ -510,7 +514,7 @@ private Control BuildWsRetagAffordance(LexicalEditRegionField field, string auto
510514
new[] { new RegionTextRun(box.Text ?? string.Empty) });
511515
var retagged = RegionRichTextEditAlgorithms.RetagSpanWritingSystem(rich, lo, hi, option.Key);
512516
if (!ReferenceEquals(retagged, rich)
513-
&& editContext.TrySetParagraphText(field, index, retagged))
517+
&& editContext != null && editContext.TrySetParagraphText(field, index, retagged))
514518
{
515519
setRich(retagged);
516520
gestureCompleted?.Invoke();

Src/Common/FwAvalonia/Region/IRegionEditContext.cs

Lines changed: 0 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -57,84 +57,6 @@ public interface IRegionEditContext : IEditSession
5757
/// </summary>
5858
bool TryRemoveReferenceItem(LexicalEditRegionField field, string optionKey);
5959

60-
/// <summary>
61-
/// §19a: stages a run-aware text edit to ONE paragraph of a
62-
/// <see cref="RegionFieldKind.StructuredText"/> (StText) field, opening the session on the first
63-
/// edit. Returns false — WITHOUT opening the session — for a non-StText row, an out-of-range
64-
/// paragraph index, or an ORC/lossy (read-only) paragraph. Like the run-aware single-WS path,
65-
/// the rich payload preserves run metadata so the product <c>ITsString</c> rebuilds without
66-
/// flattening.
67-
/// </summary>
68-
bool TrySetParagraphText(LexicalEditRegionField field, int paragraphIndex, RegionRichTextValue value);
69-
70-
/// <summary>
71-
/// §19a: stages setting (or clearing, when <paramref name="styleName"/> is null/empty) the named
72-
/// paragraph style of ONE paragraph of a <see cref="RegionFieldKind.StructuredText"/> field.
73-
/// Returns false — without opening the session — for a non-StText row or an out-of-range index.
74-
/// </summary>
75-
bool TrySetParagraphStyle(LexicalEditRegionField field, int paragraphIndex, string styleName);
76-
77-
/// <summary>
78-
/// §19a: stages inserting a new empty paragraph AFTER <paramref name="afterParagraphIndex"/> in a
79-
/// <see cref="RegionFieldKind.StructuredText"/> field (a negative index inserts at the start).
80-
/// Returns false — without opening the session — for a non-StText row. The structural gesture
81-
/// commits immediately and the host re-shows (the model's paragraph list is a compose snapshot).
82-
/// </summary>
83-
bool TryInsertParagraph(LexicalEditRegionField field, int afterParagraphIndex);
84-
85-
/// <summary>
86-
/// §19a: stages deleting paragraph <paramref name="paragraphIndex"/> of a
87-
/// <see cref="RegionFieldKind.StructuredText"/> field. Returns false — without opening the
88-
/// session — for a non-StText row, an out-of-range index, or when it would delete the only
89-
/// paragraph (the StText always keeps at least one, like the legacy editor).
90-
/// </summary>
91-
bool TryDeleteParagraph(LexicalEditRegionField field, int paragraphIndex);
92-
93-
/// <summary>
94-
/// §19d: inserts a NEW picture (an <c>ICmPicture</c>) into the picture-collection field
95-
/// <paramref name="field"/> from the source image file <paramref name="sourceFile"/>, with the
96-
/// supplied <paramref name="metadata"/> (caption/description/license/creator). The structural
97-
/// gesture commits immediately and the host re-shows (the picture rows are a compose-time
98-
/// snapshot). Returns false — WITHOUT opening the session — for a non-picture field, a missing
99-
/// source file, or a field that does not accept pictures.
100-
/// </summary>
101-
bool TryInsertPicture(LexicalEditRegionField field, string sourceFile, RegionPictureMetadata metadata);
102-
103-
/// <summary>
104-
/// §19d: replaces the image FILE of the existing picture <paramref name="field"/> represents
105-
/// (a picture row carries one <c>ICmPicture</c>)
106-
/// with <paramref name="sourceFile"/>, leaving its caption/metadata intact. Returns false —
107-
/// without opening the session — for a non-picture row, an unresolvable picture, or a missing file.
108-
/// </summary>
109-
bool TryReplacePictureFile(LexicalEditRegionField field, string sourceFile);
110-
111-
/// <summary>
112-
/// §19d: deletes the picture <paramref name="field"/> represents from its owning collection.
113-
/// Returns false — without opening the session — for a non-picture row or an unresolvable picture.
114-
/// </summary>
115-
bool TryDeletePicture(LexicalEditRegionField field);
116-
117-
/// <summary>
118-
/// §19d: updates the metadata (caption/description, and license/creator when the file metadata is
119-
/// writable) of the picture <paramref name="field"/> represents. The caption/description are real
120-
/// <c>ICmPicture</c> multistring properties (always written); license/creator are applied to the
121-
/// image file's Palaso metadata only when the file is present/writable. Returns false — without
122-
/// opening the session — for a non-picture row or an unresolvable picture.
123-
/// </summary>
124-
bool TrySetPictureMetadata(LexicalEditRegionField field, RegionPictureMetadata metadata);
125-
126-
/// <summary>
127-
/// §19d (closes §19c's picture-ORC deferral): inserts a picture ORC into the rich-text VALUE of a
128-
/// text field at <paramref name="caretPosition"/> — creates the <c>ICmPicture</c> from
129-
/// <paramref name="sourceFile"/> (with <paramref name="metadata"/>) and inserts the
130-
/// <c>kodtGuidMoveableObjDisp</c> run referencing it, like legacy
131-
/// <c>FwEditingHelper.InsertPicture</c>. The gesture commits immediately and the host re-shows.
132-
/// Returns false — without opening the session — for a field whose writing system cannot be
133-
/// resolved, a missing source file, or a field that does not carry editable rich text.
134-
/// </summary>
135-
bool TryInsertPictureOrc(LexicalEditRegionField field, string ws, int caretPosition,
136-
string sourceFile, RegionPictureMetadata metadata);
137-
13860
/// <summary>
13961
/// Validates the staged state. Empty result means commit may proceed; messages are
14062
/// user-facing (validation seam, deterministic order).

0 commit comments

Comments
 (0)