Browse Source

Merge branch 'fixes/clearTypeRendering' of https://github.com/Gillibald/Avalonia into fixes/clearTypeRendering

pull/9558/head
Benedikt Stebner 4 years ago
parent
commit
6a13d712f4
  1. 1
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  2. 8
      samples/ControlCatalog/Pages/TabControlPage.xaml
  3. 36
      samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
  4. 5
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
  5. 20
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  6. 3
      src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs
  7. 21
      src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs
  8. 11
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs
  9. 6
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  10. 29
      src/Avalonia.Base/Threading/DispatcherPriority.cs
  11. 8
      src/Avalonia.Base/Visual.cs
  12. 3
      src/Avalonia.Base/composition-schema.xml
  13. 14
      src/Avalonia.Controls/Border.cs
  14. 76
      src/Avalonia.Controls/BorderVisual.cs
  15. 6
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  16. 13
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  17. 12
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  18. 7
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  19. 10
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  20. 19
      src/Avalonia.Controls/ItemsControl.cs
  21. 17
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  22. 17
      src/Avalonia.Controls/TabControl.cs
  23. 306
      src/Avalonia.Controls/TextBox.cs
  24. 26
      src/Avalonia.Controls/Utils/UndoRedoHelper.cs
  25. 20
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  26. 170
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

1
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -32,6 +32,7 @@
</StackPanel> </StackPanel>
<ListBox Items="{Binding Items}" <ListBox Items="{Binding Items}"
Selection="{Binding Selection}" Selection="{Binding Selection}"
DisplayMemberBinding="{Binding (viewModels:ItemModel).ID, StringFormat='{}Item {0:N0}'}"
AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}" AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}"
SelectionMode="{Binding SelectionMode^}" SelectionMode="{Binding SelectionMode^}"
WrapSelection="{Binding WrapSelection}"/> WrapSelection="{Binding WrapSelection}"/>

8
samples/ControlCatalog/Pages/TabControlPage.xaml

@ -53,14 +53,8 @@
<TabControl <TabControl
Items="{Binding Tabs}" Items="{Binding Tabs}"
Margin="0 16" Margin="0 16"
HeaderDisplayMemberBinding="{Binding Header, x:DataType=viewModels:TabControlPageViewModelItem}"
TabStripPlacement="{Binding TabPlacement}"> TabStripPlacement="{Binding TabPlacement}">
<TabControl.ItemTemplate>
<DataTemplate x:DataType="viewModels:TabControlPageViewModelItem">
<TextBlock
Text="{Binding Header}">
</TextBlock>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate> <TabControl.ContentTemplate>
<DataTemplate x:DataType="viewModels:TabControlPageViewModelItem"> <DataTemplate x:DataType="viewModels:TabControlPageViewModelItem">
<StackPanel Orientation="Vertical" Spacing="8"> <StackPanel Orientation="Vertical" Spacing="8">

36
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

@ -4,6 +4,7 @@ using System.Linq;
using System.Reactive; using System.Reactive;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Selection; using Avalonia.Controls.Selection;
using ControlCatalog.Pages;
using MiniMvvm; using MiniMvvm;
namespace ControlCatalog.ViewModels namespace ControlCatalog.ViewModels
@ -20,9 +21,9 @@ namespace ControlCatalog.ViewModels
public ListBoxPageViewModel() public ListBoxPageViewModel()
{ {
Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem())); Items = new ObservableCollection<ItemModel>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
Selection = new SelectionModel<string>(); Selection = new SelectionModel<ItemModel>();
Selection.Select(1); Selection.Select(1);
_selectionMode = this.WhenAnyValue( _selectionMode = this.WhenAnyValue(
@ -58,8 +59,8 @@ namespace ControlCatalog.ViewModels
}); });
} }
public ObservableCollection<string> Items { get; } public ObservableCollection<ItemModel> Items { get; }
public SelectionModel<string> Selection { get; } public SelectionModel<ItemModel> Selection { get; }
public IObservable<SelectionMode> SelectionMode => _selectionMode; public IObservable<SelectionMode> SelectionMode => _selectionMode;
public bool Multiple public bool Multiple
@ -96,6 +97,31 @@ namespace ControlCatalog.ViewModels
public MiniCommand RemoveItemCommand { get; } public MiniCommand RemoveItemCommand { get; }
public MiniCommand SelectRandomItemCommand { get; } public MiniCommand SelectRandomItemCommand { get; }
private string GenerateItem() => $"Item {_counter++.ToString()}"; private ItemModel GenerateItem() => new ItemModel(_counter ++);
}
/// <summary>
/// An Item model for the <see cref="ListBoxPage"/>
/// </summary>
public class ItemModel
{
/// <summary>
/// Creates a new ItemModel with the given ID
/// </summary>
/// <param name="id">The ID to display</param>
public ItemModel(int id)
{
ID = id;
}
/// <summary>
/// The ID of this Item
/// </summary>
public int ID { get; }
public override string ToString()
{
return $"Item {ID}";
}
} }
} }

5
src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs

@ -128,8 +128,11 @@ namespace Avalonia.Rendering.Composition.Animations
left = kf; left = kf;
right = _keyFrames[c + 1]; right = _keyFrames[c + 1];
break;
} }
else if (c == 0)
return ExpressionVariant.Create(GetKeyFrame(ref ctx, kf));
else
break;
} }
var keyProgress = Math.Max(0, Math.Min(1, (iterationProgress - left.Key) / (right.Key - left.Key))); var keyProgress = Math.Max(0, Math.Min(1, (iterationProgress - left.Key) / (right.Key - left.Key)));

20
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -146,8 +146,18 @@ public class CompositingRenderer : IRendererWithCompositor
return result == 0 ? lhs.index.CompareTo(rhs.index) : result; return result == 0 ? lhs.index.CompareTo(rhs.index) : result;
}); });
} }
if (compositionChildren.Count == visualChildren.Count) var childVisual = v.ChildCompositionVisual;
// Check if the current visual somehow got migrated to another compositor
if (childVisual != null && childVisual.Compositor != v.CompositionVisual.Compositor)
childVisual = null;
var expectedCount = visualChildren.Count;
if (childVisual != null)
expectedCount++;
if (compositionChildren.Count == expectedCount)
{ {
bool mismatch = false; bool mismatch = false;
if (sortedChildren != null) if (sortedChildren != null)
@ -167,6 +177,9 @@ public class CompositingRenderer : IRendererWithCompositor
break; break;
} }
if (childVisual != null &&
!ReferenceEquals(compositionChildren[compositionChildren.Count - 1], childVisual))
mismatch = true;
if (!mismatch) if (!mismatch)
{ {
@ -193,6 +206,9 @@ public class CompositingRenderer : IRendererWithCompositor
if (compositionChild != null) if (compositionChild != null)
compositionChildren.Add(compositionChild); compositionChildren.Add(compositionChild);
} }
if (childVisual != null)
compositionChildren.Add(childVisual);
} }
private void UpdateCore() private void UpdateCore()

3
src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs

@ -29,4 +29,7 @@ public partial class Compositor
public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this); public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this);
public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this); public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this);
public CompositionSolidColorVisual CreateSolidColorVisual() =>
new(this, new ServerCompositionSolidColorVisual(Server));
} }

21
src/Avalonia.Base/Rendering/Composition/ElementCompositionPreview.cs

@ -1,5 +1,8 @@
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see> // Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
using System;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.Composition; namespace Avalonia.Rendering.Composition;
/// <summary> /// <summary>
@ -13,4 +16,22 @@ public static class ElementComposition
/// <param name="visual"></param> /// <param name="visual"></param>
/// <returns></returns> /// <returns></returns>
public static CompositionVisual? GetElementVisual(Visual visual) => visual.CompositionVisual; public static CompositionVisual? GetElementVisual(Visual visual) => visual.CompositionVisual;
/// <summary>
/// Sets a custom <see cref="CompositionVisual"/> as the last child of the element’s visual tree.
/// </summary>
public static void SetElementChildVisual(Visual visual, CompositionVisual? compositionVisual)
{
if (compositionVisual != null && visual.CompositionVisual != null &&
compositionVisual.Compositor != visual.CompositionVisual.Compositor)
throw new InvalidOperationException("Composition visuals belong to different compositor instances");
visual.ChildCompositionVisual = compositionVisual;
visual.GetVisualRoot()?.Renderer.RecalculateChildren(visual);
}
/// <summary>
/// Retrieves a <see cref="CompositionVisual"/> object previously set by a call to <see cref="SetElementChildVisual" />.
/// </summary>
public static CompositionVisual? GetElementChildVisual(Visual visual) => visual.ChildCompositionVisual;
} }

11
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs

@ -0,0 +1,11 @@
using Avalonia.Media.Immutable;
namespace Avalonia.Rendering.Composition.Server;
internal partial class ServerCompositionSolidColorVisual
{
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
{
canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new Rect(0, 0, Size.X, Size.Y));
}
}

6
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -48,7 +48,7 @@ namespace Avalonia.Rendering.Composition.Server
if (Opacity != 1) if (Opacity != 1)
canvas.PushOpacity(Opacity); canvas.PushOpacity(Opacity);
var boundsRect = new Rect(new Size(Size.X, Size.Y)); var boundsRect = new Rect(new Size(Size.X, Size.Y));
if(ClipToBounds) if (ClipToBounds && !HandlesClipToBounds)
canvas.PushClip(Root!.SnapToDevicePixels(boundsRect)); canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
if (Clip != null) if (Clip != null)
canvas.PushGeometryClip(Clip); canvas.PushGeometryClip(Clip);
@ -65,11 +65,13 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PopOpacityMask(); canvas.PopOpacityMask();
if (Clip != null) if (Clip != null)
canvas.PopGeometryClip(); canvas.PopGeometryClip();
if (ClipToBounds) if (ClipToBounds && !HandlesClipToBounds)
canvas.PopClip(); canvas.PopClip();
if(Opacity != 1) if(Opacity != 1)
canvas.PopOpacity(); canvas.PopOpacity();
} }
protected virtual bool HandlesClipToBounds => false;
private ReadbackData _readback0, _readback1, _readback2; private ReadbackData _readback0, _readback1, _readback2;

29
src/Avalonia.Base/Threading/DispatcherPriority.cs

@ -45,32 +45,37 @@ namespace Avalonia.Threading
/// <summary> /// <summary>
/// The job will be processed after other non-idle operations have completed. /// The job will be processed after other non-idle operations have completed.
/// </summary> /// </summary>
public static readonly DispatcherPriority Background = new(1); public static readonly DispatcherPriority Background = new(MinValue + 1);
/// <summary> /// <summary>
/// The job will be processed with the same priority as input. /// The job will be processed with the same priority as input.
/// </summary> /// </summary>
public static readonly DispatcherPriority Input = new(2); public static readonly DispatcherPriority Input = new(Background + 1);
/// <summary> /// <summary>
/// The job will be processed after layout and render but before input. /// The job will be processed after layout and render but before input.
/// </summary> /// </summary>
public static readonly DispatcherPriority Loaded = new(3); public static readonly DispatcherPriority Loaded = new(Input + 1);
/// <summary> /// <summary>
/// The job will be processed with the same priority as render. /// The job will be processed with the same priority as render.
/// </summary> /// </summary>
public static readonly DispatcherPriority Render = new(5); public static readonly DispatcherPriority Render = new(Loaded + 1);
/// <summary> /// <summary>
/// The job will be processed with the same priority as composition updates. /// The job will be processed with the same priority as composition updates.
/// </summary> /// </summary>
public static readonly DispatcherPriority Composition = new(6); public static readonly DispatcherPriority Composition = new(Render + 1);
/// <summary> /// <summary>
/// The job will be processed with the same priority as render. /// The job will be processed with before composition updates.
/// </summary>
public static readonly DispatcherPriority PreComposition = new(Composition + 1);
/// <summary>
/// The job will be processed with the same priority as layout.
/// </summary> /// </summary>
public static readonly DispatcherPriority Layout = new(7); public static readonly DispatcherPriority Layout = new(PreComposition + 1);
/// <summary> /// <summary>
/// The job will be processed with the same priority as data binding. /// The job will be processed with the same priority as data binding.
@ -80,7 +85,7 @@ namespace Avalonia.Threading
/// <summary> /// <summary>
/// The job will be processed before other asynchronous operations. /// The job will be processed before other asynchronous operations.
/// </summary> /// </summary>
public static readonly DispatcherPriority Send = new(8); public static readonly DispatcherPriority Send = new(Layout + 1);
/// <summary> /// <summary>
/// Maximum possible priority /// Maximum possible priority
@ -123,4 +128,4 @@ namespace Avalonia.Threading
/// <inheritdoc /> /// <inheritdoc />
public int CompareTo(DispatcherPriority other) => Value.CompareTo(other.Value); public int CompareTo(DispatcherPriority other) => Value.CompareTo(other.Value);
} }
} }

8
src/Avalonia.Base/Visual.cs

@ -292,6 +292,7 @@ namespace Avalonia
protected IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot); protected IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot);
internal CompositionDrawListVisual? CompositionVisual { get; private set; } internal CompositionDrawListVisual? CompositionVisual { get; private set; }
internal CompositionVisual? ChildCompositionVisual { get; set; }
public bool HasNonUniformZIndexChildren { get; private set; } public bool HasNonUniformZIndexChildren { get; private set; }
@ -452,12 +453,15 @@ namespace Avalonia
} }
} }
private protected virtual CompositionDrawListVisual CreateCompositionVisual(Compositor compositor)
=> new CompositionDrawListVisual(compositor,
new ServerCompositionDrawListVisual(compositor.Server, this), this);
internal CompositionVisual AttachToCompositor(Compositor compositor) internal CompositionVisual AttachToCompositor(Compositor compositor)
{ {
if (CompositionVisual == null || CompositionVisual.Compositor != compositor) if (CompositionVisual == null || CompositionVisual.Compositor != compositor)
{ {
CompositionVisual = new CompositionDrawListVisual(compositor, CompositionVisual = CreateCompositionVisual(compositor);
new ServerCompositionDrawListVisual(compositor.Server, this), this);
} }
return CompositionVisual; return CompositionVisual;

3
src/Avalonia.Base/composition-schema.xml

@ -27,6 +27,9 @@
<Property Name="OpacityMaskBrush" Type="Avalonia.Media.IBrush?" Internal="true" /> <Property Name="OpacityMaskBrush" Type="Avalonia.Media.IBrush?" Internal="true" />
</Object> </Object>
<Object Name="CompositionContainerVisual" Inherits="CompositionVisual"/> <Object Name="CompositionContainerVisual" Inherits="CompositionVisual"/>
<Object Name="CompositionSolidColorVisual" Inherits="CompositionContainerVisual">
<Property Name="Color" Type="Avalonia.Media.Color" Animated="true" />
</Object>
<List Name="CompositionVisualCollection" ItemType="CompositionVisual" CustomCtor="true"/> <List Name="CompositionVisualCollection" ItemType="CompositionVisual" CustomCtor="true"/>
<Object Name="CompositionTarget" CustomServerCtor="true"> <Object Name="CompositionTarget" CustomServerCtor="true">
<Property Name="Root" Type="CompositionVisual?"/> <Property Name="Root" Type="CompositionVisual?"/>

14
src/Avalonia.Controls/Border.cs

@ -4,6 +4,7 @@ using Avalonia.Controls.Shapes;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Rendering.Composition;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -73,6 +74,7 @@ namespace Avalonia.Controls
private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper(); private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
private Thickness? _layoutThickness; private Thickness? _layoutThickness;
private double _scale; private double _scale;
private CompositionBorderVisual? _borderVisual;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="Border"/> class. /// Initializes static members of the <see cref="Border"/> class.
@ -101,6 +103,10 @@ namespace Avalonia.Controls
case nameof(BorderThickness): case nameof(BorderThickness):
_layoutThickness = null; _layoutThickness = null;
break; break;
case nameof(CornerRadius):
if (_borderVisual != null)
_borderVisual.CornerRadius = CornerRadius;
break;
} }
} }
@ -245,6 +251,14 @@ namespace Avalonia.Controls
return LayoutHelper.ArrangeChild(Child, finalSize, Padding, BorderThickness); return LayoutHelper.ArrangeChild(Child, finalSize, Padding, BorderThickness);
} }
private protected override CompositionDrawListVisual CreateCompositionVisual(Compositor compositor)
{
return _borderVisual = new CompositionBorderVisual(compositor, this)
{
CornerRadius = CornerRadius
};
}
public CornerRadius ClipToBoundsRadius => CornerRadius; public CornerRadius ClipToBoundsRadius => CornerRadius;
} }
} }

76
src/Avalonia.Controls/BorderVisual.cs

@ -0,0 +1,76 @@
using System;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
using Avalonia.Rendering.SceneGraph;
namespace Avalonia.Controls;
class CompositionBorderVisual : CompositionDrawListVisual
{
private CornerRadius _cornerRadius;
private bool _cornerRadiusChanged;
public CompositionBorderVisual(Compositor compositor, Visual visual) : base(compositor,
new ServerBorderVisual(compositor.Server, visual), visual)
{
}
public CornerRadius CornerRadius
{
get => _cornerRadius;
set
{
if (_cornerRadius != value)
{
_cornerRadiusChanged = true;
_cornerRadius = value;
RegisterForSerialization();
}
}
}
private protected override void SerializeChangesCore(BatchStreamWriter writer)
{
base.SerializeChangesCore(writer);
writer.Write(_cornerRadiusChanged);
if (_cornerRadiusChanged)
writer.Write(_cornerRadius);
}
class ServerBorderVisual : ServerCompositionDrawListVisual
{
private CornerRadius _cornerRadius;
public ServerBorderVisual(ServerCompositor compositor, Visual v) : base(compositor, v)
{
}
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
{
if (ClipToBounds)
{
var clipRect = Root!.SnapToDevicePixels(new Rect(new Size(Size.X, Size.Y)));
if (_cornerRadius.IsEmpty)
canvas.PushClip(clipRect);
else
canvas.PushClip(new RoundedRect(clipRect, _cornerRadius));
}
base.RenderCore(canvas, currentTransformedClip);
if(ClipToBounds)
canvas.PopClip();
}
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt)
{
base.DeserializeChangesCore(reader, commitedAt);
if (reader.Read<bool>())
_cornerRadius = reader.Read<CornerRadius>();
}
protected override bool HandlesClipToBounds => true;
}
}

6
src/Avalonia.Controls/Generators/IItemContainerGenerator.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Styling; using Avalonia.Styling;
namespace Avalonia.Controls.Generators namespace Avalonia.Controls.Generators
@ -24,6 +25,11 @@ namespace Avalonia.Controls.Generators
/// Gets or sets the data template used to display the items in the control. /// Gets or sets the data template used to display the items in the control.
/// </summary> /// </summary>
IDataTemplate? ItemTemplate { get; set; } IDataTemplate? ItemTemplate { get; set; }
/// <summary>
/// Gets or sets the binding to use to bind to the member of an item used for displaying
/// </summary>
IBinding? DisplayMemberBinding { get; set; }
/// <summary> /// <summary>
/// Gets the ContainerType, or null if its an untyped ContainerGenerator. /// Gets the ContainerType, or null if its an untyped ContainerGenerator.

13
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@ -45,6 +45,9 @@ namespace Avalonia.Controls.Generators
/// Gets or sets the data template used to display the items in the control. /// Gets or sets the data template used to display the items in the control.
/// </summary> /// </summary>
public IDataTemplate? ItemTemplate { get; set; } public IDataTemplate? ItemTemplate { get; set; }
/// <inheritdoc />
public IBinding? DisplayMemberBinding { get; set; }
/// <summary> /// <summary>
/// Gets the owner control. /// Gets the owner control.
@ -189,7 +192,15 @@ namespace Avalonia.Controls.Generators
if (result == null) if (result == null)
{ {
result = new ContentPresenter(); result = new ContentPresenter();
result.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Style); if (DisplayMemberBinding is not null)
{
result.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
result.Bind(ContentPresenter.ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
result.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Style);
}
if (ItemTemplate != null) if (ItemTemplate != null)
{ {

12
src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs

@ -53,8 +53,16 @@ namespace Avalonia.Controls.Generators
container.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style); container.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style);
} }
container.SetValue(ContentProperty, item, BindingPriority.Style); if (DisplayMemberBinding is not null)
{
container.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
container.Bind(ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
container.SetValue(ContentProperty, item, BindingPriority.Style);
}
if (!(item is IControl)) if (!(item is IControl))
{ {
container.DataContext = item; container.DataContext = item;

7
src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -33,6 +34,12 @@ namespace Avalonia.Controls.Generators
TabControl.ItemTemplateProperty)); TabControl.ItemTemplateProperty));
} }
if (Owner.HeaderDisplayMemberBinding is not null)
{
tabItem.Bind(HeaderedContentControl.HeaderProperty, Owner.HeaderDisplayMemberBinding,
BindingPriority.Style);
}
if (tabItem.Header == null) if (tabItem.Header == null)
{ {
if (item is IHeadered headered) if (item is IHeadered headered)

10
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@ -76,7 +76,15 @@ namespace Avalonia.Controls.Generators
result.SetValue(Control.ThemeProperty, ItemContainerTheme, BindingPriority.Style); result.SetValue(Control.ThemeProperty, ItemContainerTheme, BindingPriority.Style);
} }
result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style); if (DisplayMemberBinding is not null)
{
result.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
result.Bind(ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style);
}
var itemsSelector = template.ItemsSelector(item); var itemsSelector = template.ItemsSelector(item);

19
src/Avalonia.Controls/ItemsControl.cs

@ -11,6 +11,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -61,6 +62,23 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty = public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate)); AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate));
/// <summary>
/// Defines the <see cref="DisplayMemberBinding" /> property
/// </summary>
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding));
/// <summary>
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
/// </summary>
[AssignBinding]
public IBinding? DisplayMemberBinding
{
get { return GetValue(DisplayMemberBindingProperty); }
set { SetValue(DisplayMemberBindingProperty, value); }
}
private IEnumerable? _items = new AvaloniaList<object>(); private IEnumerable? _items = new AvaloniaList<object>();
private int _itemCount; private int _itemCount;
private IItemContainerGenerator? _itemContainerGenerator; private IItemContainerGenerator? _itemContainerGenerator;
@ -97,6 +115,7 @@ namespace Avalonia.Controls
_itemContainerGenerator.ItemContainerTheme = ItemContainerTheme; _itemContainerGenerator.ItemContainerTheme = ItemContainerTheme;
_itemContainerGenerator.ItemTemplate = ItemTemplate; _itemContainerGenerator.ItemTemplate = ItemTemplate;
_itemContainerGenerator.DisplayMemberBinding = DisplayMemberBinding;
_itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e);
_itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
_itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e); _itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e);

17
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@ -5,6 +5,7 @@ using Avalonia.Collections;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Styling; using Avalonia.Styling;
@ -33,6 +34,12 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty = public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
ItemsControl.ItemTemplateProperty.AddOwner<ItemsPresenterBase>(); ItemsControl.ItemTemplateProperty.AddOwner<ItemsPresenterBase>();
/// <summary>
/// Defines the <see cref="DisplayMemberBinding" /> property
/// </summary>
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
ItemsControl.DisplayMemberBindingProperty.AddOwner<ItemsPresenterBase>();
private IEnumerable? _items; private IEnumerable? _items;
private IDisposable? _itemsSubscription; private IDisposable? _itemsSubscription;
private bool _createdPanel; private bool _createdPanel;
@ -120,6 +127,15 @@ namespace Avalonia.Controls.Presenters
set { SetValue(ItemTemplateProperty, value); } set { SetValue(ItemTemplateProperty, value); }
} }
/// <summary>
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
/// </summary>
public IBinding? DisplayMemberBinding
{
get { return GetValue(DisplayMemberBindingProperty); }
set { SetValue(DisplayMemberBindingProperty, value); }
}
/// <summary> /// <summary>
/// Gets the panel used to display the items. /// Gets the panel used to display the items.
/// </summary> /// </summary>
@ -177,6 +193,7 @@ namespace Avalonia.Controls.Presenters
{ {
result = new ItemContainerGenerator(this); result = new ItemContainerGenerator(this);
result.ItemTemplate = ItemTemplate; result.ItemTemplate = ItemTemplate;
result.DisplayMemberBinding = DisplayMemberBinding;
} }
result.Materialized += ContainerActionHandler; result.Materialized += ContainerActionHandler;

17
src/Avalonia.Controls/TabControl.cs

@ -12,6 +12,7 @@ using Avalonia.LogicalTree;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Automation; using Avalonia.Automation;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Data;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -57,6 +58,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IDataTemplate?> SelectedContentTemplateProperty = public static readonly StyledProperty<IDataTemplate?> SelectedContentTemplateProperty =
AvaloniaProperty.Register<TabControl, IDataTemplate?>(nameof(SelectedContentTemplate)); AvaloniaProperty.Register<TabControl, IDataTemplate?>(nameof(SelectedContentTemplate));
/// <summary>
/// Defines the <see cref="HeaderDisplayMemberBinding" /> property
/// </summary>
public static readonly StyledProperty<IBinding?> HeaderDisplayMemberBindingProperty =
AvaloniaProperty.Register<HeaderedItemsControl, IBinding?>(nameof(HeaderDisplayMemberBinding));
/// <summary> /// <summary>
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property. /// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
/// </summary> /// </summary>
@ -134,6 +141,16 @@ namespace Avalonia.Controls
get { return GetValue(SelectedContentTemplateProperty); } get { return GetValue(SelectedContentTemplateProperty); }
internal set { SetValue(SelectedContentTemplateProperty, value); } internal set { SetValue(SelectedContentTemplateProperty, value); }
} }
/// <summary>
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each tab-items header.
/// </summary>
[AssignBinding]
public IBinding? HeaderDisplayMemberBinding
{
get { return GetValue(HeaderDisplayMemberBindingProperty); }
set { SetValue(HeaderDisplayMemberBindingProperty, value); }
}
internal ItemsPresenter? ItemsPresenterPart { get; private set; } internal ItemsPresenter? ItemsPresenterPart { get; private set; }

306
src/Avalonia.Controls/TextBox.cs

@ -17,7 +17,6 @@ using Avalonia.Controls.Metadata;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using System.Diagnostics;
using Avalonia.Threading; using Avalonia.Threading;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -29,60 +28,108 @@ namespace Avalonia.Controls
[PseudoClasses(":empty")] [PseudoClasses(":empty")]
public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost
{ {
/// <summary>
/// Gets a platform-specific <see cref="KeyGesture"/> for the Cut action
/// </summary>
public static KeyGesture? CutGesture { get; } = AvaloniaLocator.Current public static KeyGesture? CutGesture { get; } = AvaloniaLocator.Current
.GetService<PlatformHotkeyConfiguration>()?.Cut.FirstOrDefault(); .GetService<PlatformHotkeyConfiguration>()?.Cut.FirstOrDefault();
/// <summary>
/// Gets a platform-specific <see cref="KeyGesture"/> for the Copy action
/// </summary>
public static KeyGesture? CopyGesture { get; } = AvaloniaLocator.Current public static KeyGesture? CopyGesture { get; } = AvaloniaLocator.Current
.GetService<PlatformHotkeyConfiguration>()?.Copy.FirstOrDefault(); .GetService<PlatformHotkeyConfiguration>()?.Copy.FirstOrDefault();
/// <summary>
/// Gets a platform-specific <see cref="KeyGesture"/> for the Paste action
/// </summary>
public static KeyGesture? PasteGesture { get; } = AvaloniaLocator.Current public static KeyGesture? PasteGesture { get; } = AvaloniaLocator.Current
.GetService<PlatformHotkeyConfiguration>()?.Paste.FirstOrDefault(); .GetService<PlatformHotkeyConfiguration>()?.Paste.FirstOrDefault();
/// <summary>
/// Defines the <see cref="AcceptsReturn"/> property
/// </summary>
public static readonly StyledProperty<bool> AcceptsReturnProperty = public static readonly StyledProperty<bool> AcceptsReturnProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(AcceptsReturn)); AvaloniaProperty.Register<TextBox, bool>(nameof(AcceptsReturn));
/// <summary>
/// Defines the <see cref="AcceptsTab"/> property
/// </summary>
public static readonly StyledProperty<bool> AcceptsTabProperty = public static readonly StyledProperty<bool> AcceptsTabProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(AcceptsTab)); AvaloniaProperty.Register<TextBox, bool>(nameof(AcceptsTab));
/// <summary>
/// Defines the <see cref="CaretIndex"/> property
/// </summary>
public static readonly DirectProperty<TextBox, int> CaretIndexProperty = public static readonly DirectProperty<TextBox, int> CaretIndexProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>( AvaloniaProperty.RegisterDirect<TextBox, int>(
nameof(CaretIndex), nameof(CaretIndex),
o => o.CaretIndex, o => o.CaretIndex,
(o, v) => o.CaretIndex = v); (o, v) => o.CaretIndex = v);
/// <summary>
/// Defines the <see cref="IsReadOnly"/> property
/// </summary>
public static readonly StyledProperty<bool> IsReadOnlyProperty = public static readonly StyledProperty<bool> IsReadOnlyProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(IsReadOnly)); AvaloniaProperty.Register<TextBox, bool>(nameof(IsReadOnly));
/// <summary>
/// Defines the <see cref="PasswordChar"/> property
/// </summary>
public static readonly StyledProperty<char> PasswordCharProperty = public static readonly StyledProperty<char> PasswordCharProperty =
AvaloniaProperty.Register<TextBox, char>(nameof(PasswordChar)); AvaloniaProperty.Register<TextBox, char>(nameof(PasswordChar));
/// <summary>
/// Defines the <see cref="SelectionBrush"/> property
/// </summary>
public static readonly StyledProperty<IBrush?> SelectionBrushProperty = public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
AvaloniaProperty.Register<TextBox, IBrush?>(nameof(SelectionBrush)); AvaloniaProperty.Register<TextBox, IBrush?>(nameof(SelectionBrush));
/// <summary>
/// Defines the <see cref="SelectionForegroundBrush"/> property
/// </summary>
public static readonly StyledProperty<IBrush?> SelectionForegroundBrushProperty = public static readonly StyledProperty<IBrush?> SelectionForegroundBrushProperty =
AvaloniaProperty.Register<TextBox, IBrush?>(nameof(SelectionForegroundBrush)); AvaloniaProperty.Register<TextBox, IBrush?>(nameof(SelectionForegroundBrush));
/// <summary>
/// Defines the <see cref="CaretBrush"/> property
/// </summary>
public static readonly StyledProperty<IBrush?> CaretBrushProperty = public static readonly StyledProperty<IBrush?> CaretBrushProperty =
AvaloniaProperty.Register<TextBox, IBrush?>(nameof(CaretBrush)); AvaloniaProperty.Register<TextBox, IBrush?>(nameof(CaretBrush));
/// <summary>
/// Defines the <see cref="SelectionStart"/> property
/// </summary>
public static readonly DirectProperty<TextBox, int> SelectionStartProperty = public static readonly DirectProperty<TextBox, int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>( AvaloniaProperty.RegisterDirect<TextBox, int>(
nameof(SelectionStart), nameof(SelectionStart),
o => o.SelectionStart, o => o.SelectionStart,
(o, v) => o.SelectionStart = v); (o, v) => o.SelectionStart = v);
/// <summary>
/// Defines the <see cref="SelectionEnd"/> property
/// </summary>
public static readonly DirectProperty<TextBox, int> SelectionEndProperty = public static readonly DirectProperty<TextBox, int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>( AvaloniaProperty.RegisterDirect<TextBox, int>(
nameof(SelectionEnd), nameof(SelectionEnd),
o => o.SelectionEnd, o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v); (o, v) => o.SelectionEnd = v);
/// <summary>
/// Defines the <see cref="MaxLength"/> property
/// </summary>
public static readonly StyledProperty<int> MaxLengthProperty = public static readonly StyledProperty<int> MaxLengthProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLength), defaultValue: 0); AvaloniaProperty.Register<TextBox, int>(nameof(MaxLength), defaultValue: 0);
/// <summary>
/// Defines the <see cref="MaxLines"/> property
/// </summary>
public static readonly StyledProperty<int> MaxLinesProperty = public static readonly StyledProperty<int> MaxLinesProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLines), defaultValue: 0); AvaloniaProperty.Register<TextBox, int>(nameof(MaxLines), defaultValue: 0);
/// <summary>
/// Defines the <see cref="Text"/> property
/// </summary>
public static readonly DirectProperty<TextBox, string?> TextProperty = public static readonly DirectProperty<TextBox, string?> TextProperty =
TextBlock.TextProperty.AddOwnerWithDataValidation<TextBox>( TextBlock.TextProperty.AddOwnerWithDataValidation<TextBox>(
o => o.Text, o => o.Text,
@ -90,6 +137,9 @@ namespace Avalonia.Controls
defaultBindingMode: BindingMode.TwoWay, defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true); enableDataValidation: true);
/// <summary>
/// Defines the <see cref="TextAlignment"/> property
/// </summary>
public static readonly StyledProperty<TextAlignment> TextAlignmentProperty = public static readonly StyledProperty<TextAlignment> TextAlignmentProperty =
TextBlock.TextAlignmentProperty.AddOwner<TextBox>(); TextBlock.TextAlignmentProperty.AddOwner<TextBox>();
@ -120,45 +170,78 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> LetterSpacingProperty = public static readonly StyledProperty<double> LetterSpacingProperty =
TextBlock.LetterSpacingProperty.AddOwner<TextBox>(); TextBlock.LetterSpacingProperty.AddOwner<TextBox>();
/// <summary>
/// Defines the <see cref="Watermark"/> property
/// </summary>
public static readonly StyledProperty<string?> WatermarkProperty = public static readonly StyledProperty<string?> WatermarkProperty =
AvaloniaProperty.Register<TextBox, string?>(nameof(Watermark)); AvaloniaProperty.Register<TextBox, string?>(nameof(Watermark));
/// <summary>
/// Defines the <see cref="UseFloatingWatermark"/> property
/// </summary>
public static readonly StyledProperty<bool> UseFloatingWatermarkProperty = public static readonly StyledProperty<bool> UseFloatingWatermarkProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(UseFloatingWatermark)); AvaloniaProperty.Register<TextBox, bool>(nameof(UseFloatingWatermark));
/// <summary>
/// Defines the <see cref="NewLine"/> property
/// </summary>
public static readonly DirectProperty<TextBox, string> NewLineProperty = public static readonly DirectProperty<TextBox, string> NewLineProperty =
AvaloniaProperty.RegisterDirect<TextBox, string>(nameof(NewLine), AvaloniaProperty.RegisterDirect<TextBox, string>(nameof(NewLine),
textbox => textbox.NewLine, (textbox, newline) => textbox.NewLine = newline); textbox => textbox.NewLine, (textbox, newline) => textbox.NewLine = newline);
/// <summary>
/// Defines the <see cref="InnerLeftContent"/> property
/// </summary>
public static readonly StyledProperty<object> InnerLeftContentProperty = public static readonly StyledProperty<object> InnerLeftContentProperty =
AvaloniaProperty.Register<TextBox, object>(nameof(InnerLeftContent)); AvaloniaProperty.Register<TextBox, object>(nameof(InnerLeftContent));
/// <summary>
/// Defines the <see cref="InnerRightContent"/> property
/// </summary>
public static readonly StyledProperty<object> InnerRightContentProperty = public static readonly StyledProperty<object> InnerRightContentProperty =
AvaloniaProperty.Register<TextBox, object>(nameof(InnerRightContent)); AvaloniaProperty.Register<TextBox, object>(nameof(InnerRightContent));
/// <summary>
/// Defines the <see cref="RevealPassword"/> property
/// </summary>
public static readonly StyledProperty<bool> RevealPasswordProperty = public static readonly StyledProperty<bool> RevealPasswordProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(RevealPassword)); AvaloniaProperty.Register<TextBox, bool>(nameof(RevealPassword));
/// <summary>
/// Defines the <see cref="CanCut"/> property
/// </summary>
public static readonly DirectProperty<TextBox, bool> CanCutProperty = public static readonly DirectProperty<TextBox, bool> CanCutProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>( AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCut), nameof(CanCut),
o => o.CanCut); o => o.CanCut);
/// <summary>
/// Defines the <see cref="CanCopy"/> property
/// </summary>
public static readonly DirectProperty<TextBox, bool> CanCopyProperty = public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>( AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCopy), nameof(CanCopy),
o => o.CanCopy); o => o.CanCopy);
/// <summary>
/// Defines the <see cref="CanPaste"/> property
/// </summary>
public static readonly DirectProperty<TextBox, bool> CanPasteProperty = public static readonly DirectProperty<TextBox, bool> CanPasteProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>( AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanPaste), nameof(CanPaste),
o => o.CanPaste); o => o.CanPaste);
/// <summary>
/// Defines the <see cref="IsUndoEnabled"/> property
/// </summary>
public static readonly StyledProperty<bool> IsUndoEnabledProperty = public static readonly StyledProperty<bool> IsUndoEnabledProperty =
AvaloniaProperty.Register<TextBox, bool>( AvaloniaProperty.Register<TextBox, bool>(
nameof(IsUndoEnabled), nameof(IsUndoEnabled),
defaultValue: true); defaultValue: true);
/// <summary>
/// Defines the <see cref="UndoLimit"/> property
/// </summary>
public static readonly DirectProperty<TextBox, int> UndoLimitProperty = public static readonly DirectProperty<TextBox, int> UndoLimitProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>( AvaloniaProperty.RegisterDirect<TextBox, int>(
nameof(UndoLimit), nameof(UndoLimit),
@ -166,6 +249,18 @@ namespace Avalonia.Controls
(o, v) => o.UndoLimit = v, (o, v) => o.UndoLimit = v,
unsetValue: -1); unsetValue: -1);
/// <summary>
/// Defines the <see cref="CanUndo"/> property
/// </summary>
public static readonly DirectProperty<TextBox, bool> CanUndoProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(nameof(CanUndo), x => x.CanUndo);
/// <summary>
/// Defines the <see cref="CanRedo"/> property
/// </summary>
public static readonly DirectProperty<TextBox, bool> CanRedoProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(nameof(CanRedo), x => x.CanRedo);
/// <summary> /// <summary>
/// Defines the <see cref="CopyingToClipboard"/> event. /// Defines the <see cref="CopyingToClipboard"/> event.
/// </summary> /// </summary>
@ -201,9 +296,13 @@ namespace Avalonia.Controls
RoutedEvent.Register<TextBox, TextChangingEventArgs>( RoutedEvent.Register<TextBox, TextChangingEventArgs>(
nameof(TextChanging), RoutingStrategies.Bubble); nameof(TextChanging), RoutingStrategies.Bubble);
/// <summary>
/// Stores the state information for available actions in the UndoRedoHelper
/// </summary>
readonly struct UndoRedoState : IEquatable<UndoRedoState> readonly struct UndoRedoState : IEquatable<UndoRedoState>
{ {
public string? Text { get; } public string? Text { get; }
public int CaretPosition { get; } public int CaretPosition { get; }
public UndoRedoState(string? text, int caretPosition) public UndoRedoState(string? text, int caretPosition)
@ -232,6 +331,8 @@ namespace Avalonia.Controls
private bool _canPaste; private bool _canPaste;
private string _newLine = Environment.NewLine; private string _newLine = Environment.NewLine;
private static readonly string[] invalidCharacters = new String[1] { "\u007f" }; private static readonly string[] invalidCharacters = new String[1] { "\u007f" };
private bool _canUndo;
private bool _canRedo;
private int _wordSelectionStart = -1; private int _wordSelectionStart = -1;
private int _selectedTextChangesMadeSinceLastUndoSnapshot; private int _selectedTextChangesMadeSinceLastUndoSnapshot;
@ -268,24 +369,34 @@ namespace Avalonia.Controls
ScrollViewer.HorizontalScrollBarVisibilityProperty, ScrollViewer.HorizontalScrollBarVisibilityProperty,
horizontalScrollBarVisibility, horizontalScrollBarVisibility,
BindingPriority.Style); BindingPriority.Style);
_undoRedoHelper = new UndoRedoHelper<UndoRedoState>(this); _undoRedoHelper = new UndoRedoHelper<UndoRedoState>(this);
_selectedTextChangesMadeSinceLastUndoSnapshot = 0; _selectedTextChangesMadeSinceLastUndoSnapshot = 0;
_hasDoneSnapshotOnce = false; _hasDoneSnapshotOnce = false;
UpdatePseudoclasses(); UpdatePseudoclasses();
} }
/// <summary>
/// Gets or sets a value that determines whether the TextBox allows and displays newline or return characters
/// </summary>
public bool AcceptsReturn public bool AcceptsReturn
{ {
get => GetValue(AcceptsReturnProperty); get => GetValue(AcceptsReturnProperty);
set => SetValue(AcceptsReturnProperty, value); set => SetValue(AcceptsReturnProperty, value);
} }
/// <summary>
/// Gets or sets a value that determins whether the TextBox allows and displays tabs
/// </summary>
public bool AcceptsTab public bool AcceptsTab
{ {
get => GetValue(AcceptsTabProperty); get => GetValue(AcceptsTabProperty);
set => SetValue(AcceptsTabProperty, value); set => SetValue(AcceptsTabProperty, value);
} }
/// <summary>
/// Gets or sets the index of the text caret
/// </summary>
public int CaretIndex public int CaretIndex
{ {
get => _caretIndex; get => _caretIndex;
@ -302,36 +413,54 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Gets or sets a value whether this TextBox is read-only
/// </summary>
public bool IsReadOnly public bool IsReadOnly
{ {
get => GetValue(IsReadOnlyProperty); get => GetValue(IsReadOnlyProperty);
set => SetValue(IsReadOnlyProperty, value); set => SetValue(IsReadOnlyProperty, value);
} }
/// <summary>
/// Gets or sets the <see cref="char"/> that should be used for password masking
/// </summary>
public char PasswordChar public char PasswordChar
{ {
get => GetValue(PasswordCharProperty); get => GetValue(PasswordCharProperty);
set => SetValue(PasswordCharProperty, value); set => SetValue(PasswordCharProperty, value);
} }
/// <summary>
/// Gets or sets a brush that is used to highlight selected text
/// </summary>
public IBrush? SelectionBrush public IBrush? SelectionBrush
{ {
get => GetValue(SelectionBrushProperty); get => GetValue(SelectionBrushProperty);
set => SetValue(SelectionBrushProperty, value); set => SetValue(SelectionBrushProperty, value);
} }
/// <summary>
/// Gets or sets a brush that is used for the foreground of selected text
/// </summary>
public IBrush? SelectionForegroundBrush public IBrush? SelectionForegroundBrush
{ {
get => GetValue(SelectionForegroundBrushProperty); get => GetValue(SelectionForegroundBrushProperty);
set => SetValue(SelectionForegroundBrushProperty, value); set => SetValue(SelectionForegroundBrushProperty, value);
} }
/// <summary>
/// Gets or sets a brush that is used for the text caret
/// </summary>
public IBrush? CaretBrush public IBrush? CaretBrush
{ {
get => GetValue(CaretBrushProperty); get => GetValue(CaretBrushProperty);
set => SetValue(CaretBrushProperty, value); set => SetValue(CaretBrushProperty, value);
} }
/// <summary>
/// Gets or sets the starting position of the text selected in the TextBox
/// </summary>
public int SelectionStart public int SelectionStart
{ {
get => _selectionStart; get => _selectionStart;
@ -352,6 +481,13 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Gets or sets the end position of the text selected in the TextBox
/// </summary>
/// <remarks>
/// When the SelectionEnd is equal to <see cref="SelectionStart"/>, there is no
/// selected text and it marks the caret position
/// </remarks>
public int SelectionEnd public int SelectionEnd
{ {
get => _selectionEnd; get => _selectionEnd;
@ -371,19 +507,28 @@ namespace Avalonia.Controls
} }
} }
} }
/// <summary>
/// Gets or sets the maximum character length of the TextBox
/// </summary>
public int MaxLength public int MaxLength
{ {
get => GetValue(MaxLengthProperty); get => GetValue(MaxLengthProperty);
set => SetValue(MaxLengthProperty, value); set => SetValue(MaxLengthProperty, value);
} }
/// <summary>
/// Gets or sets the maximum number of lines the TextBox can contain
/// </summary>
public int MaxLines public int MaxLines
{ {
get => GetValue(MaxLinesProperty); get => GetValue(MaxLinesProperty);
set => SetValue(MaxLinesProperty, value); set => SetValue(MaxLinesProperty, value);
} }
/// <summary>
/// Gets or sets the spacing between characters
/// </summary>
public double LetterSpacing public double LetterSpacing
{ {
get => GetValue(LetterSpacingProperty); get => GetValue(LetterSpacingProperty);
@ -399,6 +544,9 @@ namespace Avalonia.Controls
set => SetValue(LineHeightProperty, value); set => SetValue(LineHeightProperty, value);
} }
/// <summary>
/// Gets or sets the Text content of the TextBox
/// </summary>
[Content] [Content]
public string? Text public string? Text
{ {
@ -413,14 +561,20 @@ namespace Avalonia.Controls
SelectionStart = CoerceCaretIndex(selectionStart, value); SelectionStart = CoerceCaretIndex(selectionStart, value);
SelectionEnd = CoerceCaretIndex(selectionEnd, value); SelectionEnd = CoerceCaretIndex(selectionEnd, value);
var textChanged = SetAndRaise(TextProperty, ref _text, value); // Before #9490, snapshot here was done AFTER text change - this doesn't make sense
// since intial state would never be no text and you'd always have to make a text
if (textChanged && IsUndoEnabled && !_isUndoingRedoing) // 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)
{ {
_undoRedoHelper.Clear(); SnapshotUndoRedo();
SnapshotUndoRedo(); // so we always have an initial state
} }
var textChanged = SetAndRaise(TextProperty, ref _text, value);
if (textChanged) if (textChanged)
{ {
RaiseTextChangeEvents(); RaiseTextChangeEvents();
@ -428,6 +582,9 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Gets or sets the text selected in the TextBox
/// </summary>
public string SelectedText public string SelectedText
{ {
get => GetSelection(); get => GetSelection();
@ -464,6 +621,9 @@ namespace Avalonia.Controls
set => SetValue(VerticalContentAlignmentProperty, value); set => SetValue(VerticalContentAlignmentProperty, value);
} }
/// <summary>
/// Gets or sets the <see cref="Media.TextAlignment"/> of the TextBox
/// </summary>
public TextAlignment TextAlignment public TextAlignment TextAlignment
{ {
get => GetValue(TextAlignmentProperty); get => GetValue(TextAlignmentProperty);
@ -490,24 +650,36 @@ namespace Avalonia.Controls
set => SetValue(UseFloatingWatermarkProperty, value); set => SetValue(UseFloatingWatermarkProperty, value);
} }
/// <summary>
/// Gets or sets custom content that is positioned on the left side of the text layout box
/// </summary>
public object InnerLeftContent public object InnerLeftContent
{ {
get => GetValue(InnerLeftContentProperty); get => GetValue(InnerLeftContentProperty);
set => SetValue(InnerLeftContentProperty, value); set => SetValue(InnerLeftContentProperty, value);
} }
/// <summary>
/// Gets or sets custom content that is positioned on the right side of the text layout box
/// </summary>
public object InnerRightContent public object InnerRightContent
{ {
get => GetValue(InnerRightContentProperty); get => GetValue(InnerRightContentProperty);
set => SetValue(InnerRightContentProperty, value); set => SetValue(InnerRightContentProperty, value);
} }
/// <summary>
/// Gets or sets whether text masked by <see cref="PasswordChar"/> should be revealed
/// </summary>
public bool RevealPassword public bool RevealPassword
{ {
get => GetValue(RevealPasswordProperty); get => GetValue(RevealPasswordProperty);
set => SetValue(RevealPasswordProperty, value); set => SetValue(RevealPasswordProperty, value);
} }
/// <summary>
/// Gets or sets the <see cref="Media.TextWrapping"/> of the TextBox
/// </summary>
public TextWrapping TextWrapping public TextWrapping TextWrapping
{ {
get => GetValue(TextWrappingProperty); get => GetValue(TextWrappingProperty);
@ -567,6 +739,9 @@ namespace Avalonia.Controls
set => SetValue(IsUndoEnabledProperty, value); set => SetValue(IsUndoEnabledProperty, value);
} }
/// <summary>
/// Gets or sets the maximum number of items that can reside in the Undo stack
/// </summary>
public int UndoLimit public int UndoLimit
{ {
get => _undoRedoHelper.Limit; get => _undoRedoHelper.Limit;
@ -590,18 +765,45 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Gets a value that indicates whether the undo stack has an action that can be undone
/// </summary>
public bool CanUndo
{
get => _canUndo;
private set => SetAndRaise(CanUndoProperty, ref _canUndo, value);
}
/// <summary>
/// Gets a value that indicates whether the redo stack has an action that can be redone
/// </summary>
public bool CanRedo
{
get => _canRedo;
private set => SetAndRaise(CanRedoProperty, ref _canRedo, value);
}
/// <summary>
/// Raised when content is being copied to the clipboard
/// </summary>
public event EventHandler<RoutedEventArgs>? CopyingToClipboard public event EventHandler<RoutedEventArgs>? CopyingToClipboard
{ {
add => AddHandler(CopyingToClipboardEvent, value); add => AddHandler(CopyingToClipboardEvent, value);
remove => RemoveHandler(CopyingToClipboardEvent, value); remove => RemoveHandler(CopyingToClipboardEvent, value);
} }
/// <summary>
/// Raised when content is being cut to the clipboard
/// </summary>
public event EventHandler<RoutedEventArgs>? CuttingToClipboard public event EventHandler<RoutedEventArgs>? CuttingToClipboard
{ {
add => AddHandler(CuttingToClipboardEvent, value); add => AddHandler(CuttingToClipboardEvent, value);
remove => RemoveHandler(CuttingToClipboardEvent, value); remove => RemoveHandler(CuttingToClipboardEvent, value);
} }
/// <summary>
/// Raised when content is being pasted from the clipboard
/// </summary>
public event EventHandler<RoutedEventArgs>? PastingFromClipboard public event EventHandler<RoutedEventArgs>? PastingFromClipboard
{ {
add => AddHandler(PastingFromClipboardEvent, value); add => AddHandler(PastingFromClipboardEvent, value);
@ -831,6 +1033,9 @@ namespace Avalonia.Controls
return text; return text;
} }
/// <summary>
/// Cuts the current text onto the clipboard
/// </summary>
public async void Cut() public async void Cut()
{ {
var text = GetSelection(); var text = GetSelection();
@ -851,6 +1056,9 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Copies the current text onto the clipboard
/// </summary>
public async void Copy() public async void Copy()
{ {
var text = GetSelection(); var text = GetSelection();
@ -869,6 +1077,9 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Pastes the current clipboard text content into the TextBox
/// </summary>
public async void Paste() public async void Paste()
{ {
var eventArgs = new RoutedEventArgs(PastingFromClipboardEvent); var eventArgs = new RoutedEventArgs(PastingFromClipboardEvent);
@ -943,30 +1154,13 @@ namespace Avalonia.Controls
} }
else if (Match(keymap.Undo) && IsUndoEnabled) else if (Match(keymap.Undo) && IsUndoEnabled)
{ {
try Undo();
{
SnapshotUndoRedo();
_isUndoingRedoing = true;
_undoRedoHelper.Undo();
}
finally
{
_isUndoingRedoing = false;
}
handled = true; handled = true;
} }
else if (Match(keymap.Redo) && IsUndoEnabled) else if (Match(keymap.Redo) && IsUndoEnabled)
{ {
try Redo();
{
_isUndoingRedoing = true;
_undoRedoHelper.Redo();
}
finally
{
_isUndoingRedoing = false;
}
handled = true; handled = true;
} }
@ -1420,6 +1614,9 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Clears the text in the TextBox
/// </summary>
public void Clear() public void Clear()
{ {
Text = string.Empty; Text = string.Empty;
@ -1703,5 +1900,62 @@ namespace Avalonia.Controls
} }
} }
} }
/// <summary>
/// Undoes the first action in the undo stack
/// </summary>
public void Undo()
{
if (IsUndoEnabled && CanUndo)
{
try
{
// Snapshot the current Text state - this will get popped on to the redo stack
// when we call undo below
SnapshotUndoRedo();
_isUndoingRedoing = true;
_undoRedoHelper.Undo();
}
finally
{
_isUndoingRedoing = false;
}
}
}
/// <summary>
/// Reapplies the first item on the redo stack
/// </summary>
public void Redo()
{
if (IsUndoEnabled && CanRedo)
{
try
{
_isUndoingRedoing = true;
_undoRedoHelper.Redo();
}
finally
{
_isUndoingRedoing = false;
}
}
}
/// <summary>
/// Called from the UndoRedoHelper when the undo stack is modified
/// </summary>
void UndoRedoHelper<UndoRedoState>.IUndoRedoHost.OnUndoStackChanged()
{
CanUndo = _undoRedoHelper.CanUndo;
}
/// <summary>
/// Called from the UndoRedoHelper when the redo stack is modified
/// </summary>
void UndoRedoHelper<UndoRedoState>.IUndoRedoHost.OnRedoStackChanged()
{
CanRedo = _undoRedoHelper.CanRedo;
}
} }
} }

26
src/Avalonia.Controls/Utils/UndoRedoHelper.cs

@ -1,9 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Utilities;
namespace Avalonia.Controls.Utils namespace Avalonia.Controls.Utils
{ {
@ -14,9 +9,11 @@ namespace Avalonia.Controls.Utils
public interface IUndoRedoHost public interface IUndoRedoHost
{ {
TState UndoRedoState { get; set; } TState UndoRedoState { get; set; }
}
void OnUndoStackChanged();
void OnRedoStackChanged();
}
private readonly LinkedList<TState> _states = new LinkedList<TState>(); private readonly LinkedList<TState> _states = new LinkedList<TState>();
@ -28,6 +25,10 @@ namespace Avalonia.Controls.Utils
/// </summary> /// </summary>
public int Limit { get; set; } = 10; public int Limit { get; set; } = 10;
public bool CanUndo => _currentNode?.Previous != null;
public bool CanRedo => _currentNode?.Next != null;
public UndoRedoHelper(IUndoRedoHost host) public UndoRedoHelper(IUndoRedoHost host)
{ {
_host = host; _host = host;
@ -39,6 +40,8 @@ namespace Avalonia.Controls.Utils
{ {
_currentNode = _currentNode.Previous; _currentNode = _currentNode.Previous;
_host.UndoRedoState = _currentNode.Value; _host.UndoRedoState = _currentNode.Value;
_host.OnUndoStackChanged();
_host.OnRedoStackChanged();
} }
} }
@ -55,6 +58,7 @@ namespace Avalonia.Controls.Utils
} }
public bool HasState => _currentNode != null; public bool HasState => _currentNode != null;
public void UpdateLastState(TState state) public void UpdateLastState(TState state)
{ {
if (_states.Last != null) if (_states.Last != null)
@ -72,6 +76,8 @@ namespace Avalonia.Controls.Utils
{ {
while (_currentNode?.Next != null) while (_currentNode?.Next != null)
_states.Remove(_currentNode.Next); _states.Remove(_currentNode.Next);
_host.OnRedoStackChanged();
} }
public void Redo() public void Redo()
@ -80,6 +86,8 @@ namespace Avalonia.Controls.Utils
{ {
_currentNode = _currentNode.Next; _currentNode = _currentNode.Next;
_host.UndoRedoState = _currentNode.Value; _host.UndoRedoState = _currentNode.Value;
_host.OnRedoStackChanged();
_host.OnUndoStackChanged();
} }
} }
@ -94,6 +102,9 @@ namespace Avalonia.Controls.Utils
_currentNode = _states.Last; _currentNode = _states.Last;
if (Limit != -1 && _states.Count > Limit) if (Limit != -1 && _states.Count > Limit)
_states.RemoveFirst(); _states.RemoveFirst();
_host.OnUndoStackChanged();
_host.OnRedoStackChanged();
} }
} }
@ -101,6 +112,9 @@ namespace Avalonia.Controls.Utils
{ {
_states.Clear(); _states.Clear();
_currentNode = null; _currentNode = null;
_host.OnUndoStackChanged();
_host.OnRedoStackChanged();
} }
} }
} }

20
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -4,6 +4,7 @@ using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Styling; using Avalonia.Styling;
@ -736,6 +737,25 @@ namespace Avalonia.Controls.UnitTests
root.Child = null; root.Child = null;
root.Child = target; root.Child = target;
} }
[Fact]
public void Should_Use_DisplayMemberBinding()
{
var target = new ItemsControl
{
Template = GetTemplate(),
DisplayMemberBinding = new Binding("Length")
};
target.Items = new[] { "Foo" };
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var container = (ContentPresenter)target.Presenter.Panel.Children[0];
container.UpdateChild();
Assert.Equal(container.Child!.GetValue(TextBlock.TextProperty), "3");
}
private class Item private class Item
{ {

170
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -866,6 +866,176 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Fact]
public void CanUndo_CanRedo_Is_False_When_Initialized()
{
using (UnitTestApplication.Start(Services))
{
var tb = new TextBox
{
Template = CreateTemplate(),
Text = "New Text"
};
tb.Measure(Size.Infinity);
Assert.False(tb.CanUndo);
Assert.False(tb.CanRedo);
}
}
[Fact]
public void CanUndo_CanRedo_and_Programmatic_Undo_Redo_Works()
{
using (UnitTestApplication.Start(Services))
{
var tb = new TextBox
{
Template = CreateTemplate(),
};
tb.Measure(Size.Infinity);
// See GH #6024 for a bit more insight on when Undo/Redo snapshots are taken:
// - Every 'Space', but only when space is handled in OnKeyDown - Spaces in TextInput event won't work
// - Every 7 chars in a long word
RaiseTextEvent(tb, "ABC");
RaiseKeyEvent(tb, Key.Space, KeyModifiers.None);
RaiseTextEvent(tb, "DEF");
RaiseKeyEvent(tb, Key.Space, KeyModifiers.None);
RaiseTextEvent(tb, "123");
// NOTE: the spaces won't actually add spaces b/c they're sent only as key events and not Text events
// so our final text is without spaces
Assert.Equal("ABCDEF123", tb.Text);
Assert.True(tb.CanUndo);
tb.Undo();
// Undo will take us back one step
Assert.Equal("ABCDEF", tb.Text);
Assert.True(tb.CanRedo);
tb.Redo();
// Redo should restore us
Assert.Equal("ABCDEF123", tb.Text);
}
}
[Fact]
public void Setting_UndoLimit_Clears_Undo_Redo()
{
using (UnitTestApplication.Start(Services))
{
var tb = new TextBox
{
Template = CreateTemplate(),
};
tb.Measure(Size.Infinity);
// This is all the same as the above test (CanUndo_CanRedo_and_Programmatic_Undo_Redo_Works)
// We do this to get the undo/redo stacks in a state where both are active
RaiseTextEvent(tb, "ABC");
RaiseKeyEvent(tb, Key.Space, KeyModifiers.None);
RaiseTextEvent(tb, "DEF");
RaiseKeyEvent(tb, Key.Space, KeyModifiers.None);
RaiseTextEvent(tb, "123");
Assert.Equal("ABCDEF123", tb.Text);
Assert.True(tb.CanUndo);
tb.Undo();
// Undo will take us back one step
Assert.Equal("ABCDEF", tb.Text);
Assert.True(tb.CanRedo);
tb.Redo();
// Redo should restore us
Assert.Equal("ABCDEF123", tb.Text);
// Change the undo limit, this should clear both stacks setting CanUndo and CanRedo to false
tb.UndoLimit = 1;
Assert.False(tb.CanUndo);
Assert.False(tb.CanRedo);
}
}
[Fact]
public void Setting_IsUndoEnabled_To_False_Clears_Undo_Redo()
{
using (UnitTestApplication.Start(Services))
{
var tb = new TextBox
{
Template = CreateTemplate(),
};
tb.Measure(Size.Infinity);
// This is all the same as the above test (CanUndo_CanRedo_and_Programmatic_Undo_Redo_Works)
// We do this to get the undo/redo stacks in a state where both are active
RaiseTextEvent(tb, "ABC");
RaiseKeyEvent(tb, Key.Space, KeyModifiers.None);
RaiseTextEvent(tb, "DEF");
RaiseKeyEvent(tb, Key.Space, KeyModifiers.None);
RaiseTextEvent(tb, "123");
Assert.Equal("ABCDEF123", tb.Text);
Assert.True(tb.CanUndo);
tb.Undo();
// Undo will take us back one step
Assert.Equal("ABCDEF", tb.Text);
Assert.True(tb.CanRedo);
tb.Redo();
// Redo should restore us
Assert.Equal("ABCDEF123", tb.Text);
// Disable Undo/Redo, this should clear both stacks setting CanUndo and CanRedo to false
tb.IsUndoEnabled = false;
Assert.False(tb.CanUndo);
Assert.False(tb.CanRedo);
}
}
[Fact]
public void UndoLimit_Count_Is_Respected()
{
using (UnitTestApplication.Start(Services))
{
var tb = new TextBox
{
Template = CreateTemplate(),
UndoLimit = 3 // Something small for this test
};
tb.Measure(Size.Infinity);
// Push 3 undoable actions, we should only be able to recover 2
RaiseTextEvent(tb, "ABC");
RaiseKeyEvent(tb, Key.Space, KeyModifiers.None);
RaiseTextEvent(tb, "DEF");
RaiseKeyEvent(tb, Key.Space, KeyModifiers.None);
RaiseTextEvent(tb, "123");
Assert.Equal("ABCDEF123", tb.Text);
// Undo will take us back one step
tb.Undo();
Assert.Equal("ABCDEF", tb.Text);
// Undo again
tb.Undo();
Assert.Equal("ABC", tb.Text);
// We now should not be able to undo again
Assert.False(tb.CanUndo);
}
}
private static TestServices FocusServices => TestServices.MockThreadingInterface.With( private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(), focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(), keyboardDevice: () => new KeyboardDevice(),

Loading…
Cancel
Save