@ -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 |