@ -0,0 +1,433 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Styling; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class MaskedTextBox : TextBox, IStyleable |
|||
{ |
|||
public static readonly StyledProperty<bool> AsciiOnlyProperty = |
|||
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(AsciiOnly)); |
|||
|
|||
public static readonly DirectProperty<MaskedTextBox, CultureInfo?> CultureProperty = |
|||
AvaloniaProperty.RegisterDirect<MaskedTextBox, CultureInfo?>(nameof(Culture), o => o.Culture, |
|||
(o, v) => o.Culture = v, CultureInfo.CurrentCulture); |
|||
|
|||
public static readonly StyledProperty<bool> HidePromptOnLeaveProperty = |
|||
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(HidePromptOnLeave)); |
|||
|
|||
public static readonly DirectProperty<MaskedTextBox, bool?> MaskCompletedProperty = |
|||
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool?>(nameof(MaskCompleted), o => o.MaskCompleted); |
|||
|
|||
public static readonly DirectProperty<MaskedTextBox, bool?> MaskFullProperty = |
|||
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool?>(nameof(MaskFull), o => o.MaskFull); |
|||
|
|||
public static readonly StyledProperty<string?> MaskProperty = |
|||
AvaloniaProperty.Register<MaskedTextBox, string?>(nameof(Mask), string.Empty); |
|||
|
|||
public static new readonly StyledProperty<char> PasswordCharProperty = |
|||
AvaloniaProperty.Register<TextBox, char>(nameof(PasswordChar), '\0'); |
|||
|
|||
public static readonly StyledProperty<char> PromptCharProperty = |
|||
AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PromptChar), '_'); |
|||
|
|||
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnPromptProperty = |
|||
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnPrompt), o => o.ResetOnPrompt, (o, v) => o.ResetOnPrompt = v); |
|||
|
|||
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnSpaceProperty = |
|||
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnSpace), o => o.ResetOnSpace, (o, v) => o.ResetOnSpace = v); |
|||
|
|||
private CultureInfo? _culture; |
|||
|
|||
private bool _resetOnPrompt = true; |
|||
|
|||
private bool _ignoreTextChanges; |
|||
|
|||
private bool _resetOnSpace = true; |
|||
|
|||
public MaskedTextBox() { } |
|||
|
|||
/// <summary>
|
|||
/// Constructs the MaskedTextBox with the specified MaskedTextProvider object.
|
|||
/// </summary>
|
|||
public MaskedTextBox(MaskedTextProvider maskedTextProvider) |
|||
{ |
|||
if (maskedTextProvider == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(maskedTextProvider)); |
|||
} |
|||
AsciiOnly = maskedTextProvider.AsciiOnly; |
|||
Culture = maskedTextProvider.Culture; |
|||
Mask = maskedTextProvider.Mask; |
|||
PasswordChar = maskedTextProvider.PasswordChar; |
|||
PromptChar = maskedTextProvider.PromptChar; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating if the masked text box is restricted to accept only ASCII characters.
|
|||
/// Default value is false.
|
|||
/// </summary>
|
|||
public bool AsciiOnly |
|||
{ |
|||
get => GetValue(AsciiOnlyProperty); |
|||
set => SetValue(AsciiOnlyProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the culture information associated with the masked text box.
|
|||
/// </summary>
|
|||
public CultureInfo? Culture |
|||
{ |
|||
get => _culture; |
|||
set => SetAndRaise(CultureProperty, ref _culture, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating if the prompt character is hidden when the masked text box loses focus.
|
|||
/// </summary>
|
|||
public bool HidePromptOnLeave |
|||
{ |
|||
get => GetValue(HidePromptOnLeaveProperty); |
|||
set => SetValue(HidePromptOnLeaveProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the mask to apply to the TextBox.
|
|||
/// </summary>
|
|||
public string? Mask |
|||
{ |
|||
get => GetValue(MaskProperty); |
|||
set => SetValue(MaskProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Specifies whether the test string required input positions, as specified by the mask, have
|
|||
/// all been assigned.
|
|||
/// </summary>
|
|||
public bool? MaskCompleted |
|||
{ |
|||
get => MaskProvider?.MaskCompleted; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Specifies whether all inputs (required and optional) have been provided into the mask successfully.
|
|||
/// </summary>
|
|||
public bool? MaskFull |
|||
{ |
|||
get => MaskProvider?.MaskFull; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the MaskTextProvider for the specified Mask.
|
|||
/// </summary>
|
|||
public MaskedTextProvider? MaskProvider { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the character to be displayed in substitute for user input.
|
|||
/// </summary>
|
|||
public new char PasswordChar |
|||
{ |
|||
get => GetValue(PasswordCharProperty); |
|||
set => SetValue(PasswordCharProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the character used to represent the absence of user input in MaskedTextBox.
|
|||
/// </summary>
|
|||
public char PromptChar |
|||
{ |
|||
get => GetValue(PromptCharProperty); |
|||
set => SetValue(PromptCharProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating if selected characters should be reset when the prompt character is pressed.
|
|||
/// </summary>
|
|||
public bool ResetOnPrompt |
|||
{ |
|||
get => _resetOnPrompt; |
|||
set |
|||
{ |
|||
SetAndRaise(ResetOnPromptProperty, ref _resetOnPrompt, value); |
|||
if (MaskProvider != null) |
|||
{ |
|||
MaskProvider.ResetOnPrompt = value; |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating if selected characters should be reset when the space character is pressed.
|
|||
/// </summary>
|
|||
public bool ResetOnSpace |
|||
{ |
|||
get => _resetOnSpace; |
|||
set |
|||
{ |
|||
SetAndRaise(ResetOnSpaceProperty, ref _resetOnSpace, value); |
|||
if (MaskProvider != null) |
|||
{ |
|||
MaskProvider.ResetOnSpace = value; |
|||
} |
|||
|
|||
} |
|||
|
|||
|
|||
} |
|||
|
|||
Type IStyleable.StyleKey => typeof(TextBox); |
|||
|
|||
protected override void OnGotFocus(GotFocusEventArgs e) |
|||
{ |
|||
if (HidePromptOnLeave == true && MaskProvider != null) |
|||
{ |
|||
Text = MaskProvider.ToDisplayString(); |
|||
} |
|||
base.OnGotFocus(e); |
|||
} |
|||
|
|||
protected override async void OnKeyDown(KeyEventArgs e) |
|||
{ |
|||
if (MaskProvider == null) |
|||
{ |
|||
base.OnKeyDown(e); |
|||
return; |
|||
} |
|||
|
|||
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>(); |
|||
|
|||
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e)); |
|||
|
|||
if (Match(keymap.Paste)) |
|||
{ |
|||
var text = await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))).GetTextAsync(); |
|||
|
|||
if (text == null) |
|||
return; |
|||
|
|||
foreach (var item in text) |
|||
{ |
|||
var index = GetNextCharacterPosition(CaretIndex); |
|||
if (MaskProvider.InsertAt(item, index)) |
|||
{ |
|||
CaretIndex = ++index; |
|||
} |
|||
} |
|||
|
|||
Text = MaskProvider.ToDisplayString(); |
|||
e.Handled = true; |
|||
return; |
|||
} |
|||
|
|||
if (e.Key != Key.Back) |
|||
{ |
|||
base.OnKeyDown(e); |
|||
} |
|||
|
|||
switch (e.Key) |
|||
{ |
|||
case Key.Delete: |
|||
if (CaretIndex < Text.Length) |
|||
{ |
|||
if (MaskProvider.RemoveAt(CaretIndex)) |
|||
{ |
|||
RefreshText(MaskProvider, CaretIndex); |
|||
} |
|||
|
|||
e.Handled = true; |
|||
} |
|||
break; |
|||
case Key.Space: |
|||
if (!MaskProvider.ResetOnSpace || string.IsNullOrEmpty(SelectedText)) |
|||
{ |
|||
if (MaskProvider.InsertAt(" ", CaretIndex)) |
|||
{ |
|||
RefreshText(MaskProvider, CaretIndex); |
|||
} |
|||
} |
|||
|
|||
e.Handled = true; |
|||
break; |
|||
case Key.Back: |
|||
if (CaretIndex > 0) |
|||
{ |
|||
MaskProvider.RemoveAt(CaretIndex - 1); |
|||
} |
|||
RefreshText(MaskProvider, CaretIndex - 1); |
|||
e.Handled = true; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
protected override void OnLostFocus(RoutedEventArgs e) |
|||
{ |
|||
if (HidePromptOnLeave == true && MaskProvider != null) |
|||
{ |
|||
Text = MaskProvider.ToString(!HidePromptOnLeave, true); |
|||
} |
|||
base.OnLostFocus(e); |
|||
} |
|||
|
|||
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) |
|||
{ |
|||
void UpdateMaskProvider() |
|||
{ |
|||
MaskProvider = new MaskedTextProvider(Mask, Culture, true, PromptChar, PasswordChar, AsciiOnly) { ResetOnSpace = ResetOnSpace, ResetOnPrompt = ResetOnPrompt }; |
|||
if (Text != null) |
|||
{ |
|||
MaskProvider.Set(Text); |
|||
} |
|||
RefreshText(MaskProvider, 0); |
|||
} |
|||
if (change.Property == TextProperty && MaskProvider != null && _ignoreTextChanges == false) |
|||
{ |
|||
if (string.IsNullOrEmpty(Text)) |
|||
{ |
|||
MaskProvider.Clear(); |
|||
RefreshText(MaskProvider, CaretIndex); |
|||
base.OnPropertyChanged(change); |
|||
return; |
|||
} |
|||
|
|||
MaskProvider.Set(Text); |
|||
RefreshText(MaskProvider, CaretIndex); |
|||
} |
|||
else if (change.Property == MaskProperty) |
|||
{ |
|||
UpdateMaskProvider(); |
|||
|
|||
if (!string.IsNullOrEmpty(Mask)) |
|||
{ |
|||
foreach (var c in Mask!) |
|||
{ |
|||
if (!MaskedTextProvider.IsValidMaskChar(c)) |
|||
{ |
|||
throw new ArgumentException("Specified mask contains characters that are not valid."); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
else if (change.Property == PasswordCharProperty) |
|||
{ |
|||
if (!MaskedTextProvider.IsValidPasswordChar(PasswordChar)) |
|||
{ |
|||
throw new ArgumentException("Specified character value is not allowed for this property.", nameof(PasswordChar)); |
|||
} |
|||
if (MaskProvider != null && PasswordChar == MaskProvider.PromptChar) |
|||
{ |
|||
// Prompt and password chars must be different.
|
|||
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same."); |
|||
} |
|||
if (MaskProvider != null && MaskProvider.PasswordChar != PasswordChar) |
|||
{ |
|||
UpdateMaskProvider(); |
|||
} |
|||
} |
|||
else if (change.Property == PromptCharProperty) |
|||
{ |
|||
if (!MaskedTextProvider.IsValidInputChar(PromptChar)) |
|||
{ |
|||
throw new ArgumentException("Specified character value is not allowed for this property."); |
|||
} |
|||
if (PromptChar == PasswordChar) |
|||
{ |
|||
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same."); |
|||
} |
|||
if (MaskProvider != null && MaskProvider.PromptChar != PromptChar) |
|||
{ |
|||
UpdateMaskProvider(); |
|||
} |
|||
} |
|||
else if (change.Property == AsciiOnlyProperty && MaskProvider != null && MaskProvider.AsciiOnly != AsciiOnly |
|||
|| change.Property == CultureProperty && MaskProvider != null && !MaskProvider.Culture.Equals(Culture)) |
|||
{ |
|||
UpdateMaskProvider(); |
|||
} |
|||
base.OnPropertyChanged(change); |
|||
} |
|||
protected override void OnTextInput(TextInputEventArgs e) |
|||
{ |
|||
_ignoreTextChanges = true; |
|||
try |
|||
{ |
|||
if (IsReadOnly) |
|||
{ |
|||
e.Handled = true; |
|||
base.OnTextInput(e); |
|||
return; |
|||
} |
|||
if (MaskProvider == null) |
|||
{ |
|||
base.OnTextInput(e); |
|||
return; |
|||
} |
|||
if ((MaskProvider.ResetOnSpace && e.Text == " " || MaskProvider.ResetOnPrompt && e.Text == MaskProvider.PromptChar.ToString()) && !string.IsNullOrEmpty(SelectedText)) |
|||
{ |
|||
if (SelectionStart > SelectionEnd ? MaskProvider.RemoveAt(SelectionEnd, SelectionStart - 1) : MaskProvider.RemoveAt(SelectionStart, SelectionEnd - 1)) |
|||
{ |
|||
SelectedText = string.Empty; |
|||
} |
|||
} |
|||
|
|||
if (CaretIndex < Text.Length) |
|||
{ |
|||
CaretIndex = GetNextCharacterPosition(CaretIndex); |
|||
|
|||
if (MaskProvider.InsertAt(e.Text, CaretIndex)) |
|||
{ |
|||
CaretIndex++; |
|||
} |
|||
var nextPos = GetNextCharacterPosition(CaretIndex); |
|||
if (nextPos != 0 && CaretIndex != Text.Length) |
|||
{ |
|||
CaretIndex = nextPos; |
|||
} |
|||
} |
|||
|
|||
RefreshText(MaskProvider, CaretIndex); |
|||
|
|||
|
|||
e.Handled = true; |
|||
|
|||
base.OnTextInput(e); |
|||
} |
|||
finally |
|||
{ |
|||
_ignoreTextChanges = false; |
|||
} |
|||
|
|||
} |
|||
|
|||
private int GetNextCharacterPosition(int startPosition) |
|||
{ |
|||
if (MaskProvider != null) |
|||
{ |
|||
var position = MaskProvider.FindEditPositionFrom(startPosition, true); |
|||
if (CaretIndex != -1) |
|||
{ |
|||
return position; |
|||
} |
|||
} |
|||
return startPosition; |
|||
} |
|||
|
|||
private void RefreshText(MaskedTextProvider provider, int position) |
|||
{ |
|||
if (provider != null) |
|||
{ |
|||
Text = provider.ToDisplayString(); |
|||
CaretIndex = position; |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,170 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Avalonia.Platform; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
public enum GeometryCombineMode |
|||
{ |
|||
/// <summary>
|
|||
/// The two regions are combined by taking the union of both. The resulting geometry is
|
|||
/// geometry A + geometry B.
|
|||
/// </summary>
|
|||
Union, |
|||
|
|||
/// <summary>
|
|||
/// The two regions are combined by taking their intersection. The new area consists of the
|
|||
/// overlapping region between the two geometries.
|
|||
/// </summary>
|
|||
Intersect, |
|||
|
|||
/// <summary>
|
|||
/// The two regions are combined by taking the area that exists in the first region but not
|
|||
/// the second and the area that exists in the second region but not the first. The new
|
|||
/// region consists of (A-B) + (B-A), where A and B are geometries.
|
|||
/// </summary>
|
|||
Xor, |
|||
|
|||
/// <summary>
|
|||
/// The second region is excluded from the first. Given two geometries, A and B, the area of
|
|||
/// geometry B is removed from the area of geometry A, producing a region that is A-B.
|
|||
/// </summary>
|
|||
Exclude, |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a 2-D geometric shape defined by the combination of two Geometry objects.
|
|||
/// </summary>
|
|||
public class CombinedGeometry : Geometry |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the <see cref="Geometry1"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<Geometry?> Geometry1Property = |
|||
AvaloniaProperty.Register<CombinedGeometry, Geometry?>(nameof(Geometry1)); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Geometry2"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<Geometry?> Geometry2Property = |
|||
AvaloniaProperty.Register<CombinedGeometry, Geometry?>(nameof(Geometry2)); |
|||
/// <summary>
|
|||
/// Defines the <see cref="GeometryCombineMode"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<GeometryCombineMode> GeometryCombineModeProperty = |
|||
AvaloniaProperty.Register<CombinedGeometry, GeometryCombineMode>(nameof(GeometryCombineMode)); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class.
|
|||
/// </summary>
|
|||
public CombinedGeometry() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
|
|||
/// specified <see cref="Geometry"/> objects.
|
|||
/// </summary>
|
|||
/// <param name="geometry1">The first geometry to combine.</param>
|
|||
/// <param name="geometry2">The second geometry to combine.</param>
|
|||
public CombinedGeometry(Geometry geometry1, Geometry geometry2) |
|||
{ |
|||
Geometry1 = geometry1; |
|||
Geometry2 = geometry2; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
|
|||
/// specified <see cref="Geometry"/> objects and <see cref="GeometryCombineMode"/>.
|
|||
/// </summary>
|
|||
/// <param name="combineMode">The method by which geometry1 and geometry2 are combined.</param>
|
|||
/// <param name="geometry1">The first geometry to combine.</param>
|
|||
/// <param name="geometry2">The second geometry to combine.</param>
|
|||
public CombinedGeometry(GeometryCombineMode combineMode, Geometry? geometry1, Geometry? geometry2) |
|||
{ |
|||
Geometry1 = geometry1; |
|||
Geometry2 = geometry2; |
|||
GeometryCombineMode = combineMode; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
|
|||
/// specified <see cref="Geometry"/> objects, <see cref="GeometryCombineMode"/> and
|
|||
/// <see cref="Transform"/>.
|
|||
/// </summary>
|
|||
/// <param name="combineMode">The method by which geometry1 and geometry2 are combined.</param>
|
|||
/// <param name="geometry1">The first geometry to combine.</param>
|
|||
/// <param name="geometry2">The second geometry to combine.</param>
|
|||
/// <param name="transform">The transform applied to the geometry.</param>
|
|||
public CombinedGeometry( |
|||
GeometryCombineMode combineMode, |
|||
Geometry? geometry1, |
|||
Geometry? geometry2, |
|||
Transform? transform) |
|||
{ |
|||
Geometry1 = geometry1; |
|||
Geometry2 = geometry2; |
|||
GeometryCombineMode = combineMode; |
|||
Transform = transform; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the first <see cref="Geometry"/> object of this
|
|||
/// <see cref="CombinedGeometry"/> object.
|
|||
/// </summary>
|
|||
public Geometry? Geometry1 |
|||
{ |
|||
get => GetValue(Geometry1Property); |
|||
set => SetValue(Geometry1Property, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the second <see cref="Geometry"/> object of this
|
|||
/// <see cref="CombinedGeometry"/> object.
|
|||
/// </summary>
|
|||
public Geometry? Geometry2 |
|||
{ |
|||
get => GetValue(Geometry2Property); |
|||
set => SetValue(Geometry2Property, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the method by which the two geometries (specified by the
|
|||
/// <see cref="Geometry1"/> and <see cref="Geometry2"/> properties) are combined. The
|
|||
/// default value is <see cref="GeometryCombineMode.Union"/>.
|
|||
/// </summary>
|
|||
public GeometryCombineMode GeometryCombineMode |
|||
{ |
|||
get => GetValue(GeometryCombineModeProperty); |
|||
set => SetValue(GeometryCombineModeProperty, value); |
|||
} |
|||
|
|||
public override Geometry Clone() |
|||
{ |
|||
return new CombinedGeometry(GeometryCombineMode, Geometry1, Geometry2, Transform); |
|||
} |
|||
|
|||
protected override IGeometryImpl? CreateDefiningGeometry() |
|||
{ |
|||
var g1 = Geometry1; |
|||
var g2 = Geometry2; |
|||
|
|||
if (g1 is object && g2 is object) |
|||
{ |
|||
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>(); |
|||
return factory.CreateCombinedGeometry(GeometryCombineMode, g1, g2); |
|||
} |
|||
else if (GeometryCombineMode == GeometryCombineMode.Intersect) |
|||
return null; |
|||
else if (g1 is object) |
|||
return g1.PlatformImpl; |
|||
else if (g2 is object) |
|||
return g2.PlatformImpl; |
|||
else |
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Animation; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
public class GeometryCollection : Animatable, IList<Geometry>, IReadOnlyList<Geometry> |
|||
{ |
|||
private List<Geometry> _inner; |
|||
|
|||
public GeometryCollection() => _inner = new List<Geometry>(); |
|||
public GeometryCollection(IEnumerable<Geometry> collection) => _inner = new List<Geometry>(collection); |
|||
public GeometryCollection(int capacity) => _inner = new List<Geometry>(capacity); |
|||
|
|||
public Geometry this[int index] |
|||
{ |
|||
get => _inner[index]; |
|||
set => _inner[index] = value; |
|||
} |
|||
|
|||
public int Count => _inner.Count; |
|||
public bool IsReadOnly => false; |
|||
|
|||
public void Add(Geometry item) => _inner.Add(item); |
|||
public void Clear() => _inner.Clear(); |
|||
public bool Contains(Geometry item) => _inner.Contains(item); |
|||
public void CopyTo(Geometry[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex); |
|||
public IEnumerator<Geometry> GetEnumerator() => _inner.GetEnumerator(); |
|||
public int IndexOf(Geometry item) => _inner.IndexOf(item); |
|||
public void Insert(int index, Geometry item) => _inner.Insert(index, item); |
|||
public bool Remove(Geometry item) => _inner.Remove(item); |
|||
public void RemoveAt(int index) => _inner.RemoveAt(index); |
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Metadata; |
|||
using Avalonia.Platform; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a composite geometry, composed of other <see cref="Geometry"/> objects.
|
|||
/// </summary>
|
|||
public class GeometryGroup : Geometry |
|||
{ |
|||
public static readonly DirectProperty<GeometryGroup, GeometryCollection?> ChildrenProperty = |
|||
AvaloniaProperty.RegisterDirect<GeometryGroup, GeometryCollection?> ( |
|||
nameof(Children), |
|||
o => o.Children, |
|||
(o, v) => o.Children = v); |
|||
|
|||
public static readonly StyledProperty<FillRule> FillRuleProperty = |
|||
AvaloniaProperty.Register<GeometryGroup, FillRule>(nameof(FillRule)); |
|||
|
|||
private GeometryCollection? _children; |
|||
private bool _childrenSet; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the collection that contains the child geometries.
|
|||
/// </summary>
|
|||
[Content] |
|||
public GeometryCollection? Children |
|||
{ |
|||
get => _children ??= (!_childrenSet ? new GeometryCollection() : null); |
|||
set |
|||
{ |
|||
SetAndRaise(ChildrenProperty, ref _children, value); |
|||
_childrenSet = true; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets how the intersecting areas of the objects contained in this
|
|||
/// <see cref="GeometryGroup"/> are combined. The default is <see cref="FillRule.EvenOdd"/>.
|
|||
/// </summary>
|
|||
public FillRule FillRule |
|||
{ |
|||
get => GetValue(FillRuleProperty); |
|||
set => SetValue(FillRuleProperty, value); |
|||
} |
|||
|
|||
public override Geometry Clone() |
|||
{ |
|||
var result = new GeometryGroup { FillRule = FillRule, Transform = Transform }; |
|||
if (_children?.Count > 0) |
|||
result.Children = new GeometryCollection(_children); |
|||
return result; |
|||
} |
|||
|
|||
protected override IGeometryImpl? CreateDefiningGeometry() |
|||
{ |
|||
if (_children?.Count > 0) |
|||
{ |
|||
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>(); |
|||
return factory.CreateGeometryGroup(FillRule, _children); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) |
|||
{ |
|||
base.OnPropertyChanged(change); |
|||
|
|||
if (change.Property == ChildrenProperty || change.Property == FillRuleProperty) |
|||
{ |
|||
InvalidateGeometry(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
namespace Avalonia.LinuxFramebuffer |
|||
{ |
|||
/// <summary>
|
|||
/// Platform-specific options which apply to the Linux framebuffer.
|
|||
/// </summary>
|
|||
public class LinuxFramebufferPlatformOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the number of frames per second at which the renderer should run.
|
|||
/// Default 60.
|
|||
/// </summary>
|
|||
public int Fps { get; set; } = 60; |
|||
} |
|||
} |
|||
@ -1,46 +0,0 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.LinuxFramebuffer |
|||
{ |
|||
unsafe class LockedFramebuffer : ILockedFramebuffer |
|||
{ |
|||
private readonly int _fb; |
|||
private readonly fb_fix_screeninfo _fixedInfo; |
|||
private fb_var_screeninfo _varInfo; |
|||
private readonly IntPtr _address; |
|||
|
|||
public LockedFramebuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr address, Vector dpi) |
|||
{ |
|||
_fb = fb; |
|||
_fixedInfo = fixedInfo; |
|||
_varInfo = varInfo; |
|||
_address = address; |
|||
Dpi = dpi; |
|||
//Use double buffering to avoid flicker
|
|||
Address = Marshal.AllocHGlobal(RowBytes * Size.Height); |
|||
} |
|||
|
|||
|
|||
void VSync() |
|||
{ |
|||
NativeUnsafeMethods.ioctl(_fb, FbIoCtl.FBIO_WAITFORVSYNC, null); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
VSync(); |
|||
NativeUnsafeMethods.memcpy(_address, Address, new IntPtr(RowBytes * Size.Height)); |
|||
|
|||
Marshal.FreeHGlobal(Address); |
|||
Address = IntPtr.Zero; |
|||
} |
|||
|
|||
public IntPtr Address { get; private set; } |
|||
public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres); |
|||
public int RowBytes => (int) _fixedInfo.line_length; |
|||
public Vector Dpi { get; } |
|||
public PixelFormat Format => _varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565 : _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888; |
|||
} |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
using System.Threading; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.LinuxFramebuffer.Output |
|||
{ |
|||
internal unsafe class FbDevBackBuffer : IDisposable |
|||
{ |
|||
private readonly int _fb; |
|||
private readonly fb_fix_screeninfo _fixedInfo; |
|||
private readonly fb_var_screeninfo _varInfo; |
|||
private readonly IntPtr _targetAddress; |
|||
private readonly object _lock = new object(); |
|||
|
|||
public FbDevBackBuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr targetAddress) |
|||
{ |
|||
_fb = fb; |
|||
_fixedInfo = fixedInfo; |
|||
_varInfo = varInfo; |
|||
_targetAddress = targetAddress; |
|||
Address = Marshal.AllocHGlobal(RowBytes * Size.Height); |
|||
} |
|||
|
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (Address != IntPtr.Zero) |
|||
{ |
|||
Marshal.FreeHGlobal(Address); |
|||
Address = IntPtr.Zero; |
|||
} |
|||
} |
|||
|
|||
public ILockedFramebuffer Lock(Vector dpi) |
|||
{ |
|||
Monitor.Enter(_lock); |
|||
try |
|||
{ |
|||
return new LockedFramebuffer(Address, |
|||
new PixelSize((int)_varInfo.xres, (int)_varInfo.yres), |
|||
(int)_fixedInfo.line_length, dpi, |
|||
_varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565 |
|||
: _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 |
|||
: PixelFormat.Bgra8888, |
|||
() => |
|||
{ |
|||
try |
|||
{ |
|||
NativeUnsafeMethods.ioctl(_fb, FbIoCtl.FBIO_WAITFORVSYNC, null); |
|||
NativeUnsafeMethods.memcpy(_targetAddress, Address, new IntPtr(RowBytes * Size.Height)); |
|||
} |
|||
finally |
|||
{ |
|||
Monitor.Exit(_lock); |
|||
} |
|||
}); |
|||
} |
|||
catch |
|||
{ |
|||
Monitor.Exit(_lock); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
public IntPtr Address { get; private set; } |
|||
public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres); |
|||
public int RowBytes => (int) _fixedInfo.line_length; |
|||
} |
|||
} |
|||
@ -1 +1 @@ |
|||
Subproject commit 9e90d34e97c766ba8dcb70128147fcded65d195a |
|||
Subproject commit f4ac681b91a9dc7a7a095d1050a683de23d86b72 |
|||
@ -0,0 +1,35 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia.Media; |
|||
using SkiaSharp; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
/// <summary>
|
|||
/// A Skia implementation of a <see cref="Avalonia.Media.GeometryGroup"/>.
|
|||
/// </summary>
|
|||
internal class CombinedGeometryImpl : GeometryImpl |
|||
{ |
|||
public CombinedGeometryImpl(GeometryCombineMode combineMode, Geometry g1, Geometry g2) |
|||
{ |
|||
var path1 = ((GeometryImpl)g1.PlatformImpl).EffectivePath; |
|||
var path2 = ((GeometryImpl)g2.PlatformImpl).EffectivePath; |
|||
var op = combineMode switch |
|||
{ |
|||
GeometryCombineMode.Intersect => SKPathOp.Intersect, |
|||
GeometryCombineMode.Xor => SKPathOp.Xor, |
|||
GeometryCombineMode.Exclude => SKPathOp.Difference, |
|||
_ => SKPathOp.Union, |
|||
}; |
|||
|
|||
var path = path1.Op(path2, op); |
|||
|
|||
EffectivePath = path; |
|||
Bounds = path.Bounds.ToAvaloniaRect(); |
|||
} |
|||
|
|||
public override Rect Bounds { get; } |
|||
public override SKPath EffectivePath { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia.Media; |
|||
using SkiaSharp; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
/// <summary>
|
|||
/// A Skia implementation of a <see cref="Avalonia.Media.GeometryGroup"/>.
|
|||
/// </summary>
|
|||
internal class GeometryGroupImpl : GeometryImpl |
|||
{ |
|||
public GeometryGroupImpl(FillRule fillRule, IReadOnlyList<Geometry> children) |
|||
{ |
|||
var path = new SKPath |
|||
{ |
|||
FillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd, |
|||
}; |
|||
|
|||
var count = children.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
if (children[i]?.PlatformImpl is GeometryImpl child) |
|||
path.AddPath(child.EffectivePath); |
|||
} |
|||
|
|||
EffectivePath = path; |
|||
Bounds = path.Bounds.ToAvaloniaRect(); |
|||
} |
|||
|
|||
public override Rect Bounds { get; } |
|||
public override SKPath EffectivePath { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using SharpDX.Direct2D1; |
|||
using AM = Avalonia.Media; |
|||
|
|||
namespace Avalonia.Direct2D1.Media |
|||
{ |
|||
/// <summary>
|
|||
/// A Direct2D implementation of a <see cref="Avalonia.Media.CombinedGeometry"/>.
|
|||
/// </summary>
|
|||
internal class CombinedGeometryImpl : GeometryImpl |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
|
|||
/// </summary>
|
|||
public CombinedGeometryImpl( |
|||
AM.GeometryCombineMode combineMode, |
|||
AM.Geometry geometry1, |
|||
AM.Geometry geometry2) |
|||
: base(CreateGeometry(combineMode, geometry1, geometry2)) |
|||
{ |
|||
} |
|||
|
|||
private static Geometry CreateGeometry( |
|||
AM.GeometryCombineMode combineMode, |
|||
AM.Geometry geometry1, |
|||
AM.Geometry geometry2) |
|||
{ |
|||
var g1 = ((GeometryImpl)geometry1.PlatformImpl).Geometry; |
|||
var g2 = ((GeometryImpl)geometry2.PlatformImpl).Geometry; |
|||
var dest = new PathGeometry(Direct2D1Platform.Direct2D1Factory); |
|||
using var sink = dest.Open(); |
|||
g1.Combine(g2, (CombineMode)combineMode, sink); |
|||
sink.Close(); |
|||
return dest; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using System.Collections.Generic; |
|||
using SharpDX.Direct2D1; |
|||
using AM = Avalonia.Media; |
|||
|
|||
namespace Avalonia.Direct2D1.Media |
|||
{ |
|||
/// <summary>
|
|||
/// A Direct2D implementation of a <see cref="Avalonia.Media.GeometryGroup"/>.
|
|||
/// </summary>
|
|||
internal class GeometryGroupImpl : GeometryImpl |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
|
|||
/// </summary>
|
|||
public GeometryGroupImpl(AM.FillRule fillRule, IReadOnlyList<AM.Geometry> geometry) |
|||
: base(CreateGeometry(fillRule, geometry)) |
|||
{ |
|||
} |
|||
|
|||
private static Geometry CreateGeometry(AM.FillRule fillRule, IReadOnlyList<AM.Geometry> children) |
|||
{ |
|||
var count = children.Count; |
|||
var c = new Geometry[count]; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
c[i] = ((GeometryImpl)children[i].PlatformImpl).Geometry; |
|||
} |
|||
|
|||
return new GeometryGroup(Direct2D1Platform.Direct2D1Factory, (FillMode)fillRule, c); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Shapes; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.UnitTests; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Logging |
|||
{ |
|||
public class LoggingTests |
|||
{ |
|||
[Fact] |
|||
public void Control_Should_Not_Log_Binding_Errors_When_Detached_From_Visual_Tree() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
{ |
|||
var xaml = @"
|
|||
<Window xmlns='https://github.com/avaloniaui'
|
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
xmlns:local='clr-namespace:Avalonia.Base.UnitTests.Logging;assembly=Avalonia.UnitTests'> |
|||
<Panel Name='panel'> |
|||
<Rectangle Name='rect' Fill='{Binding $parent[Window].Background}'/> |
|||
</Panel> |
|||
</Window>";
|
|||
|
|||
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
var calledTimes = 0; |
|||
using var logSink = TestLogSink.Start((l, a, s, m, d) => |
|||
{ |
|||
if (l >= Avalonia.Logging.LogEventLevel.Warning) |
|||
{ |
|||
calledTimes++; |
|||
} |
|||
}); |
|||
var panel = window.FindControl<Panel>("panel"); |
|||
var rect = window.FindControl<Rectangle>("rect"); |
|||
window.ApplyTemplate(); |
|||
window.Presenter.ApplyTemplate(); |
|||
panel.Children.Remove(rect); |
|||
Assert.Equal(0, calledTimes); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Control_Should_Log_Binding_Errors_When_No_Ancestor_With_Such_Name() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
{ |
|||
var xaml = @"
|
|||
<Window xmlns='https://github.com/avaloniaui'
|
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
xmlns:local='clr-namespace:Avalonia.Base.UnitTests.Logging;assembly=Avalonia.UnitTests'> |
|||
<Panel> |
|||
<Rectangle Fill='{Binding $parent[Grid].Background}'/> |
|||
</Panel> |
|||
</Window>";
|
|||
var calledTimes = 0; |
|||
using var logSink = TestLogSink.Start((l, a, s, m, d) => |
|||
{ |
|||
if (l >= Avalonia.Logging.LogEventLevel.Warning && s is Rectangle) |
|||
{ |
|||
calledTimes++; |
|||
} |
|||
}); |
|||
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
window.ApplyTemplate(); |
|||
window.Presenter.ApplyTemplate(); |
|||
Assert.Equal(1, calledTimes); |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,990 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Reactive.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls.Presenters; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Data; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.UnitTests; |
|||
using Moq; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Controls.UnitTests |
|||
{ |
|||
public class MaskedTextBoxTests |
|||
{ |
|||
[Fact] |
|||
public void Opening_Context_Menu_Does_not_Lose_Selection() |
|||
{ |
|||
using (Start(FocusServices)) |
|||
{ |
|||
var target1 = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "1234", |
|||
ContextMenu = new TestContextMenu() |
|||
}; |
|||
|
|||
var target2 = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "5678" |
|||
}; |
|||
|
|||
var sp = new StackPanel(); |
|||
sp.Children.Add(target1); |
|||
sp.Children.Add(target2); |
|||
|
|||
target1.ApplyTemplate(); |
|||
target2.ApplyTemplate(); |
|||
|
|||
var root = new TestRoot() { Child = sp }; |
|||
|
|||
target1.SelectionStart = 0; |
|||
target1.SelectionEnd = 3; |
|||
|
|||
target1.Focus(); |
|||
Assert.False(target2.IsFocused); |
|||
Assert.True(target1.IsFocused); |
|||
|
|||
target2.Focus(); |
|||
|
|||
Assert.Equal("123", target1.SelectedText); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Opening_Context_Flyout_Does_not_Lose_Selection() |
|||
{ |
|||
using (Start(FocusServices)) |
|||
{ |
|||
var target1 = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "1234", |
|||
ContextFlyout = new MenuFlyout |
|||
{ |
|||
Items = new List<MenuItem> |
|||
{ |
|||
new MenuItem { Header = "Item 1" }, |
|||
new MenuItem {Header = "Item 2" }, |
|||
new MenuItem {Header = "Item 3" } |
|||
} |
|||
} |
|||
}; |
|||
|
|||
|
|||
target1.ApplyTemplate(); |
|||
|
|||
var root = new TestRoot() { Child = target1 }; |
|||
|
|||
target1.SelectionStart = 0; |
|||
target1.SelectionEnd = 3; |
|||
|
|||
target1.Focus(); |
|||
Assert.True(target1.IsFocused); |
|||
|
|||
target1.ContextFlyout.ShowAt(target1); |
|||
|
|||
Assert.Equal("123", target1.SelectedText); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void DefaultBindingMode_Should_Be_TwoWay() |
|||
{ |
|||
Assert.Equal( |
|||
BindingMode.TwoWay, |
|||
TextBox.TextProperty.GetMetadata(typeof(MaskedTextBox)).DefaultBindingMode); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CaretIndex_Can_Moved_To_Position_After_The_End_Of_Text_With_Arrow_Key() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "1234" |
|||
}; |
|||
|
|||
target.CaretIndex = 3; |
|||
RaiseKeyEvent(target, Key.Right, 0); |
|||
|
|||
Assert.Equal(4, target.CaretIndex); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Press_Ctrl_A_Select_All_Text() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "1234" |
|||
}; |
|||
|
|||
RaiseKeyEvent(target, Key.A, KeyModifiers.Control); |
|||
|
|||
Assert.Equal(0, target.SelectionStart); |
|||
Assert.Equal(4, target.SelectionEnd); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Press_Ctrl_A_Select_All_Null_Text() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate() |
|||
}; |
|||
|
|||
RaiseKeyEvent(target, Key.A, KeyModifiers.Control); |
|||
|
|||
Assert.Equal(0, target.SelectionStart); |
|||
Assert.Equal(0, target.SelectionEnd); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Press_Ctrl_Z_Will_Not_Modify_Text() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "1234" |
|||
}; |
|||
|
|||
RaiseKeyEvent(target, Key.Z, KeyModifiers.Control); |
|||
|
|||
Assert.Equal("1234", target.Text); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var source = new Class1(); |
|||
var target = new MaskedTextBox |
|||
{ |
|||
DataContext = source, |
|||
Template = CreateTemplate(), |
|||
}; |
|||
|
|||
target.ApplyTemplate(); |
|||
target.Bind(TextBox.TextProperty, new Binding(nameof(Class1.Foo), BindingMode.TwoWay)); |
|||
|
|||
Assert.Equal("0", target.Text); |
|||
|
|||
target.CaretIndex = 1; |
|||
target.RaiseEvent(new TextInputEventArgs |
|||
{ |
|||
RoutedEvent = InputElement.TextInputEvent, |
|||
Text = "2", |
|||
}); |
|||
|
|||
Assert.Equal("02", target.Text); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
MaskedTextBox textBox = new MaskedTextBox |
|||
{ |
|||
Text = "First Second Third Fourth", |
|||
CaretIndex = 5 |
|||
}; |
|||
|
|||
// (First| Second Third Fourth)
|
|||
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); |
|||
Assert.Equal(" Second Third Fourth", textBox.Text); |
|||
|
|||
// ( Second |Third Fourth)
|
|||
textBox.CaretIndex = 8; |
|||
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); |
|||
Assert.Equal(" Third Fourth", textBox.Text); |
|||
|
|||
// ( Thi|rd Fourth)
|
|||
textBox.CaretIndex = 4; |
|||
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); |
|||
Assert.Equal(" rd Fourth", textBox.Text); |
|||
|
|||
// ( rd F[ou]rth)
|
|||
textBox.SelectionStart = 5; |
|||
textBox.SelectionEnd = 7; |
|||
|
|||
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); |
|||
Assert.Equal(" rd Frth", textBox.Text); |
|||
|
|||
// ( |rd Frth)
|
|||
textBox.CaretIndex = 1; |
|||
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control); |
|||
Assert.Equal("rd Frth", textBox.Text); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Control_Delete_Should_Remove_The_Word_After_The_Caret_If_There_Is_No_Selection() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var textBox = new MaskedTextBox |
|||
{ |
|||
Text = "First Second Third Fourth", |
|||
CaretIndex = 19 |
|||
}; |
|||
|
|||
// (First Second Third |Fourth)
|
|||
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); |
|||
Assert.Equal("First Second Third ", textBox.Text); |
|||
|
|||
// (First Second |Third )
|
|||
textBox.CaretIndex = 13; |
|||
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); |
|||
Assert.Equal("First Second ", textBox.Text); |
|||
|
|||
// (First Sec|ond )
|
|||
textBox.CaretIndex = 9; |
|||
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); |
|||
Assert.Equal("First Sec", textBox.Text); |
|||
|
|||
// (Fi[rs]t Sec )
|
|||
textBox.SelectionStart = 2; |
|||
textBox.SelectionEnd = 4; |
|||
|
|||
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); |
|||
Assert.Equal("Fit Sec", textBox.Text); |
|||
|
|||
// (Fit Sec| )
|
|||
textBox.Text += " "; |
|||
textBox.CaretIndex = 7; |
|||
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control); |
|||
Assert.Equal("Fit Sec", textBox.Text); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Setting_SelectionStart_To_SelectionEnd_Sets_CaretPosition_To_SelectionStart() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var textBox = new MaskedTextBox |
|||
{ |
|||
Text = "0123456789" |
|||
}; |
|||
|
|||
textBox.SelectionStart = 2; |
|||
textBox.SelectionEnd = 2; |
|||
Assert.Equal(2, textBox.CaretIndex); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Setting_Text_Updates_CaretPosition() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Text = "Initial Text", |
|||
CaretIndex = 11 |
|||
}; |
|||
|
|||
var invoked = false; |
|||
|
|||
target.GetObservable(TextBox.TextProperty).Skip(1).Subscribe(_ => |
|||
{ |
|||
// Caret index should be set before Text changed notification, as we don't want
|
|||
// to notify with an invalid CaretIndex.
|
|||
Assert.Equal(7, target.CaretIndex); |
|||
invoked = true; |
|||
}); |
|||
|
|||
target.Text = "Changed"; |
|||
|
|||
Assert.True(invoked); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Press_Enter_Does_Not_Accept_Return() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
AcceptsReturn = false, |
|||
Text = "1234" |
|||
}; |
|||
|
|||
RaiseKeyEvent(target, Key.Enter, 0); |
|||
|
|||
Assert.Equal("1234", target.Text); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Press_Enter_Add_Default_Newline() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
AcceptsReturn = true |
|||
}; |
|||
|
|||
RaiseKeyEvent(target, Key.Enter, 0); |
|||
|
|||
Assert.Equal(Environment.NewLine, target.Text); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("00/00/0000", "12102000", "12/10/2000")] |
|||
[InlineData("LLLL", "дбs", "____")] |
|||
[InlineData("AA", "Ü1", "__")] |
|||
public void AsciiOnly_Should_Not_Accept_Non_Ascii(string mask, string textEventArg, string expected) |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Mask = mask, |
|||
AsciiOnly = true |
|||
}; |
|||
|
|||
RaiseTextEvent(target, textEventArg); |
|||
|
|||
Assert.Equal(expected, target.Text); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Programmatically_Set_Text_Should_Not_Be_Removed_On_Key_Press() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Mask = "00:00:00.000", |
|||
Text = "12:34:56.000" |
|||
}; |
|||
|
|||
target.CaretIndex = target.Text.Length; |
|||
RaiseKeyEvent(target, Key.Back, 0); |
|||
|
|||
Assert.Equal("12:34:56.00_", target.Text); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Invalid_Programmatically_Set_Text_Should_Be_Rejected() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Mask = "00:00:00.000", |
|||
Text = "12:34:560000" |
|||
}; |
|||
|
|||
Assert.Equal("__:__:__.___", target.Text); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("00/00/0000", "12102000", "**/**/****")] |
|||
[InlineData("LLLL", "дбs", "***_")] |
|||
[InlineData("AA#00", "S2 33", "**_**")] |
|||
public void PasswordChar_Should_Hide_User_Input(string mask, string textEventArg, string expected) |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Mask = mask, |
|||
PasswordChar = '*' |
|||
}; |
|||
|
|||
RaiseTextEvent(target, textEventArg); |
|||
|
|||
Assert.Equal(expected, target.Text); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("00/00/0000", "12102000", "12/10/2000")] |
|||
[InlineData("LLLL", "дбs", "дбs_")] |
|||
[InlineData("AA#00", "S2 33", "S2_33")] |
|||
public void Mask_Should_Work_Correctly(string mask, string textEventArg, string expected) |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Mask = mask |
|||
}; |
|||
|
|||
RaiseTextEvent(target, textEventArg); |
|||
|
|||
Assert.Equal(expected, target.Text); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Press_Enter_Add_Custom_Newline() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
AcceptsReturn = true, |
|||
NewLine = "Test" |
|||
}; |
|||
|
|||
RaiseKeyEvent(target, Key.Enter, 0); |
|||
|
|||
Assert.Equal("Test", target.Text); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(new object[] { false, TextWrapping.NoWrap, ScrollBarVisibility.Hidden })] |
|||
[InlineData(new object[] { false, TextWrapping.Wrap, ScrollBarVisibility.Disabled })] |
|||
[InlineData(new object[] { true, TextWrapping.NoWrap, ScrollBarVisibility.Auto })] |
|||
[InlineData(new object[] { true, TextWrapping.Wrap, ScrollBarVisibility.Disabled })] |
|||
public void Has_Correct_Horizontal_ScrollBar_Visibility( |
|||
bool acceptsReturn, |
|||
TextWrapping wrapping, |
|||
ScrollBarVisibility expected) |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
AcceptsReturn = acceptsReturn, |
|||
TextWrapping = wrapping, |
|||
}; |
|||
|
|||
Assert.Equal(expected, ScrollViewer.GetHorizontalScrollBarVisibility(target)); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void SelectionEnd_Doesnt_Cause_Exception() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "0123456789" |
|||
}; |
|||
|
|||
target.SelectionStart = 0; |
|||
target.SelectionEnd = 9; |
|||
|
|||
target.Text = "123"; |
|||
|
|||
RaiseTextEvent(target, "456"); |
|||
|
|||
Assert.True(true); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void SelectionStart_Doesnt_Cause_Exception() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "0123456789" |
|||
}; |
|||
|
|||
target.SelectionStart = 8; |
|||
target.SelectionEnd = 9; |
|||
|
|||
target.Text = "123"; |
|||
|
|||
RaiseTextEvent(target, "456"); |
|||
|
|||
Assert.True(true); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void SelectionStartEnd_Are_Valid_AterTextChange() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "0123456789" |
|||
}; |
|||
|
|||
target.SelectionStart = 8; |
|||
target.SelectionEnd = 9; |
|||
|
|||
target.Text = "123"; |
|||
|
|||
Assert.True(target.SelectionStart <= "123".Length); |
|||
Assert.True(target.SelectionEnd <= "123".Length); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void SelectedText_Changes_OnSelectionChange() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "0123456789" |
|||
}; |
|||
|
|||
Assert.True(target.SelectedText == ""); |
|||
|
|||
target.SelectionStart = 2; |
|||
target.SelectionEnd = 4; |
|||
|
|||
Assert.True(target.SelectedText == "23"); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void SelectedText_EditsText() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "0123" |
|||
}; |
|||
|
|||
target.SelectedText = "AA"; |
|||
Assert.True(target.Text == "AA0123"); |
|||
|
|||
target.SelectionStart = 1; |
|||
target.SelectionEnd = 3; |
|||
target.SelectedText = "BB"; |
|||
|
|||
Assert.True(target.Text == "ABB123"); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void SelectedText_CanClearText() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "0123" |
|||
}; |
|||
target.SelectionStart = 1; |
|||
target.SelectionEnd = 3; |
|||
target.SelectedText = ""; |
|||
|
|||
Assert.True(target.Text == "03"); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void SelectedText_NullClearsText() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "0123" |
|||
}; |
|||
target.SelectionStart = 1; |
|||
target.SelectionEnd = 3; |
|||
target.SelectedText = null; |
|||
|
|||
Assert.True(target.Text == "03"); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "0123456789\r" |
|||
}; |
|||
target.CaretIndex = 11; |
|||
|
|||
Assert.True(true); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(Key.Up)] |
|||
[InlineData(Key.Down)] |
|||
[InlineData(Key.Home)] |
|||
[InlineData(Key.End)] |
|||
public void Textbox_doesnt_crash_when_Receives_input_and_template_not_applied(Key key) |
|||
{ |
|||
using (Start(FocusServices)) |
|||
{ |
|||
var target1 = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "1234", |
|||
IsVisible = false |
|||
}; |
|||
|
|||
var root = new TestRoot { Child = target1 }; |
|||
|
|||
target1.Focus(); |
|||
Assert.True(target1.IsFocused); |
|||
|
|||
RaiseKeyEvent(target1, key, KeyModifiers.None); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void TextBox_GotFocus_And_LostFocus_Work_Properly() |
|||
{ |
|||
using (Start(FocusServices)) |
|||
{ |
|||
var target1 = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "1234" |
|||
}; |
|||
var target2 = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "5678" |
|||
}; |
|||
var sp = new StackPanel(); |
|||
sp.Children.Add(target1); |
|||
sp.Children.Add(target2); |
|||
|
|||
target1.ApplyTemplate(); |
|||
target2.ApplyTemplate(); |
|||
|
|||
var root = new TestRoot { Child = sp }; |
|||
|
|||
var gfcount = 0; |
|||
var lfcount = 0; |
|||
|
|||
target1.GotFocus += (s, e) => gfcount++; |
|||
target2.LostFocus += (s, e) => lfcount++; |
|||
|
|||
target2.Focus(); |
|||
Assert.False(target1.IsFocused); |
|||
Assert.True(target2.IsFocused); |
|||
|
|||
target1.Focus(); |
|||
Assert.False(target2.IsFocused); |
|||
Assert.True(target1.IsFocused); |
|||
|
|||
Assert.Equal(1, gfcount); |
|||
Assert.Equal(1, lfcount); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void TextBox_CaretIndex_Persists_When_Focus_Lost() |
|||
{ |
|||
using (Start(FocusServices)) |
|||
{ |
|||
var target1 = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "1234" |
|||
}; |
|||
var target2 = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "5678" |
|||
}; |
|||
var sp = new StackPanel(); |
|||
sp.Children.Add(target1); |
|||
sp.Children.Add(target2); |
|||
|
|||
target1.ApplyTemplate(); |
|||
target2.ApplyTemplate(); |
|||
|
|||
var root = new TestRoot { Child = sp }; |
|||
|
|||
target2.Focus(); |
|||
target2.CaretIndex = 2; |
|||
Assert.False(target1.IsFocused); |
|||
Assert.True(target2.IsFocused); |
|||
|
|||
target1.Focus(); |
|||
|
|||
Assert.Equal(2, target2.CaretIndex); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void TextBox_Reveal_Password_Reset_When_Lost_Focus() |
|||
{ |
|||
using (Start(FocusServices)) |
|||
{ |
|||
var target1 = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "1234", |
|||
PasswordChar = '*' |
|||
}; |
|||
var target2 = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "5678" |
|||
}; |
|||
var sp = new StackPanel(); |
|||
sp.Children.Add(target1); |
|||
sp.Children.Add(target2); |
|||
|
|||
target1.ApplyTemplate(); |
|||
target2.ApplyTemplate(); |
|||
|
|||
var root = new TestRoot { Child = sp }; |
|||
|
|||
target1.Focus(); |
|||
target1.RevealPassword = true; |
|||
|
|||
target2.Focus(); |
|||
|
|||
Assert.False(target1.RevealPassword); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Setting_Bound_Text_To_Null_Works() |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var source = new Class1 { Bar = "bar" }; |
|||
var target = new MaskedTextBox { DataContext = source }; |
|||
|
|||
target.Bind(TextBox.TextProperty, new Binding("Bar")); |
|||
|
|||
Assert.Equal("bar", target.Text); |
|||
source.Bar = null; |
|||
Assert.Null(target.Text); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("abc", "d", 3, 0, 0, false, "abc")] |
|||
[InlineData("abc", "dd", 4, 3, 3, false, "abcd")] |
|||
[InlineData("abc", "ddd", 3, 0, 2, true, "ddc")] |
|||
[InlineData("abc", "dddd", 4, 1, 3, true, "addd")] |
|||
[InlineData("abc", "ddddd", 5, 3, 3, true, "abcdd")] |
|||
public void MaxLength_Works_Properly( |
|||
string initalText, |
|||
string textInput, |
|||
int maxLength, |
|||
int selectionStart, |
|||
int selectionEnd, |
|||
bool fromClipboard, |
|||
string expected) |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = initalText, |
|||
MaxLength = maxLength, |
|||
SelectionStart = selectionStart, |
|||
SelectionEnd = selectionEnd |
|||
}; |
|||
|
|||
if (fromClipboard) |
|||
{ |
|||
AvaloniaLocator.CurrentMutable.Bind<IClipboard>().ToSingleton<ClipboardStub>(); |
|||
|
|||
var clipboard = AvaloniaLocator.CurrentMutable.GetService<IClipboard>(); |
|||
clipboard.SetTextAsync(textInput).GetAwaiter().GetResult(); |
|||
|
|||
RaiseKeyEvent(target, Key.V, KeyModifiers.Control); |
|||
clipboard.ClearAsync().GetAwaiter().GetResult(); |
|||
} |
|||
else |
|||
{ |
|||
RaiseTextEvent(target, textInput); |
|||
} |
|||
|
|||
Assert.Equal(expected, target.Text); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(Key.X, KeyModifiers.Control)] |
|||
[InlineData(Key.Back, KeyModifiers.None)] |
|||
[InlineData(Key.Delete, KeyModifiers.None)] |
|||
[InlineData(Key.Tab, KeyModifiers.None)] |
|||
[InlineData(Key.Enter, KeyModifiers.None)] |
|||
public void Keys_Allow_Undo(Key key, KeyModifiers modifiers) |
|||
{ |
|||
using (Start()) |
|||
{ |
|||
var target = new MaskedTextBox |
|||
{ |
|||
Template = CreateTemplate(), |
|||
Text = "0123", |
|||
AcceptsReturn = true, |
|||
AcceptsTab = true |
|||
}; |
|||
target.SelectionStart = 1; |
|||
target.SelectionEnd = 3; |
|||
AvaloniaLocator.CurrentMutable |
|||
.Bind<Input.Platform.IClipboard>().ToSingleton<ClipboardStub>(); |
|||
|
|||
RaiseKeyEvent(target, key, modifiers); |
|||
RaiseKeyEvent(target, Key.Z, KeyModifiers.Control); // undo
|
|||
Assert.True(target.Text == "0123"); |
|||
} |
|||
} |
|||
|
|||
private static TestServices FocusServices => TestServices.MockThreadingInterface.With( |
|||
focusManager: new FocusManager(), |
|||
keyboardDevice: () => new KeyboardDevice(), |
|||
keyboardNavigation: new KeyboardNavigationHandler(), |
|||
inputManager: new InputManager(), |
|||
renderInterface: new MockPlatformRenderInterface(), |
|||
fontManagerImpl: new MockFontManagerImpl(), |
|||
textShaperImpl: new MockTextShaperImpl(), |
|||
standardCursorFactory: Mock.Of<ICursorFactory>()); |
|||
|
|||
private static TestServices Services => TestServices.MockThreadingInterface.With( |
|||
standardCursorFactory: Mock.Of<ICursorFactory>()); |
|||
|
|||
private IControlTemplate CreateTemplate() |
|||
{ |
|||
return new FuncControlTemplate<MaskedTextBox>((control, scope) => |
|||
new TextPresenter |
|||
{ |
|||
Name = "PART_TextPresenter", |
|||
[!!TextPresenter.TextProperty] = new Binding |
|||
{ |
|||
Path = "Text", |
|||
Mode = BindingMode.TwoWay, |
|||
Priority = BindingPriority.TemplatedParent, |
|||
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), |
|||
}, |
|||
}.RegisterInNameScope(scope)); |
|||
} |
|||
|
|||
private void RaiseKeyEvent(MaskedTextBox textBox, Key key, KeyModifiers inputModifiers) |
|||
{ |
|||
textBox.RaiseEvent(new KeyEventArgs |
|||
{ |
|||
RoutedEvent = InputElement.KeyDownEvent, |
|||
KeyModifiers = inputModifiers, |
|||
Key = key |
|||
}); |
|||
} |
|||
|
|||
private void RaiseTextEvent(MaskedTextBox textBox, string text) |
|||
{ |
|||
textBox.RaiseEvent(new TextInputEventArgs |
|||
{ |
|||
RoutedEvent = InputElement.TextInputEvent, |
|||
Text = text |
|||
}); |
|||
} |
|||
|
|||
private static IDisposable Start(TestServices services = null) |
|||
{ |
|||
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US"); |
|||
return UnitTestApplication.Start(services ?? Services); |
|||
} |
|||
|
|||
private class Class1 : NotifyingBase |
|||
{ |
|||
private int _foo; |
|||
private string _bar; |
|||
|
|||
public int Foo |
|||
{ |
|||
get { return _foo; } |
|||
set { _foo = value; RaisePropertyChanged(); } |
|||
} |
|||
|
|||
public string Bar |
|||
{ |
|||
get { return _bar; } |
|||
set { _bar = value; RaisePropertyChanged(); } |
|||
} |
|||
} |
|||
|
|||
private class ClipboardStub : IClipboard // in order to get tests working that use the clipboard
|
|||
{ |
|||
private string _text; |
|||
|
|||
public Task<string> GetTextAsync() => Task.FromResult(_text); |
|||
|
|||
public Task SetTextAsync(string text) |
|||
{ |
|||
_text = text; |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public Task ClearAsync() |
|||
{ |
|||
_text = null; |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; |
|||
|
|||
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>()); |
|||
|
|||
public Task<object> GetDataAsync(string format) => Task.FromResult((object)null); |
|||
} |
|||
|
|||
private class TestContextMenu : ContextMenu |
|||
{ |
|||
public TestContextMenu() |
|||
{ |
|||
IsOpen = true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,89 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Shapes; |
|||
using Avalonia.Media; |
|||
using Xunit; |
|||
|
|||
#if AVALONIA_SKIA
|
|||
namespace Avalonia.Skia.RenderTests |
|||
#else
|
|||
namespace Avalonia.Direct2D1.RenderTests.Media |
|||
#endif
|
|||
{ |
|||
public class CombinedGeometryTests : TestBase |
|||
{ |
|||
public CombinedGeometryTests() |
|||
: base(@"Media\CombinedGeometry") |
|||
{ |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(Avalonia.Media.GeometryCombineMode.Union)] |
|||
[InlineData(Avalonia.Media.GeometryCombineMode.Intersect)] |
|||
[InlineData(Avalonia.Media.GeometryCombineMode.Xor)] |
|||
[InlineData(Avalonia.Media.GeometryCombineMode.Exclude)] |
|||
public async Task GeometryCombineMode(GeometryCombineMode mode) |
|||
{ |
|||
var target = new Border |
|||
{ |
|||
Width = 200, |
|||
Height = 200, |
|||
Background = Brushes.White, |
|||
Child = new Path |
|||
{ |
|||
Data = new CombinedGeometry |
|||
{ |
|||
GeometryCombineMode = mode, |
|||
Geometry1 = new RectangleGeometry(new Rect(25, 25, 100, 100)), |
|||
Geometry2 = new EllipseGeometry |
|||
{ |
|||
Center = new Point(125, 125), |
|||
RadiusX = 50, |
|||
RadiusY = 50, |
|||
} |
|||
}, |
|||
Fill = Brushes.Blue, |
|||
Stroke = Brushes.Red, |
|||
StrokeThickness = 1, |
|||
} |
|||
}; |
|||
|
|||
var testName = $"{nameof(GeometryCombineMode)}_{mode}"; |
|||
await RenderToFile(target, testName); |
|||
CompareImages(testName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Geometry1_Transform() |
|||
{ |
|||
var target = new Border |
|||
{ |
|||
Width = 200, |
|||
Height = 200, |
|||
Background = Brushes.White, |
|||
Child = new Path |
|||
{ |
|||
Data = new CombinedGeometry |
|||
{ |
|||
Geometry1 = new RectangleGeometry(new Rect(25, 25, 100, 100)) |
|||
{ |
|||
Transform = new RotateTransform(45, 75, 75) |
|||
}, |
|||
Geometry2 = new EllipseGeometry |
|||
{ |
|||
Center = new Point(125, 125), |
|||
RadiusX = 50, |
|||
RadiusY = 50, |
|||
} |
|||
}, |
|||
Fill = Brushes.Blue, |
|||
Stroke = Brushes.Red, |
|||
StrokeThickness = 1, |
|||
} |
|||
}; |
|||
|
|||
await RenderToFile(target); |
|||
CompareImages(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Shapes; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Imaging; |
|||
using Xunit; |
|||
|
|||
#if AVALONIA_SKIA
|
|||
namespace Avalonia.Skia.RenderTests |
|||
#else
|
|||
namespace Avalonia.Direct2D1.RenderTests.Media |
|||
#endif
|
|||
{ |
|||
public class GeometryGroupTests : TestBase |
|||
{ |
|||
public GeometryGroupTests() |
|||
: base(@"Media\GeometryGroup") |
|||
{ |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(FillRule.EvenOdd)] |
|||
[InlineData(FillRule.NonZero)] |
|||
public async Task FillRule_Stroke(FillRule fillRule) |
|||
{ |
|||
var target = new Border |
|||
{ |
|||
Width = 200, |
|||
Height = 200, |
|||
Background = Brushes.White, |
|||
Child = new Path |
|||
{ |
|||
Data = new GeometryGroup |
|||
{ |
|||
FillRule = fillRule, |
|||
Children = |
|||
{ |
|||
new RectangleGeometry(new Rect(25, 25, 100, 100)), |
|||
new EllipseGeometry |
|||
{ |
|||
Center = new Point(125, 125), |
|||
RadiusX = 50, |
|||
RadiusY = 50, |
|||
}, |
|||
} |
|||
}, |
|||
Fill = Brushes.Blue, |
|||
Stroke = Brushes.Red, |
|||
StrokeThickness = 1, |
|||
} |
|||
}; |
|||
|
|||
var testName = $"{nameof(FillRule_Stroke)}_{fillRule}"; |
|||
await RenderToFile(target, testName); |
|||
CompareImages(testName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Child_Transform() |
|||
{ |
|||
var target = new Border |
|||
{ |
|||
Width = 200, |
|||
Height = 200, |
|||
Background = Brushes.White, |
|||
Child = new Path |
|||
{ |
|||
Data = new GeometryGroup |
|||
{ |
|||
Children = |
|||
{ |
|||
new RectangleGeometry(new Rect(25, 25, 100, 100)) |
|||
{ |
|||
Transform = new RotateTransform(45, 75, 75) |
|||
}, |
|||
new EllipseGeometry |
|||
{ |
|||
Center = new Point(125, 125), |
|||
RadiusX = 50, |
|||
RadiusY = 50, |
|||
}, |
|||
} |
|||
}, |
|||
Fill = Brushes.Blue, |
|||
Stroke = Brushes.Red, |
|||
StrokeThickness = 1, |
|||
} |
|||
}; |
|||
|
|||
await RenderToFile(target); |
|||
CompareImages(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using Avalonia.Media; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Visuals.UnitTests.Media |
|||
{ |
|||
public class GeometryGroupTests |
|||
{ |
|||
[Fact] |
|||
public void Children_Should_Have_Initial_Collection() |
|||
{ |
|||
var target = new GeometryGroup(); |
|||
|
|||
Assert.NotNull(target.Children); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Children_Can_Be_Set_To_Null() |
|||
{ |
|||
var target = new GeometryGroup(); |
|||
|
|||
target.Children = null; |
|||
|
|||
Assert.Null(target.Children); |
|||
} |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |