committed by
GitHub
3 changed files with 1424 additions and 0 deletions
@ -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,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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
Loading…
Reference in new issue