diff --git a/Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs b/Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs
index 58c5b108ab..0da318c9be 100644
--- a/Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs
+++ b/Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs
@@ -108,6 +108,7 @@ public ArrangeButtons ButtonType
}
_buttonType = value;
+ SetTextDirect (GetButtonGlyph ());
ApplyOrientationAndDirection ();
SetupKeyBindings ();
}
@@ -123,19 +124,15 @@ public ArrangeButtons ButtonType
public NavigationDirection Direction { get; set; }
///
- public override string Text
- {
- get =>
- ButtonType switch
- {
- ArrangeButtons.Move => $"{Glyphs.Move}",
- ArrangeButtons.AllSize => $"{Glyphs.SizeBottomRight}",
- ArrangeButtons.LeftSize or ArrangeButtons.RightSize => $"{Glyphs.SizeHorizontal}",
- ArrangeButtons.TopSize or ArrangeButtons.BottomSize => $"{Glyphs.SizeVertical}",
- _ => base.Text
- };
- set => base.Text = value;
- }
+ private string GetButtonGlyph () =>
+ ButtonType switch
+ {
+ ArrangeButtons.Move => $"{Glyphs.Move}",
+ ArrangeButtons.AllSize => $"{Glyphs.SizeBottomRight}",
+ ArrangeButtons.LeftSize or ArrangeButtons.RightSize => $"{Glyphs.SizeHorizontal}",
+ ArrangeButtons.TopSize or ArrangeButtons.BottomSize => $"{Glyphs.SizeVertical}",
+ _ => Text
+ };
///
/// Sets and based on .
diff --git a/Terminal.Gui/ViewBase/Adornment/TitleView.cs b/Terminal.Gui/ViewBase/Adornment/TitleView.cs
index 0274ce5bb2..49c2cd5d1f 100644
--- a/Terminal.Gui/ViewBase/Adornment/TitleView.cs
+++ b/Terminal.Gui/ViewBase/Adornment/TitleView.cs
@@ -112,7 +112,12 @@ protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attri
public NavigationDirection Direction { get; set; }
///
- public override string Text { get => base.Text; set => base.Text = Title = value; }
+ protected override void OnTextChanged ()
+ {
+ Title = Text;
+
+ base.OnTextChanged ();
+ }
///
/// Binds the appropriate arrow keys to their directional based on
diff --git a/Terminal.Gui/ViewBase/View.Text.cs b/Terminal.Gui/ViewBase/View.Text.cs
index 5625c77ecd..b58552d540 100644
--- a/Terminal.Gui/ViewBase/View.Text.cs
+++ b/Terminal.Gui/ViewBase/View.Text.cs
@@ -1,14 +1,11 @@
+using System.ComponentModel;
+
namespace Terminal.Gui.ViewBase;
public partial class View // Text Property APIs
{
private string _text = string.Empty;
- ///
- /// Called when the has changed. Fires the event.
- ///
- public void OnTextChanged () => TextChanged?.Invoke (this, EventArgs.Empty);
-
///
/// Gets or sets whether trailing spaces at the end of word-wrapped lines are preserved
/// or not when is enabled.
@@ -50,9 +47,19 @@ public bool PreserveTrailingSpaces
/// If or are using ,
/// the will be adjusted to fit the text.
///
- /// When the text changes, the is fired.
+ ///
+ /// Setting to the same value as the current value is a no-op; neither
+ /// nor will be raised.
+ ///
+ ///
+ /// Before the text is changed, the CWP hook is invoked. If cancelled,
+ /// the text remains unchanged and is not raised.
+ ///
+ ///
+ /// After the text is changed, the event is raised.
+ ///
///
- public virtual string Text
+ public string Text
{
get => _text;
set
@@ -62,14 +69,107 @@ public virtual string Text
return;
}
- _text = value;
+ if (OnTextChanging (value))
+ {
+ return;
+ }
- UpdateTextFormatterText ();
- SetNeedsLayout ();
- OnTextChanged ();
+ SetTextDirect (value);
+
+ RaiseTextChanged ();
}
}
+ ///
+ /// Sets the backing field directly without raising
+ /// or events and without invoking the
+ /// or virtual methods.
+ ///
+ ///
+ ///
+ /// Use this method in derived views that maintain an internal text model (e.g., an editor
+ /// buffer) and need to keep in sync after internal edits without
+ /// re-entering the CWP flow.
+ ///
+ ///
+ /// The new text value to store.
+ protected void SetTextDirect (string value)
+ {
+ _text = value;
+ UpdateTextFormatterText ();
+ SetNeedsLayout ();
+ }
+
+ ///
+ /// Called before the changes. Invokes the event, which can
+ /// be cancelled.
+ ///
+ ///
+ ///
+ /// The base implementation raises the event. Override in derived views
+ /// to perform validation or fire control-specific pre-change events.
+ ///
+ ///
+ /// The proposed new text value.
+ /// if the text change should be cancelled; otherwise .
+ protected virtual bool OnTextChanging (string newText)
+ {
+ CancelEventArgs args = new ();
+ TextChanging?.Invoke (this, args);
+
+ return args.Cancel;
+ }
+
+ ///
+ /// Raised when the is about to change. Set to
+ /// to prevent the change.
+ ///
+ ///
+ ///
+ /// This is a signal-only notification at the level. It does not carry old or new
+ /// text values. Derived controls that need richer text-edit semantics may expose their own specific events.
+ ///
+ ///
+ public event EventHandler? TextChanging;
+
+ ///
+ /// Called after the has been changed.
+ ///
+ ///
+ ///
+ /// Override in derived views to react to text changes. The base implementation is empty.
+ /// The event is raised by the caller after this method returns.
+ ///
+ ///
+ protected virtual void OnTextChanged () { }
+
+ ///
+ /// Invokes the CWP post-change workflow for : calls
+ /// then raises .
+ ///
+ ///
+ ///
+ /// Derived views that bypass the base setter (e.g., using new) should
+ /// call this method after mutating text to participate in the CWP workflow.
+ ///
+ ///
+ protected internal void RaiseTextChanged ()
+ {
+ OnTextChanged ();
+ TextChanged?.Invoke (this, EventArgs.Empty);
+ }
+
+ ///
+ /// Raised after the has been changed.
+ ///
+ ///
+ ///
+ /// This is a signal-only notification at the level. It does not carry old or new
+ /// text values. Derived controls that need richer text-edit semantics may expose their own specific events.
+ ///
+ ///
+ public event EventHandler? TextChanged;
+
///
/// Gets or sets how the View's is aligned horizontally when drawn. Changing this property will
/// redisplay the .
@@ -92,11 +192,6 @@ public Alignment TextAlignment
}
}
- ///
- /// Text changed event, raised when the text has changed.
- ///
- public event EventHandler? TextChanged;
-
///
/// Gets or sets the direction of the View's . Changing this property will redisplay the
/// .
diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs
index 04e3d8d2b5..d010ca4d9b 100644
--- a/Terminal.Gui/Views/Button.cs
+++ b/Terminal.Gui/Views/Button.cs
@@ -171,13 +171,18 @@ private void SetMouseBindings (MouseFlags? mouseHoldRepeat)
private void Button_TitleChanged (object? sender, EventArgs e)
{
- base.Text = e.Value;
+ SetTextDirect (e.Value);
TextFormatter.HotKeySpecifier = HotKeySpecifier;
_interiorTextFormatter.HotKeySpecifier = HotKeySpecifier;
}
///
- public override string Text { get => Title; set => base.Text = Title = value; }
+ protected override void OnTextChanged ()
+ {
+ Title = Text;
+
+ base.OnTextChanged ();
+ }
///
public override Rune HotKeySpecifier
diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs
index 5fc1bb4cc2..a8551d64be 100644
--- a/Terminal.Gui/Views/CheckBox.cs
+++ b/Terminal.Gui/Views/CheckBox.cs
@@ -64,12 +64,17 @@ protected override void OnActivated (ICommandContext? commandContext)
private void Checkbox_TitleChanged (object? sender, EventArgs e)
{
- base.Text = e.Value;
+ SetTextDirect (e.Value);
TextFormatter.HotKeySpecifier = HotKeySpecifier;
}
///
- public override string Text { get => Title; set => base.Text = Title = value; }
+ protected override void OnTextChanged ()
+ {
+ Title = Text;
+
+ base.OnTextChanged ();
+ }
///
public override Rune HotKeySpecifier { get => base.HotKeySpecifier; set => TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value; }
diff --git a/Terminal.Gui/Views/Code.cs b/Terminal.Gui/Views/Code.cs
index 9bbb6b6e78..5c61f8ccab 100644
--- a/Terminal.Gui/Views/Code.cs
+++ b/Terminal.Gui/Views/Code.cs
@@ -16,19 +16,11 @@ public Code ()
}
/// Gets or sets the source text to render.
- public override string Text
+ protected override void OnTextChanged ()
{
- get => base.Text;
- set
- {
- if (base.Text == value)
- {
- return;
- }
+ UpdateStyledLines ();
- base.Text = value;
- UpdateStyledLines ();
- }
+ base.OnTextChanged ();
}
/// Gets or sets the language hint used for syntax highlighting.
diff --git a/Terminal.Gui/Views/Color/ColorPicker.cs b/Terminal.Gui/Views/Color/ColorPicker.cs
index 8be7577efa..4780c9de99 100644
--- a/Terminal.Gui/Views/Color/ColorPicker.cs
+++ b/Terminal.Gui/Views/Color/ColorPicker.cs
@@ -154,16 +154,26 @@ protected override bool OnDrawingContent (DrawContext? context)
protected virtual void OnValueChanged (ValueChangedEventArgs args) { }
///
- public override string Text
+ protected override bool OnTextChanging (string newText)
{
- get => SelectedColor.ToString ();
- set
+ // Reject text that cannot be parsed as a valid color
+ if (!_colorNameResolver.TryParseColor (newText, out _))
{
- if (_colorNameResolver.TryParseColor (value, out Color newColor))
- {
- SelectedColor = newColor;
- }
+ return true;
+ }
+
+ return base.OnTextChanging (newText);
+ }
+
+ ///
+ protected override void OnTextChanged ()
+ {
+ if (_colorNameResolver.TryParseColor (Text, out Color newColor))
+ {
+ SelectedColor = newColor;
}
+
+ base.OnTextChanged ();
}
///
@@ -289,6 +299,9 @@ private void SetSelectedColor (Color value, bool syncBars)
// Do the work
_selectedColor = value;
+ // Keep Text in sync with the new color
+ SetTextDirect (_selectedColor.ToString ());
+
// CWP: Fire ValueChanged
ValueChangedEventArgs changedArgs = new (oldValue, value);
OnValueChanged (changedArgs);
diff --git a/Terminal.Gui/Views/DatePicker.cs b/Terminal.Gui/Views/DatePicker.cs
index 23a3fdaddc..cf67b4204e 100644
--- a/Terminal.Gui/Views/DatePicker.cs
+++ b/Terminal.Gui/Views/DatePicker.cs
@@ -50,16 +50,26 @@ public CultureInfo? Culture
}
///
- public override string Text
+ protected override bool OnTextChanging (string newText)
{
- get => Value.ToString (Format);
- set
+ // Reject text that cannot be parsed as a valid DateTime
+ if (!DateTime.TryParse (newText, out _))
{
- if (DateTime.TryParse (value, out DateTime result))
- {
- Value = result;
- }
+ return true;
+ }
+
+ return base.OnTextChanging (newText);
+ }
+
+ ///
+ protected override void OnTextChanged ()
+ {
+ if (DateTime.TryParse (Text, out DateTime result))
+ {
+ Value = result;
}
+
+ base.OnTextChanged ();
}
#region IValue Implementation
@@ -93,6 +103,9 @@ public DateTime Value
_date = value;
+ // Keep Text in sync with the new date value
+ SetTextDirect (_date.ToString (Format));
+
// Propagate value to embedded editor
_dateEditor?.Value = value;
diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs
index 7615112cc7..7eb864248c 100644
--- a/Terminal.Gui/Views/Label.cs
+++ b/Terminal.Gui/Views/Label.cs
@@ -29,12 +29,17 @@ public Label ()
private void Label_TitleChanged (object? sender, EventArgs e)
{
- base.Text = e.Value;
+ SetTextDirect (e.Value);
TextFormatter.HotKeySpecifier = HotKeySpecifier;
}
///
- public override string Text { get => Title; set => base.Text = Title = value; }
+ protected override void OnTextChanged ()
+ {
+ Title = Text;
+
+ base.OnTextChanged ();
+ }
///
public override Rune HotKeySpecifier { get => base.HotKeySpecifier; set => TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value; }
diff --git a/Terminal.Gui/Views/LinearRange/LinearRangeViewBase.cs b/Terminal.Gui/Views/LinearRange/LinearRangeViewBase.cs
index 522b8eac67..44dd2c8bec 100644
--- a/Terminal.Gui/Views/LinearRange/LinearRangeViewBase.cs
+++ b/Terminal.Gui/Views/LinearRange/LinearRangeViewBase.cs
@@ -189,22 +189,21 @@ protected LinearRangeViewBase (List? options, Orientation orientation,
///
/// Setting the Text of a linear range is a shortcut to setting options. The text is a CSV string of the options.
///
- public override string Text
+ protected override void OnTextChanged ()
{
- // Return labels as a CSV string
- get => _options is null or { Count: 0 } ? string.Empty : string.Join (",", _options);
- set
+ string value = Text;
+
+ if (string.IsNullOrEmpty (value))
{
- if (string.IsNullOrEmpty (value))
- {
- Options = [];
- }
- else
- {
- IEnumerable list = value.Split (',').Select (x => x.Trim ());
- Options = list.Select (x => new LinearRangeOption { Legend = x }).ToList ();
- }
+ Options = [];
+ }
+ else
+ {
+ IEnumerable list = value.Split (',').Select (x => x.Trim ());
+ Options = list.Select (x => new LinearRangeOption { Legend = x }).ToList ();
}
+
+ base.OnTextChanged ();
}
/// Allow no selection.
@@ -370,6 +369,10 @@ public List> Options
// Drop any selected indices that are no longer valid
_setOptions.RemoveAll (i => i < 0 || i >= _options.Count);
+ // Keep Text in sync with Options
+ string csv = _options.Count == 0 ? string.Empty : string.Join (",", _options);
+ SetTextDirect (csv);
+
if (_options.Count == 0)
{
return;
diff --git a/Terminal.Gui/Views/Markdown/Markdown.cs b/Terminal.Gui/Views/Markdown/Markdown.cs
index 57a7303c2b..1f2ed440dc 100644
--- a/Terminal.Gui/Views/Markdown/Markdown.cs
+++ b/Terminal.Gui/Views/Markdown/Markdown.cs
@@ -124,7 +124,23 @@ protected override bool OnActivating (CommandEventArgs args)
/// Gets or sets the Markdown-formatted text displayed by this view.
/// The raw Markdown string. Setting this property triggers reparsing, re-layout, and a redraw.
- public override string Text { get => _markdown; set => SetMarkdown (value); }
+ protected override void OnTextChanged ()
+ {
+ SetMarkdown (Text);
+
+ base.OnTextChanged ();
+ }
+
+ ///
+ ///
+ /// does not use for rendering.
+ /// It uses its own styled-line pipeline. Clearing the formatter text prevents the base
+ /// from drawing raw markdown as plain text.
+ ///
+ protected override void UpdateTextFormatterText ()
+ {
+ TextFormatter.Text = string.Empty;
+ }
///
///
diff --git a/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs b/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs
index 1ee078382f..a1b046dfc3 100644
--- a/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs
+++ b/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs
@@ -43,15 +43,17 @@ public MarkdownCodeBlock ()
/// (```lang\ncode\n```) and extracts the language automatically. Plain text
/// (without fences) is also accepted and treated as language-less code.
///
- public override string Text
+ protected override void OnTextChanged ()
{
- get
- {
- string body = string.Join ("\n", CodeLines);
+ ParseFencedText (Text);
- return !string.IsNullOrEmpty (Language) ? $"```{Language}\n{body}\n```" : body;
- }
- set => ParseFencedText (value);
+ base.OnTextChanged ();
+ }
+
+ ///
+ protected override void UpdateTextFormatterText ()
+ {
+ TextFormatter.Text = string.Empty;
}
///
diff --git a/Terminal.Gui/Views/Markdown/MarkdownTable.cs b/Terminal.Gui/Views/Markdown/MarkdownTable.cs
index 04275e5181..0f22da723e 100644
--- a/Terminal.Gui/Views/Markdown/MarkdownTable.cs
+++ b/Terminal.Gui/Views/Markdown/MarkdownTable.cs
@@ -86,55 +86,61 @@ public MarkdownTable ()
/// The setter parses the text via and updates .
/// Invalid or empty text clears the table.
///
- public override string Text
+ protected override void OnTextChanged ()
{
- get
+ string value = Text;
+
+ if (string.IsNullOrWhiteSpace (value))
{
- if (_data.ColumnCount == 0)
- {
- return string.Empty;
- }
+ TableData = _emptyData;
+ }
+ else
+ {
+ string [] lines = value.Split ('\n', StringSplitOptions.RemoveEmptyEntries);
+ TableData? parsed = TableData.TryParse (lines);
+ TableData = parsed ?? _emptyData;
+ }
- // Reconstruct pipe-delimited table text
- List lines = [$"| {string.Join (" | ", _data.Headers)} |"];
+ base.OnTextChanged ();
+ }
- // Separator row
- var seps = new string [_data.ColumnCount];
+ ///
+ protected override void UpdateTextFormatterText ()
+ {
+ TextFormatter.Text = string.Empty;
+ }
- for (var i = 0; i < _data.ColumnCount; i++)
- {
- seps [i] = _data.ColumnAlignments [i] switch
- {
- Alignment.Center => ":---:",
- Alignment.End => "---:",
- _ => "---"
- };
- }
+ private string BuildTableText ()
+ {
+ if (_data.ColumnCount == 0)
+ {
+ return string.Empty;
+ }
- lines.Add ($"| {string.Join (" | ", seps)} |");
+ // Reconstruct pipe-delimited table text
+ List lines = [$"| {string.Join (" | ", _data.Headers)} |"];
- foreach (string [] row in _data.Rows)
- {
- lines.Add ($"| {string.Join (" | ", row)} |");
- }
+ // Separator row
+ string [] seps = new string [_data.ColumnCount];
- return string.Join ("\n", lines);
- }
- set
+ for (var i = 0; i < _data.ColumnCount; i++)
{
- // Guard: View base constructor calls Text setter before MarkdownTable() initializes fields.
-
- if (string.IsNullOrWhiteSpace (value))
- {
- TableData = _emptyData;
+ seps [i] = _data.ColumnAlignments [i] switch
+ {
+ Alignment.Center => ":---:",
+ Alignment.End => "---:",
+ _ => "---"
+ };
+ }
- return;
- }
+ lines.Add ($"| {string.Join (" | ", seps)} |");
- string [] lines = value.Split ('\n', StringSplitOptions.RemoveEmptyEntries);
- TableData? parsed = TableData.TryParse (lines);
- TableData = parsed ?? _emptyData;
+ foreach (string [] row in _data.Rows)
+ {
+ lines.Add ($"| {string.Join (" | ", row)} |");
}
+
+ return string.Join ("\n", lines);
}
///
@@ -155,6 +161,9 @@ public TableData TableData
_rowSegments [r] = ParseCellSegments (value.Rows [r], MarkdownStyleRole.Normal);
}
+ // Keep Text in sync with TableData
+ SetTextDirect (BuildTableText ());
+
// Compute initial layout using current Frame width (or a default for standalone use)
int initialWidth = Frame.Width > 0 ? Frame.Width : 80;
_lastComputedWidth = -1;
diff --git a/Terminal.Gui/Views/ProgressBar.cs b/Terminal.Gui/Views/ProgressBar.cs
index 570b269abe..63c1afa208 100644
--- a/Terminal.Gui/Views/ProgressBar.cs
+++ b/Terminal.Gui/Views/ProgressBar.cs
@@ -108,6 +108,7 @@ public ProgressBar ()
Height = Dim.Auto (DimAutoStyle.Content, 1);
CanFocus = false;
_fraction = 0;
+ SetTextDirect ("0%");
}
///
@@ -125,6 +126,7 @@ public float Fraction
{
_fraction = Math.Min (value, 1);
_isActivity = false;
+ SetTextDirect ($"{_fraction * 100:F0}%");
UpdateTerminalProgress ();
SetNeedsDraw ();
}
@@ -216,16 +218,15 @@ public ProgressBarStyle ProgressBarStyle
/// is the percentage will be
/// displayed. If is a marquee style, the text will be displayed.
///
- public override string Text
+ protected override bool OnTextChanging (string newText)
{
- get => string.IsNullOrEmpty (base.Text) ? $"{_fraction * 100:F0}%" : base.Text;
- set
+ // Only allow text to be set externally in marquee mode
+ if (ProgressBarStyle is not (ProgressBarStyle.MarqueeBlocks or ProgressBarStyle.MarqueeContinuous or ProgressBarStyle.Fire))
{
- if (ProgressBarStyle is ProgressBarStyle.MarqueeBlocks or ProgressBarStyle.MarqueeContinuous or ProgressBarStyle.Fire)
- {
- base.Text = value;
- }
+ return true;
}
+
+ return base.OnTextChanging (newText);
}
///
diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs
index 3846cc7051..e9308dcc18 100644
--- a/Terminal.Gui/Views/Shortcut.cs
+++ b/Terminal.Gui/Views/Shortcut.cs
@@ -140,6 +140,7 @@ public Shortcut (Key key, string? commandText, Action? action, string? helpText
HelpView.Id = "_helpView";
#endif
HelpView.Text = helpText ?? string.Empty;
+ SetTextDirect (helpText ?? string.Empty);
HelpView.GettingAttributeForRole += SubViewOnGettingAttributeForRole;
#if DEBUG
@@ -680,14 +681,12 @@ private void SetHelpViewDefaultLayout ()
/// Gets or sets the help text displayed in the middle of the Shortcut. Identical in function to
/// .
///
- public override string Text
+ protected override void OnTextChanged ()
{
- get => HelpView.Text;
- set
- {
- HelpView.Text = value;
- ShowHide ();
- }
+ HelpView.Text = Text;
+ ShowHide ();
+
+ base.OnTextChanged ();
}
///
@@ -699,6 +698,7 @@ public string HelpText
set
{
HelpView.Text = value;
+ SetTextDirect (value);
ShowHide ();
}
}
diff --git a/Terminal.Gui/Views/TextInput/DateEditor.cs b/Terminal.Gui/Views/TextInput/DateEditor.cs
index 4391de81c2..6e339d1122 100644
--- a/Terminal.Gui/Views/TextInput/DateEditor.cs
+++ b/Terminal.Gui/Views/TextInput/DateEditor.cs
@@ -161,7 +161,7 @@ bool IValue.TrySetValueFromString (string input)
///
/// Synchronizes the backing field when the base class
- /// property changes programmatically.
+ /// property changes programmatically.
///
protected override void OnValueChanged (ValueChangedEventArgs args) => _value = DateProvider.DateValue;
diff --git a/Terminal.Gui/Views/TextInput/TextField/TextField.Text.cs b/Terminal.Gui/Views/TextInput/TextField/TextField.Text.cs
index b6fce8850d..fa22cf78be 100644
--- a/Terminal.Gui/Views/TextInput/TextField/TextField.Text.cs
+++ b/Terminal.Gui/Views/TextInput/TextField/TextField.Text.cs
@@ -9,7 +9,7 @@ public partial class TextField
private string? _lastPastedText;
/// Raised before changes. The change can be canceled the text adjusted.
- public event EventHandler>? TextChanging;
+ public new event EventHandler>? TextChanging;
///
/// Tracks whether the text field should be considered "used", that is, that the user has moved in the entry, so
@@ -296,18 +296,6 @@ private static bool IsWithinPasteRange (PasteEditOperation operation, int propos
return proposedIndex >= pasteStart && proposedIndex < pasteEnd;
}
- /// Raises the event, enabling canceling the change or adjusting the text.
- /// The event arguments.
- /// if the event was cancelled or the text was adjusted by the event.
- public bool RaiseTextChanging (ResultEventArgs args)
- {
- // TODO: CWP: Add an OnTextChanging protected virtual method that can be overridden to handle text changing events.
-
- TextChanging?.Invoke (this, args);
-
- return args.Handled;
- }
-
private List DeleteSelectedText ()
{
SetSelectedStartSelectedLength ();
@@ -390,77 +378,120 @@ private void InsertText (Key a, bool usePreTextChangedCursorPos)
///
private List _text;
+ ///
+ /// Stashes the final text value determined during (after sanitization
+ /// and possible subscriber modification) for use in .
+ ///
+ private string? _pendingText;
+
private void SetText (List newText) => Text = StringExtensions.ToString (newText);
private void SetText (IEnumerable newText) => SetText (newText.ToList ());
- /// Sets or gets the text held by the view.
- public override string Text
+ ///
+ ///
+ ///
+ /// TextField overrides this to sanitize text (strip tabs and newlines), fire
+ /// , and raise the TextField-specific
+ /// event (which allows subscribers to modify the text).
+ ///
+ ///
+ protected override bool OnTextChanging (string newText)
{
- get => StringExtensions.ToString (_text);
- set
+ // Guard against base constructor calling before _text is initialized
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+ if (_text is null)
{
- // Guard against base constructor calling before _text is initialized
- // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
- if (_text is null)
- {
- return;
- }
+ return true;
+ }
- var oldText = StringExtensions.ToString (_text);
+ // Sanitize: single-line, no tabs
+ string sanitized = newText.Replace ("\t", "").Split ("\n") [0];
- if (oldText == value)
- {
- return;
- }
+ var oldText = StringExtensions.ToString (_text);
- string newText = value.Replace ("\t", "").Split ("\n") [0];
+ // After sanitization, check if there is an effective change
+ if (oldText == sanitized)
+ {
+ return true;
+ }
- // Raise IValue.ValueChanging
- if (RaiseValueChanging (oldText, newText))
+ // Raise IValue.ValueChanging
+ if (RaiseValueChanging (oldText, sanitized))
+ {
+ return true;
+ }
+
+ ResultEventArgs args = new (sanitized);
+ TextChanging?.Invoke (this, args);
+
+ if (args.Handled)
+ {
+ if (InsertionPoint > _text.Count)
{
- return;
+ InsertionPoint = _text.Count;
}
- ResultEventArgs args = new (newText);
- RaiseTextChanging (args);
+ return true;
+ }
- if (args.Handled)
- {
- if (InsertionPoint > _text.Count)
- {
- InsertionPoint = _text.Count;
- }
+ // Stash the (possibly subscriber-modified) result for OnTextChanged
+ _pendingText = args.Result;
- return;
- }
+ if (base.OnTextChanging (newText))
+ {
+ _pendingText = null;
- ClearAllSelection ();
+ return true;
+ }
- // Note we use NewValue here; TextChanging subscribers may have changed it
- _text = args.Result!.ToStringList ();
+ return false;
+ }
- if (!Secret && !_historyText.IsFromHistory)
- {
- _historyText.Add ([Cell.ToCellList (oldText)], new Point (InsertionPoint, 0));
+ ///
+ ///
+ ///
+ /// Syncs the internal grapheme-list editing model from the base value,
+ /// applying any sanitization or subscriber modifications stashed during .
+ ///
+ ///
+ protected override void OnTextChanged ()
+ {
+ // Use stashed text from OnTextChanging if available; otherwise sanitize base.Text
+ string finalText = _pendingText ?? Text.Replace ("\t", "").Split ("\n") [0];
+ _pendingText = null;
- _historyText.Add ([Cell.ToCells (_text)], new Point (InsertionPoint, 0), TextEditingLineStatus.Replaced);
- }
+ var oldText = StringExtensions.ToString (_text);
+
+ ClearAllSelection ();
+ _text = finalText.ToStringList ();
- OnTextChanged ();
+ // Ensure base View._text holds the sanitized/modified value
+ if (Text != finalText)
+ {
+ SetTextDirect (finalText);
+ }
- // Raise IValue.ValueChanged
- RaiseValueChanged (oldText, StringExtensions.ToString (_text));
+ if (!Secret && !_historyText.IsFromHistory)
+ {
+ _historyText.Add ([Cell.ToCellList (oldText)], new Point (InsertionPoint, 0));
- ProcessAutocomplete ();
+ _historyText.Add ([Cell.ToCells (_text)], new Point (InsertionPoint, 0), TextEditingLineStatus.Replaced);
+ }
- if (InsertionPoint > _text.Count)
- {
- InsertionPoint = Math.Max (TextModel.DisplaySize (_text, 0).size - 1, 0);
- }
+ base.OnTextChanged ();
- Adjust ();
- SetNeedsDraw ();
+ // Raise IValue.ValueChanged
+ RaiseValueChanged (oldText, StringExtensions.ToString (_text));
+
+ ProcessAutocomplete ();
+
+ if (InsertionPoint > _text.Count)
+ {
+ InsertionPoint = Math.Max (TextModel.DisplaySize (_text, 0).size - 1, 0);
}
+
+ Adjust ();
+ SetNeedsDraw ();
}
///
diff --git a/Terminal.Gui/Views/TextInput/TextValidateField.cs b/Terminal.Gui/Views/TextInput/TextValidateField.cs
index 4ea9f9f723..a3da6c8fd0 100644
--- a/Terminal.Gui/Views/TextInput/TextValidateField.cs
+++ b/Terminal.Gui/Views/TextInput/TextValidateField.cs
@@ -153,6 +153,7 @@ public ITextValidateProvider? Provider
{
_provider.TextChanged += ProviderOnTextChanged;
_lastKnownText = _provider.Text;
+ SetTextDirect (_provider.Text);
}
if (_provider!.Fixed)
@@ -203,57 +204,93 @@ protected virtual void OnValueChanged (ValueChangedEventArgs args) { }
#endregion
- /// Text
- public override string Text
+ ///
+ /// Stashes the final text value determined during (after possible
+ /// subscriber modification) for use in .
+ ///
+ private string? _pendingValidatedText;
+
+ ///
+ protected override bool OnTextChanging (string newText)
{
- get => _provider is null ? string.Empty : _provider.Text;
- set
+ if (_provider is null || SuppressValueEvents)
{
- if (_provider is null)
- {
- return;
- }
+ return base.OnTextChanging (newText);
+ }
- string oldValue = _provider.Text;
+ string oldValue = _provider.Text;
- if (oldValue == value)
- {
- return;
- }
+ if (oldValue == newText)
+ {
+ return true;
+ }
- if (!SuppressValueEvents)
- {
- ValueChangingEventArgs args = new (oldValue, value);
+ ValueChangingEventArgs args = new (oldValue, newText);
- if (OnValueChanging (args) || args.Handled)
- {
- return;
- }
+ if (OnValueChanging (args) || args.Handled)
+ {
+ return true;
+ }
- ValueChanging?.Invoke (this, args);
+ ValueChanging?.Invoke (this, args);
- if (args.Handled)
- {
- return;
- }
+ if (args.Handled)
+ {
+ return true;
+ }
- // Allow subscribers to modify the new value
- value = args.NewValue ?? string.Empty;
- }
+ // Allow subscribers to modify the new value
+ _pendingValidatedText = args.NewValue ?? string.Empty;
- _lastKnownText = value;
- _provider.Text = value;
+ return base.OnTextChanging (newText);
+ }
- if (!SuppressValueEvents)
- {
- ValueChangedEventArgs changedArgs = new (oldValue, value);
- OnValueChanged (changedArgs);
- ValueChanged?.Invoke (this, changedArgs);
- ValueChangedUntyped?.Invoke (this, new ValueChangedEventArgs