@ -362,11 +362,21 @@ namespace Avalonia.Controls
public override int GetHashCode ( ) = > Text ? . GetHashCode ( ) ? ? 0 ;
public override int GetHashCode ( ) = > Text ? . GetHashCode ( ) ? ? 0 ;
}
}
private enum TextMutationKind
{
ExternalReplacement ,
Edit ,
InternalSynchronization ,
}
private TextPresenter ? _ presenter ;
private TextPresenter ? _ presenter ;
private ScrollViewer ? _ scrollViewer ;
private ScrollViewer ? _ scrollViewer ;
private readonly TextBoxTextInputMethodClient _ imClient = new ( ) ;
private readonly TextBoxTextInputMethodClient _ imClient = new ( ) ;
private readonly UndoRedoHelper < UndoRedoState > _ undoRedoHelper ;
private readonly UndoRedoHelper < UndoRedoState > _ undoRedoHelper ;
private bool _ isUndoingRedoing ;
private bool _ isUndoingRedoing ;
private TextMutationKind _ textMutationKind ;
// Coercion runs before the new value is committed, so a snapshot taken there would capture the old text.
private bool _ needsUndoRedoSnapshotAfterTextChange ;
private bool _ canCut ;
private bool _ canCut ;
private bool _ canCopy ;
private bool _ canCopy ;
private bool _ canPaste ;
private bool _ canPaste ;
@ -641,16 +651,29 @@ namespace Avalonia.Controls
/// </remarks>
/// </remarks>
protected virtual string? CoerceText ( string? value )
protected virtual string? CoerceText ( string? value )
{
{
// Before #9490, snapshot here was done AFTER text change - this doesn't make sense
// since initial state would never be no text and you'd always have to make a text
// change before undo would be available
// The undo/redo stacks were also cleared at this point, which also doesn't make sense
// as it is still valid to want to undo a programmatic text set
// So we snapshot text now BEFORE the change so we can always revert
// Also don't need to check IsUndoEnabled here, that's done in SnapshotUndoRedo
if ( ! _ isUndoingRedoing )
if ( ! _ isUndoingRedoing )
{
{
SnapshotUndoRedo ( ) ;
switch ( _ textMutationKind )
{
case TextMutationKind . Edit :
SnapshotUndoRedo ( ) ;
if ( ! _ undoRedoHelper . CanUndo & &
! string . Equals ( Text , value , StringComparison . Ordinal ) )
{
_ needsUndoRedoSnapshotAfterTextChange = true ;
}
break ;
case TextMutationKind . InternalSynchronization :
break ;
case TextMutationKind . ExternalReplacement :
default :
ClearUndoRedo ( ) ;
_ needsUndoRedoSnapshotAfterTextChange = true ;
break ;
}
}
}
return value ;
return value ;
@ -878,9 +901,7 @@ namespace Avalonia.Controls
// from docs at
// from docs at
// https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled:
// https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled:
// "Setting UndoLimit clears the undo queue."
// "Setting UndoLimit clears the undo queue."
_ undoRedoHelper . Clear ( ) ;
ClearUndoRedo ( ) ;
_ selectedTextChangesMadeSinceLastUndoSnapshot = 0 ;
_ hasDoneSnapshotOnce = false ;
}
}
/// <summary>
/// <summary>
@ -1032,6 +1053,12 @@ namespace Avalonia.Controls
if ( change . Property = = TextProperty )
if ( change . Property = = TextProperty )
{
{
if ( _ needsUndoRedoSnapshotAfterTextChange )
{
_ needsUndoRedoSnapshotAfterTextChange = false ;
SnapshotUndoRedo ( ) ;
}
CoerceValue ( CaretIndexProperty ) ;
CoerceValue ( CaretIndexProperty ) ;
CoerceValue ( SelectionStartProperty ) ;
CoerceValue ( SelectionStartProperty ) ;
CoerceValue ( SelectionEndProperty ) ;
CoerceValue ( SelectionEndProperty ) ;
@ -1078,9 +1105,7 @@ namespace Avalonia.Controls
// "Setting this property to false clears the undo stack.
// "Setting this property to false clears the undo stack.
// Therefore, if you disable undo and then re-enable it, undo commands still do not work
// Therefore, if you disable undo and then re-enable it, undo commands still do not work
// because the undo stack was emptied when you disabled undo."
// because the undo stack was emptied when you disabled undo."
_ undoRedoHelper . Clear ( ) ;
ClearUndoRedo ( ) ;
_ selectedTextChangesMadeSinceLastUndoSnapshot = 0 ;
_ hasDoneSnapshotOnce = false ;
}
}
}
}
@ -1206,7 +1231,7 @@ namespace Avalonia.Controls
var text = StringBuilderCache . GetStringAndRelease ( textBuilder ) ;
var text = StringBuilderCache . GetStringAndRelease ( textBuilder ) ;
SetCurrentValue ( TextProperty , text ) ;
SetTextFromEdit ( text ) ;
ClearSelection ( ) ;
ClearSelection ( ) ;
@ -1611,7 +1636,7 @@ namespace Avalonia.Controls
sb . Append ( text ) ;
sb . Append ( text ) ;
sb . Remove ( start , end - start ) ;
sb . Remove ( start , end - start ) ;
SetCurrentValue ( TextProperty , StringBuilderCache . GetStringAndRelease ( sb ) ) ;
SetTextFromEdit ( StringBuilderCache . GetStringAndRelease ( sb ) ) ;
SetCurrentValue ( CaretIndexProperty , start ) ;
SetCurrentValue ( CaretIndexProperty , start ) ;
@ -1650,7 +1675,7 @@ namespace Avalonia.Controls
sb . Append ( text ) ;
sb . Append ( text ) ;
sb . Remove ( start , end - start ) ;
sb . Remove ( start , end - start ) ;
SetCurrentValue ( TextProperty , StringBuilderCache . GetStringAndRelease ( sb ) ) ;
SetTextFromEdit ( StringBuilderCache . GetStringAndRelease ( sb ) ) ;
}
}
}
}
@ -2139,7 +2164,7 @@ namespace Avalonia.Controls
/// <summary>
/// <summary>
/// Clears the text in the TextBox
/// Clears the text in the TextBox
/// </summary>
/// </summary>
public void Clear ( ) = > SetCurrentValue ( TextProperty , string . Empty ) ;
public void Clear ( ) = > SetTextFromEdit ( string . Empty ) ;
private void MoveHorizontal ( int direction , bool wholeWord , bool isSelecting , bool moveCaretPosition )
private void MoveHorizontal ( int direction , bool wholeWord , bool isSelecting , bool moveCaretPosition )
{
{
@ -2396,7 +2421,7 @@ namespace Avalonia.Controls
textBuilder . Append ( text ) ;
textBuilder . Append ( text ) ;
textBuilder . Remove ( start , end - start ) ;
textBuilder . Remove ( start , end - start ) ;
SetCurrentValue ( TextProperty , StringBuilderCache . GetStringAndRelease ( textBuilder ) ) ;
SetTextFromEdit ( StringBuilderCache . GetStringAndRelease ( textBuilder ) ) ;
_ presenter ? . MoveCaretToTextPosition ( start ) ;
_ presenter ? . MoveCaretToTextPosition ( start ) ;
@ -2434,6 +2459,27 @@ namespace Avalonia.Controls
return text . Substring ( start , end - start ) ;
return text . Substring ( start , end - start ) ;
}
}
internal void SetTextFromEdit ( string? value ) = > SetTextCore ( value , TextMutationKind . Edit ) ;
internal void SetTextFromInternalSynchronization ( string? value ) = >
SetTextCore ( value , TextMutationKind . InternalSynchronization ) ;
private void SetTextCore ( string? value , TextMutationKind mutationKind )
{
// Stays set through the synchronous TwoWay source echo so it isn't mistaken for an external replacement.
var previousMutationKind = _ textMutationKind ;
_ textMutationKind = mutationKind ;
try
{
SetCurrentValue ( TextProperty , value ) ;
}
finally
{
_ textMutationKind = previousMutationKind ;
}
}
/// <summary>
/// <summary>
/// Returns the sum of any vertical whitespace added between the <see cref="ScrollViewer"/> and <see cref="TextPresenter"/> in the control template.
/// Returns the sum of any vertical whitespace added between the <see cref="ScrollViewer"/> and <see cref="TextPresenter"/> in the control template.
/// </summary>
/// </summary>
@ -2561,6 +2607,13 @@ namespace Avalonia.Controls
}
}
}
}
private void ClearUndoRedo ( )
{
_ undoRedoHelper . Clear ( ) ;
_ selectedTextChangesMadeSinceLastUndoSnapshot = 0 ;
_ hasDoneSnapshotOnce = false ;
}
/// <summary>
/// <summary>
/// Undoes the first action in the undo stack
/// Undoes the first action in the undo stack
/// </summary>
/// </summary>