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 AsciiOnlyProperty = AvaloniaProperty.Register(nameof(AsciiOnly)); public static readonly StyledProperty CultureProperty = AvaloniaProperty.Register(nameof(Culture), CultureInfo.CurrentCulture); public static readonly StyledProperty HidePromptOnLeaveProperty = AvaloniaProperty.Register(nameof(HidePromptOnLeave)); public static readonly DirectProperty MaskCompletedProperty = AvaloniaProperty.RegisterDirect(nameof(MaskCompleted), o => o.MaskCompleted); public static readonly DirectProperty MaskFullProperty = AvaloniaProperty.RegisterDirect(nameof(MaskFull), o => o.MaskFull); public static readonly StyledProperty MaskProperty = AvaloniaProperty.Register(nameof(Mask), string.Empty); public static readonly StyledProperty PromptCharProperty = AvaloniaProperty.Register(nameof(PromptChar), '_', coerce: CoercePromptChar); public static readonly StyledProperty ResetOnPromptProperty = AvaloniaProperty.Register(nameof(ResetOnPrompt), true); public static readonly StyledProperty ResetOnSpaceProperty = AvaloniaProperty.Register(nameof(ResetOnSpace), true); private bool _ignoreTextChanges; static MaskedTextBox() { PasswordCharProperty.OverrideMetadata(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() { } /// /// Constructs the MaskedTextBox with the specified MaskedTextProvider object. /// [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; } /// /// Gets or sets a value indicating if the masked text box is restricted to accept only ASCII characters. /// Default value is false. /// public bool AsciiOnly { get => GetValue(AsciiOnlyProperty); set => SetValue(AsciiOnlyProperty, value); } /// /// Gets or sets the culture information associated with the masked text box. /// public CultureInfo? Culture { get => GetValue(CultureProperty); set => SetValue(CultureProperty, value); } /// /// Gets or sets a value indicating if the prompt character is hidden when the masked text box loses focus. /// public bool HidePromptOnLeave { get => GetValue(HidePromptOnLeaveProperty); set => SetValue(HidePromptOnLeaveProperty, value); } /// /// Gets or sets the mask to apply to the TextBox. /// public string? Mask { get => GetValue(MaskProperty); set => SetValue(MaskProperty, value); } /// /// Specifies whether the test string required input positions, as specified by the mask, have /// all been assigned. /// public bool? MaskCompleted { get => MaskProvider?.MaskCompleted; } /// /// Specifies whether all inputs (required and optional) have been provided into the mask successfully. /// public bool? MaskFull { get => MaskProvider?.MaskFull; } /// /// Gets the MaskTextProvider for the specified Mask. /// public MaskedTextProvider? MaskProvider { get; private set; } /// /// Gets or sets the character used to represent the absence of user input in MaskedTextBox. /// public char PromptChar { get => GetValue(PromptCharProperty); set => SetValue(PromptCharProperty, value); } /// /// Gets or sets a value indicating if selected characters should be reset when the prompt character is pressed. /// public bool ResetOnPrompt { get => GetValue(ResetOnPromptProperty); set => SetValue(ResetOnPromptProperty, value); } /// /// Gets or sets a value indicating if selected characters should be reset when the space character is pressed. /// public bool ResetOnSpace { get => GetValue(ResetOnSpaceProperty); set => SetValue(ResetOnSpaceProperty, value); } protected override Type StyleKeyOverride => typeof(TextBox); /// protected override void OnGotFocus(GotFocusEventArgs e) { if (HidePromptOnLeave == true && MaskProvider != null) { SetCurrentValue(TextProperty, MaskProvider.ToDisplayString()); } base.OnGotFocus(e); } /// protected override async void OnKeyDown(KeyEventArgs e) { if (MaskProvider == null) { base.OnKeyDown(e); return; } var keymap = Application.Current!.PlatformSettings?.HotkeyConfiguration; bool Match(List 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; } } /// protected override void OnLostFocus(RoutedEventArgs e) { if (HidePromptOnLeave && MaskProvider != null) { SetCurrentValue(TextProperty, MaskProvider.ToString(!HidePromptOnLeave, true)); } base.OnLostFocus(e); } /// 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() is { } newValue) { MaskProvider.ResetOnPrompt = newValue; } } else if (change.Property == ResetOnSpaceProperty) { if (MaskProvider != null && change.GetNewValue() 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); } /// 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); } } /// 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); } } }