diff --git a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml new file mode 100644 index 0000000..ecc21d6 --- /dev/null +++ b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs new file mode 100644 index 0000000..6ac89e7 --- /dev/null +++ b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class VerificationCodeDemo : UserControl +{ + public VerificationCodeDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 5a0cd77..aa9f90a 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -56,6 +56,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(), MenuKeys.MenuKeyThemeToggler => new ThemeTogglerDemoViewModel(), MenuKeys.MenuKeyToolBar => new ToolBarDemoViewModel(), + MenuKeys.MenuKeyVerificationCode => new VerificationCodeDemoViewModel(), }; } } \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index a48238e..1fa6c8a 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -42,7 +42,8 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "Theme Toggler", Key = MenuKeys.MenuKeyThemeToggler, Status = "New" }, new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline, Status = "WIP" }, new() { MenuHeader = "TwoTonePathIcon", Key = MenuKeys.MenuKeyTwoTonePathIcon}, - new() { MenuHeader = "ToolBar", Key = MenuKeys.MenuKeyToolBar, Status = "New" } + new() { MenuHeader = "ToolBar", Key = MenuKeys.MenuKeyToolBar, Status = "New" }, + new() { MenuHeader = "Verification Code", Key = MenuKeys.MenuKeyVerificationCode, Status = "New" }, }; } } @@ -81,5 +82,6 @@ public static class MenuKeys public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon"; public const string MenuKeyThemeToggler = "ThemeToggler"; public const string MenuKeyToolBar = "ToolBar"; + public const string MenuKeyVerificationCode = "VerificationCode"; } \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs new file mode 100644 index 0000000..6f106e3 --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public class VerificationCodeDemoViewModel: ObservableObject +{ + +} \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml b/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml new file mode 100644 index 0000000..74e6993 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index ddb53fd..e79e02f 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -33,5 +33,6 @@ + diff --git a/src/Ursa/Controls/VerificationCode/VerificationCode.cs b/src/Ursa/Controls/VerificationCode/VerificationCode.cs new file mode 100644 index 0000000..efbc150 --- /dev/null +++ b/src/Ursa/Controls/VerificationCode/VerificationCode.cs @@ -0,0 +1,117 @@ +using System.Windows.Input; +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Irihi.Avalonia.Shared.Helpers; + +namespace Ursa.Controls; + +[TemplatePart(PART_ItemsControl, typeof(ItemsControl))] +public class VerificationCode: TemplatedControl +{ + public const string PART_ItemsControl = "PART_ItemsControl"; + private ItemsControl? _itemsControl; + private int _currentIndex = 0; + + public static readonly StyledProperty CompleteCommandProperty = AvaloniaProperty.Register( + nameof(CompleteCommand)); + + public ICommand? CompleteCommand + { + get => GetValue(CompleteCommandProperty); + set => SetValue(CompleteCommandProperty, value); + } + + public static readonly StyledProperty CountOfDigitProperty = AvaloniaProperty.Register( + nameof(CountOfDigit)); + + public int CountOfDigit + { + get => GetValue(CountOfDigitProperty); + set => SetValue(CountOfDigitProperty, value); + } + + public static readonly StyledProperty PasswordCharProperty = + AvaloniaProperty.Register( + nameof(PasswordChar)); + + public char PasswordChar + { + get => GetValue(PasswordCharProperty); + set => SetValue(PasswordCharProperty, value); + } + + public static readonly DirectProperty> DigitsProperty = AvaloniaProperty.RegisterDirect>( + nameof(Digits), o => o.Digits, (o, v) => o.Digits = v); + + private AvaloniaList _digits = []; + internal AvaloniaList Digits + { + get => _digits; + set => SetAndRaise(DigitsProperty, ref _digits, value); + } + + static VerificationCode() + { + CountOfDigitProperty.Changed.AddClassHandler((code, args) => code.OnCountOfDigitChanged(args)); + FocusableProperty.OverrideDefaultValue(true); + } + + public VerificationCode() + { + InputMethod.SetIsInputMethodEnabled(this, false); + } + + private void OnCountOfDigitChanged(AvaloniaPropertyChangedEventArgs args) + { + var newValue = args.NewValue.Value; + if (newValue > 0) + { + Digits = new AvaloniaList(Enumerable.Repeat(string.Empty, newValue)); + } + + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _itemsControl = e.NameScope.Get(PART_ItemsControl); + PointerPressedEvent.AddHandler(OnControlPressed, RoutingStrategies.Tunnel, false, this); + } + + private void OnControlPressed(object sender, PointerPressedEventArgs e) + { + if (e.Source is Control t) + { + var text = t.FindLogicalAncestorOfType(); + if (text != null) + { + _currentIndex = _itemsControl?.IndexFromContainer(text) ?? 0; + } + } + } + + protected override void OnTextInput(TextInputEventArgs e) + { + base.OnTextInput(e); + if (e.Text?.Length == 1 && _currentIndex < CountOfDigit) + { + Digits[_currentIndex] = e.Text; + var presenter = _itemsControl?.ContainerFromIndex(_currentIndex) as TextBox; + if (presenter is null) return; + _currentIndex++; + var newPresenter = _itemsControl?.ContainerFromIndex(_currentIndex)?.Focus(); + presenter.Text = e.Text; + if (_currentIndex == CountOfDigit) + { + CompleteCommand?.Execute(Digits); + } + } + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/VerificationCode/VerificationCodeCollection.cs b/src/Ursa/Controls/VerificationCode/VerificationCodeCollection.cs new file mode 100644 index 0000000..333af44 --- /dev/null +++ b/src/Ursa/Controls/VerificationCode/VerificationCodeCollection.cs @@ -0,0 +1,25 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Media; + +namespace Ursa.Controls; + +public class VerificationCodeCollection: ItemsControl +{ + protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + return NeedsContainer(item, out recycleKey); + } + + protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return new TextBox() + { + TextAlignment = TextAlignment.Center, + [InputMethod.IsInputMethodEnabledProperty] = false, + }; + } +} \ No newline at end of file