diff --git a/demo/Ursa.Demo/Pages/TagInputDemo.axaml b/demo/Ursa.Demo/Pages/TagInputDemo.axaml index dadccf2..730921c 100644 --- a/demo/Ursa.Demo/Pages/TagInputDemo.axaml +++ b/demo/Ursa.Demo/Pages/TagInputDemo.axaml @@ -17,6 +17,7 @@ Margin="20" AllowDuplicates="True" Separator="-" + Watermark="Hello world" Tags="{Binding Tags}" /> + + diff --git a/src/Ursa.Themes.Semi/Controls/TagInput.axaml b/src/Ursa.Themes.Semi/Controls/TagInput.axaml index d79f9a6..f15bf93 100644 --- a/src/Ursa.Themes.Semi/Controls/TagInput.axaml +++ b/src/Ursa.Themes.Semi/Controls/TagInput.axaml @@ -25,6 +25,12 @@ BorderThickness="1" CornerRadius="3"> + + @@ -123,10 +132,10 @@ Foreground="{TemplateBinding Foreground}" /> diff --git a/src/Ursa/Controls/TagInput/TagInput.cs b/src/Ursa/Controls/TagInput/TagInput.cs index 30773d4..4cdba32 100644 --- a/src/Ursa/Controls/TagInput/TagInput.cs +++ b/src/Ursa/Controls/TagInput/TagInput.cs @@ -10,45 +10,89 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Layout; using Avalonia.Styling; +using Irihi.Avalonia.Shared.Common; +using Irihi.Avalonia.Shared.Helpers; namespace Ursa.Controls; [TemplatePart(PART_ItemsControl, typeof(ItemsControl))] +[TemplatePart(PART_Watermark, typeof(Visual))] +[PseudoClasses(PseudoClassName.PC_Empty)] public class TagInput : TemplatedControl { public const string PART_ItemsControl = "PART_ItemsControl"; - - private readonly TextBox _textBox; - private ItemsControl? _itemsControl; - + public const string PART_Watermark = "PART_Watermark"; public static readonly StyledProperty> TagsProperty = AvaloniaProperty.Register>( nameof(Tags)); - public IList Tags + public static readonly StyledProperty WatermarkProperty = TextBox.WatermarkProperty.AddOwner(); + + + public static readonly StyledProperty AcceptsReturnProperty = + TextBox.AcceptsReturnProperty.AddOwner(); + + public bool AcceptsReturn { - get => GetValue(TagsProperty); - set => SetValue(TagsProperty, value); + get => GetValue(AcceptsReturnProperty); + set => SetValue(AcceptsReturnProperty, value); } + public static readonly StyledProperty MaxCountProperty = AvaloniaProperty.Register( + nameof(MaxCount), int.MaxValue); + public static readonly DirectProperty ItemsProperty = AvaloniaProperty.RegisterDirect( nameof(Items), o => o.Items); - private IList _items; + public static readonly StyledProperty InputThemeProperty = + AvaloniaProperty.Register( + nameof(InputTheme)); - public IList Items + public static readonly StyledProperty ItemTemplateProperty = + AvaloniaProperty.Register( + nameof(ItemTemplate)); + + public static readonly StyledProperty SeparatorProperty = AvaloniaProperty.Register( + nameof(Separator)); + + public static readonly StyledProperty LostFocusBehaviorProperty = + AvaloniaProperty.Register( + nameof(LostFocusBehavior)); + + + public static readonly StyledProperty AllowDuplicatesProperty = AvaloniaProperty.Register( + nameof(AllowDuplicates), true); + + public static readonly StyledProperty InnerLeftContentProperty = + AvaloniaProperty.Register( + nameof(InnerLeftContent)); + + public static readonly StyledProperty InnerRightContentProperty = + AvaloniaProperty.Register( + nameof(InnerRightContent)); + + private readonly TextBox _textBox; + + private IList _items = null!; + private ItemsControl? _itemsControl; + + private TextPresenter? _presenter; + private Visual? _watermark; + + + static TagInput() { - get => _items; - private set => SetAndRaise(ItemsProperty, ref _items, value); + InputThemeProperty.Changed.AddClassHandler((o, e) => o.OnInputThemePropertyChanged(e)); + TagsProperty.Changed.AddClassHandler((o, e) => o.OnTagsPropertyChanged(e)); } public TagInput() { _textBox = new TextBox(); + _textBox[!AcceptsReturnProperty] = this.GetObservable(AcceptsReturnProperty).ToBinding(); _textBox.AddHandler(KeyDownEvent, OnTextBoxKeyDown, RoutingStrategies.Tunnel); _textBox.AddHandler(LostFocusEvent, OnTextBox_LostFocus, RoutingStrategies.Bubble); Items = new AvaloniaList @@ -58,22 +102,29 @@ public class TagInput : TemplatedControl Tags = new ObservableCollection(); } - private void OnTextBox_LostFocus(object? sender, RoutedEventArgs e) + public string? Watermark { - switch (LostFocusBehavior) - { - case LostFocusBehavior.Add: - AddTags(); - break; - case LostFocusBehavior.Clear: - _textBox.Text = ""; - break; - } + get => GetValue(WatermarkProperty); + set => SetValue(WatermarkProperty, value); } - public static readonly StyledProperty InputThemeProperty = - AvaloniaProperty.Register( - nameof(InputTheme)); + public IList Tags + { + get => GetValue(TagsProperty); + set => SetValue(TagsProperty, value); + } + + public int MaxCount + { + get => GetValue(MaxCountProperty); + set => SetValue(MaxCountProperty, value); + } + + public IList Items + { + get => _items; + private set => SetAndRaise(ItemsProperty, ref _items, value); + } public ControlTheme InputTheme { @@ -81,91 +132,93 @@ public class TagInput : TemplatedControl set => SetValue(InputThemeProperty, value); } - public static readonly StyledProperty ItemTemplateProperty = - AvaloniaProperty.Register( - nameof(ItemTemplate)); - public IDataTemplate? ItemTemplate { get => GetValue(ItemTemplateProperty); set => SetValue(ItemTemplateProperty, value); } - public static readonly StyledProperty SeparatorProperty = AvaloniaProperty.Register( - nameof(Separator)); - public string Separator { get => GetValue(SeparatorProperty); set => SetValue(SeparatorProperty, value); } - public static readonly StyledProperty LostFocusBehaviorProperty = AvaloniaProperty.Register( - nameof(LostFocusBehavior)); - public LostFocusBehavior LostFocusBehavior { get => GetValue(LostFocusBehaviorProperty); set => SetValue(LostFocusBehaviorProperty, value); } - - public static readonly StyledProperty AllowDuplicatesProperty = AvaloniaProperty.Register( - nameof(AllowDuplicates), defaultValue: true); - public bool AllowDuplicates { get => GetValue(AllowDuplicatesProperty); set => SetValue(AllowDuplicatesProperty, value); } - public static readonly StyledProperty InnerLeftContentProperty = - AvaloniaProperty.Register( - nameof(InnerLeftContent)); - public object? InnerLeftContent { get => GetValue(InnerLeftContentProperty); set => SetValue(InnerLeftContentProperty, value); } - public static readonly StyledProperty InnerRightContentProperty = - AvaloniaProperty.Register( - nameof(InnerRightContent)); - public object? InnerRightContent { get => GetValue(InnerRightContentProperty); set => SetValue(InnerRightContentProperty, value); } - - static TagInput() + private void OnTextBox_LostFocus(object? sender, RoutedEventArgs e) { - InputThemeProperty.Changed.AddClassHandler((o, e) => o.OnInputThemePropertyChanged(e)); - TagsProperty.Changed.AddClassHandler((o, e) => o.OnTagsPropertyChanged(e)); + switch (LostFocusBehavior) + { + case LostFocusBehavior.Add: + AddTags(_textBox.Text); + break; + case LostFocusBehavior.Clear: + _textBox.Text = ""; + break; + } } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); _itemsControl = e.NameScope.Find(PART_ItemsControl); + _watermark = e.NameScope.Find(PART_Watermark); + } + + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + if (_watermark is null) return; + _presenter = _textBox.GetTemplateChildren().OfType().FirstOrDefault(); + _presenter?.GetObservable(TextPresenter.PreeditTextProperty).Subscribe(_ => CheckEmpty()); + _textBox.GetObservable(TextBox.TextProperty).Subscribe(_ => CheckEmpty()); + if (Tags is INotifyCollectionChanged incc) + incc.GetWeakCollectionChangedObservable().Subscribe(_ => CheckEmpty()); } private void OnInputThemePropertyChanged(AvaloniaPropertyChangedEventArgs args) { - var newTheme = args.GetNewValue(); - if (newTheme?.TargetType == typeof(TextBox)) - { - _textBox.Theme = newTheme; - } + var newTheme = args.GetNewValue(); + if (newTheme?.TargetType == typeof(TextBox)) _textBox.Theme = newTheme; + } + + private void CheckEmpty() + { + if (string.IsNullOrWhiteSpace(_presenter?.PreeditText) && string.IsNullOrEmpty(_textBox.Text) && + Tags.Count == 0) + PseudoClasses.Set(PseudoClassName.PC_Empty, true); + else + PseudoClasses.Set(PseudoClassName.PC_Empty, false); } private void OnTagsPropertyChanged(AvaloniaPropertyChangedEventArgs args) { - var newTags = args.GetNewValue>(); - var oldTags = args.GetOldValue>(); - + var newTags = args.GetNewValue?>(); + var oldTags = args.GetOldValue?>(); + if (Items is AvaloniaList avaloniaList) { avaloniaList.RemoveRange(0, avaloniaList.Count - 1); @@ -175,23 +228,13 @@ public class TagInput : TemplatedControl Items.Clear(); Items.Add(_textBox); } - - if (newTags != null) - { - for (int i = 0; i < newTags.Count; i++) - { - Items.Insert(Items.Count - 1, newTags[i]); - } - } - if (oldTags is INotifyCollectionChanged inccold) - { - inccold.CollectionChanged-= OnCollectionChanged; - } - if (Tags is INotifyCollectionChanged incc) - { - incc.CollectionChanged += OnCollectionChanged; - } + if (newTags != null) + for (var i = 0; i < newTags.Count; i++) + Items.Insert(Items.Count - 1, newTags[i]); + if (oldTags is INotifyCollectionChanged inccold) inccold.CollectionChanged -= OnCollectionChanged; + + if (Tags is INotifyCollectionChanged incc) incc.CollectionChanged += OnCollectionChanged; } private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -199,27 +242,21 @@ public class TagInput : TemplatedControl if (e.Action == NotifyCollectionChangedAction.Add) { var items = e.NewItems; - int index = e.NewStartingIndex; + var index = e.NewStartingIndex; foreach (var item in items) - { if (item is string s) { Items.Insert(index, s); index++; } - } } else if (e.Action == NotifyCollectionChangedAction.Remove) { var items = e.OldItems; - int index = e.OldStartingIndex; + var index = e.OldStartingIndex; foreach (var item in items) - { - if (item is string s) - { + if (item is string) Items.RemoveAt(index); - } - } } else if (e.Action == NotifyCollectionChangedAction.Reset) { @@ -227,70 +264,66 @@ public class TagInput : TemplatedControl Items.Add(_textBox); InvalidateVisual(); } - } private void OnTextBoxKeyDown(object? sender, KeyEventArgs args) { - if (args.Key == Key.Enter) + if (!AcceptsReturn && args.Key == Key.Enter) { - AddTags(); + AddTags(_textBox.Text); + } + else if (AcceptsReturn && args.Key==Key.Enter) + { + var texts = _textBox.Text?.Split(["\r", "\n"], StringSplitOptions.RemoveEmptyEntries) ?? []; + foreach (var text in texts) + { + AddTags(text); + } + args.Handled = true; } else if (args.Key == Key.Delete || args.Key == Key.Back) - { - if (string.IsNullOrEmpty(_textBox.Text)||_textBox.Text?.Length == 0) + if (string.IsNullOrEmpty(_textBox.Text) || _textBox.Text?.Length == 0) { - if (Tags.Count == 0) - { - return; - } - int index = Items.Count - 2; + if (Tags.Count == 0) return; + var index = Items.Count - 2; // Items.RemoveAt(index); Tags.RemoveAt(index); } - } } - - private void AddTags() + + private void AddTags(string? text) { - if (!(_textBox.Text?.Length > 0)) return; + if (!(text?.Length > 0)) return; + if (Tags.Count >= MaxCount) return; string[] values; if (!string.IsNullOrEmpty(Separator)) - { - values = _textBox.Text.Split(new string[] { Separator }, + values = text.Split(new[] { Separator }, StringSplitOptions.RemoveEmptyEntries); - } else - { values = new[] { _textBox.Text }; - } - if (!AllowDuplicates && Tags != null) + if (!AllowDuplicates) values = values.Distinct().Except(Tags).ToArray(); - for (int i = 0; i < values.Length; i++) + for (var i = 0; i < values.Length; i++) { - int index = Items.Count - 1; + var index = Items.Count - 1; // Items.Insert(index, values[i]); Tags?.Insert(index, values[i]); } - _textBox.Text = ""; + _textBox.Clear(); } public void Close(object o) { if (o is Control t) - { if (t.Parent is ContentPresenter presenter) { - int? index = _itemsControl?.IndexFromContainer(presenter); + var index = _itemsControl?.IndexFromContainer(presenter); if (index is >= 0 && index < Items.Count - 1) - { // Items.RemoveAt(index.Value); Tags.RemoveAt(index.Value); - } } - } } } \ No newline at end of file