csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
448 lines
16 KiB
448 lines
16 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
|
|
namespace Avalonia.Controls
|
|
{
|
|
public class MaskedTextBox : TextBox
|
|
{
|
|
public static readonly StyledProperty<bool> AsciiOnlyProperty =
|
|
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(AsciiOnly));
|
|
|
|
public static readonly StyledProperty<CultureInfo?> CultureProperty =
|
|
AvaloniaProperty.Register<MaskedTextBox, CultureInfo?>(nameof(Culture), 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 readonly StyledProperty<char> PromptCharProperty =
|
|
AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PromptChar), '_', coerce: CoercePromptChar);
|
|
|
|
public static readonly StyledProperty<bool> ResetOnPromptProperty =
|
|
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(ResetOnPrompt), true);
|
|
|
|
public static readonly StyledProperty<bool> ResetOnSpaceProperty =
|
|
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(ResetOnSpace), true);
|
|
|
|
private bool _ignoreTextChanges;
|
|
|
|
static MaskedTextBox()
|
|
{
|
|
PasswordCharProperty.OverrideMetadata<MaskedTextBox>(new('\0', coerce: CoercePasswordChar));
|
|
}
|
|
|
|
private static char CoercePasswordChar(AvaloniaObject sender, char baseValue)
|
|
{
|
|
if (!MaskedTextProvider.IsValidPasswordChar(baseValue))
|
|
{
|
|
throw new ArgumentException($"'{baseValue}' is not a valid value for PasswordChar.");
|
|
}
|
|
var textbox = (MaskedTextBox)sender;
|
|
if (textbox.MaskProvider is { } maskProvider && baseValue == maskProvider.PromptChar)
|
|
{
|
|
// Prompt and password chars must be different.
|
|
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
|
|
}
|
|
|
|
return baseValue;
|
|
}
|
|
|
|
private static char CoercePromptChar(AvaloniaObject sender, char baseValue)
|
|
{
|
|
if (!MaskedTextProvider.IsValidInputChar(baseValue))
|
|
{
|
|
throw new ArgumentException($"'{baseValue}' is not a valid value for PromptChar.");
|
|
}
|
|
if (baseValue == sender.GetValue(PasswordCharProperty))
|
|
{
|
|
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
|
|
}
|
|
|
|
return baseValue;
|
|
}
|
|
|
|
public MaskedTextBox() { }
|
|
|
|
/// <summary>
|
|
/// Constructs the MaskedTextBox with the specified MaskedTextProvider object.
|
|
/// </summary>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty",
|
|
"AVP1012:An AvaloniaObject should use SetCurrentValue when assigning its own StyledProperty or AttachedProperty values",
|
|
Justification = "These values are being explicitly provided by a constructor parameter.")]
|
|
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 => GetValue(CultureProperty);
|
|
set => SetValue(CultureProperty, 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 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 => GetValue(ResetOnPromptProperty);
|
|
set => SetValue(ResetOnPromptProperty, 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 => GetValue(ResetOnSpaceProperty);
|
|
set => SetValue(ResetOnSpaceProperty, value);
|
|
}
|
|
|
|
protected override Type StyleKeyOverride => typeof(TextBox);
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnGotFocus(GotFocusEventArgs e)
|
|
{
|
|
if (HidePromptOnLeave == true && MaskProvider != null)
|
|
{
|
|
SetCurrentValue(TextProperty, MaskProvider.ToDisplayString());
|
|
}
|
|
base.OnGotFocus(e);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async void OnKeyDown(KeyEventArgs e)
|
|
{
|
|
if (MaskProvider == null)
|
|
{
|
|
base.OnKeyDown(e);
|
|
return;
|
|
}
|
|
|
|
var keymap = Application.Current!.PlatformSettings?.HotkeyConfiguration;
|
|
|
|
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
|
|
|
|
if (keymap is not null && Match(keymap.Paste))
|
|
{
|
|
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
|
|
|
|
if (clipboard is null)
|
|
return;
|
|
|
|
string? text = null;
|
|
try
|
|
{
|
|
text = await clipboard.GetTextAsync();
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
// Silently ignore.
|
|
}
|
|
|
|
if (text == null)
|
|
return;
|
|
|
|
foreach (var item in text)
|
|
{
|
|
var index = GetNextCharacterPosition(CaretIndex);
|
|
if (MaskProvider.InsertAt(item, index))
|
|
{
|
|
SetCurrentValue(CaretIndexProperty, ++index);
|
|
}
|
|
}
|
|
|
|
SetCurrentValue(TextProperty, 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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnLostFocus(RoutedEventArgs e)
|
|
{
|
|
if (HidePromptOnLeave && MaskProvider != null)
|
|
{
|
|
SetCurrentValue(TextProperty, MaskProvider.ToString(!HidePromptOnLeave, true));
|
|
}
|
|
base.OnLostFocus(e);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs 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 == 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 (MaskProvider != null && MaskProvider.PasswordChar != PasswordChar)
|
|
{
|
|
UpdateMaskProvider();
|
|
}
|
|
}
|
|
else if (change.Property == PromptCharProperty)
|
|
{
|
|
if (MaskProvider != null && MaskProvider.PromptChar != PromptChar)
|
|
{
|
|
UpdateMaskProvider();
|
|
}
|
|
}
|
|
else if (change.Property == ResetOnPromptProperty)
|
|
{
|
|
if (MaskProvider != null && change.GetNewValue<bool>() is { } newValue)
|
|
{
|
|
MaskProvider.ResetOnPrompt = newValue;
|
|
}
|
|
}
|
|
else if (change.Property == ResetOnSpaceProperty)
|
|
{
|
|
if (MaskProvider != null && change.GetNewValue<bool>() is { } newValue)
|
|
{
|
|
MaskProvider.ResetOnSpace = newValue;
|
|
}
|
|
}
|
|
else if (change.Property == AsciiOnlyProperty && MaskProvider != null && MaskProvider.AsciiOnly != AsciiOnly
|
|
|| change.Property == CultureProperty && MaskProvider != null && !MaskProvider.Culture.Equals(Culture))
|
|
{
|
|
UpdateMaskProvider();
|
|
}
|
|
base.OnPropertyChanged(change);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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)
|
|
{
|
|
SetCurrentValue(CaretIndexProperty, GetNextCharacterPosition(CaretIndex));
|
|
|
|
if (MaskProvider.InsertAt(e.Text!, CaretIndex))
|
|
{
|
|
CaretIndex++;
|
|
}
|
|
var nextPos = GetNextCharacterPosition(CaretIndex);
|
|
if (nextPos != 0 && CaretIndex != Text.Length)
|
|
{
|
|
SetCurrentValue(CaretIndexProperty, 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)
|
|
{
|
|
SetCurrentValue(TextProperty, provider.ToDisplayString());
|
|
SetCurrentValue(CaretIndexProperty, position);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override string? CoerceText(string? text)
|
|
{
|
|
if (!_ignoreTextChanges && MaskProvider is { } maskProvider)
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
maskProvider.Clear();
|
|
else
|
|
maskProvider.Set(text);
|
|
|
|
text = maskProvider.ToDisplayString();
|
|
}
|
|
|
|
return base.CoerceText(text);
|
|
}
|
|
}
|
|
}
|
|
|