From d271623a66d09988402551107761800b7b54dfff Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 22 Mar 2024 22:02:00 +0800 Subject: [PATCH 1/9] feat: init --- .../Controls/MultiComboBox.axaml | 12 +++++ src/Ursa/Controls/ComboBox/MultiComboBox.cs | 47 +++++++++++++++++++ .../Controls/ComboBox/MultiComboBoxItem.cs | 18 +++++++ 3 files changed, 77 insertions(+) create mode 100644 src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml create mode 100644 src/Ursa/Controls/ComboBox/MultiComboBox.cs create mode 100644 src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs diff --git a/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml b/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml new file mode 100644 index 0000000..96fe32a --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/Ursa/Controls/ComboBox/MultiComboBox.cs b/src/Ursa/Controls/ComboBox/MultiComboBox.cs new file mode 100644 index 0000000..3bdba24 --- /dev/null +++ b/src/Ursa/Controls/ComboBox/MultiComboBox.cs @@ -0,0 +1,47 @@ +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; + +namespace Ursa.Controls; + +public class MultiComboBox: SelectingItemsControl +{ + private ComboBox _box; + + private static ITemplate _defaultPanel = new FuncTemplate(() => new VirtualizingStackPanel()); + + public static readonly StyledProperty IsDropDownOpenProperty = + ComboBox.IsDropDownOpenProperty.AddOwner(); + + public bool IsDropDownOpen + { + get => GetValue(IsDropDownOpenProperty); + set => SetValue(IsDropDownOpenProperty, value); + } + + static MultiComboBox() + { + FocusableProperty.OverrideDefaultValue(true); + ItemsPanelProperty.OverrideDefaultValue(_defaultPanel); + } + + 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 MultiComboBoxItem(); + } + + internal void ItemFocused(MultiComboBoxItem dropDownItem) + { + if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) + { + dropDownItem.BringIntoView(); + } + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs b/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs new file mode 100644 index 0000000..8eaf26a --- /dev/null +++ b/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Controls; +using Irihi.Avalonia.Shared.Helpers; + +namespace Ursa.Controls; + +public class MultiComboBoxItem: ListBoxItem +{ + public MultiComboBoxItem() + { + this.GetObservable(IsFocusedProperty).Subscribe(a=> { + if (a) + { + (Parent as MultiComboBox)?.ItemFocused(this); + } + }); + } +} \ No newline at end of file From d91a17474c192b6599eb7f211df966ef0c98ddff Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sun, 24 Mar 2024 16:45:29 +0800 Subject: [PATCH 2/9] feat: add a simple template. --- .../Controls/MultiComboBox.axaml | 20 +++++++++++++++++++ src/Ursa.Themes.Semi/Controls/_index.axaml | 1 + src/Ursa/Controls/ComboBox/MultiComboBox.cs | 6 +++--- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml b/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml index 96fe32a..9f3ae00 100644 --- a/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml +++ b/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml @@ -4,6 +4,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index a369754..9a138a9 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -20,6 +20,7 @@ + diff --git a/src/Ursa/Controls/ComboBox/MultiComboBox.cs b/src/Ursa/Controls/ComboBox/MultiComboBox.cs index 3bdba24..7ce9d28 100644 --- a/src/Ursa/Controls/ComboBox/MultiComboBox.cs +++ b/src/Ursa/Controls/ComboBox/MultiComboBox.cs @@ -1,4 +1,6 @@ -using System.Collections.ObjectModel; +using System.Collections; +using System.Collections.ObjectModel; +using System.Collections.Specialized; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -8,8 +10,6 @@ namespace Ursa.Controls; public class MultiComboBox: SelectingItemsControl { - private ComboBox _box; - private static ITemplate _defaultPanel = new FuncTemplate(() => new VirtualizingStackPanel()); public static readonly StyledProperty IsDropDownOpenProperty = From b15d237056c0954be4f9726db53813cec630b9e5 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sun, 24 Mar 2024 17:00:34 +0800 Subject: [PATCH 3/9] feat: WIP add a demo. --- demo/Ursa.Demo/Pages/MultiComboBoxDemo.axaml | 14 ++++++++ .../Pages/MultiComboBoxDemo.axaml.cs | 13 ++++++++ .../Ursa.Demo/ViewModels/MainViewViewModel.cs | 1 + demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 2 ++ .../ViewModels/MultiComboBoxDemoViewModel.cs | 30 +++++++++++++++++ .../Controls/MultiComboBox.axaml | 2 +- src/Ursa/Controls/ComboBox/MultiComboBox.cs | 32 +++++++++++++++++++ 7 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 demo/Ursa.Demo/Pages/MultiComboBoxDemo.axaml create mode 100644 demo/Ursa.Demo/Pages/MultiComboBoxDemo.axaml.cs create mode 100644 demo/Ursa.Demo/ViewModels/MultiComboBoxDemoViewModel.cs diff --git a/demo/Ursa.Demo/Pages/MultiComboBoxDemo.axaml b/demo/Ursa.Demo/Pages/MultiComboBoxDemo.axaml new file mode 100644 index 0000000..2fb5a4b --- /dev/null +++ b/demo/Ursa.Demo/Pages/MultiComboBoxDemo.axaml @@ -0,0 +1,14 @@ + + + + + diff --git a/demo/Ursa.Demo/Pages/MultiComboBoxDemo.axaml.cs b/demo/Ursa.Demo/Pages/MultiComboBoxDemo.axaml.cs new file mode 100644 index 0000000..2a51be5 --- /dev/null +++ b/demo/Ursa.Demo/Pages/MultiComboBoxDemo.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class MultiComboBoxDemo : UserControl +{ + public MultiComboBoxDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index e55a00a..15af99e 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -43,6 +43,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyKeyGestureInput => new KeyGestureInputDemoViewModel(), MenuKeys.MenuKeyLoading => new LoadingDemoViewModel(), MenuKeys.MenuKeyMessageBox => new MessageBoxDemoViewModel(), + MenuKeys.MenuKeyMultiComboBox => new MultiComboBoxDemoViewModel(), MenuKeys.MenuKeyNavMenu => new NavMenuDemoViewModel(), MenuKeys.MenuKeyNumberDisplayer => new NumberDisplayerDemoViewModel(), MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(), diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index 3ac4eac..5a345ef 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -30,6 +30,7 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "KeyGestureInput", Key = MenuKeys.MenuKeyKeyGestureInput }, new() { MenuHeader = "Loading", Key = MenuKeys.MenuKeyLoading }, new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox }, + new() { MenuHeader = "MultiComboBox", Key = MenuKeys.MenuKeyMultiComboBox, Status = "New" }, new() { MenuHeader = "Nav Menu", Key = MenuKeys.MenuKeyNavMenu, Status = "Updated" }, // new() { MenuHeader = "Number Displayer", Key = MenuKeys.MenuKeyNumberDisplayer, Status = "New" }, new() { MenuHeader = "Numeric UpDown", Key = MenuKeys.MenuKeyNumericUpDown }, @@ -70,6 +71,7 @@ public static class MenuKeys public const string MenuKeyKeyGestureInput = "KeyGestureInput"; public const string MenuKeyLoading = "Loading"; public const string MenuKeyMessageBox = "MessageBox"; + public const string MenuKeyMultiComboBox = "MultiComboBox"; public const string MenuKeyNavMenu = "NavMenu"; public const string MenuKeyNumberDisplayer = "NumberDisplayer"; public const string MenuKeyNumericUpDown = "NumericUpDown"; diff --git a/demo/Ursa.Demo/ViewModels/MultiComboBoxDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/MultiComboBoxDemoViewModel.cs new file mode 100644 index 0000000..239dc58 --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/MultiComboBoxDemoViewModel.cs @@ -0,0 +1,30 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public class MultiComboBoxDemoViewModel: ObservableObject +{ + public ObservableCollection Items { get; set; } + + public MultiComboBoxDemoViewModel() + { + Items = new ObservableCollection() + { + "Item 1", + "Item 2", + "Item 3", + "Item 4", + "Item 5", + "Item 6", + "Item 7", + "Item 8", + "Illinois", + "Indiana", + "Iowa", + "Kansas", + "Kentucky", + "Louisiana", + }; + } +} \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml b/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml index 9f3ae00..080cbfc 100644 --- a/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml +++ b/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml @@ -17,7 +17,7 @@ - + diff --git a/src/Ursa/Controls/ComboBox/MultiComboBox.cs b/src/Ursa/Controls/ComboBox/MultiComboBox.cs index 7ce9d28..29bee67 100644 --- a/src/Ursa/Controls/ComboBox/MultiComboBox.cs +++ b/src/Ursa/Controls/ComboBox/MultiComboBox.cs @@ -3,13 +3,18 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; namespace Ursa.Controls; +[TemplatePart( PART_ListBox, typeof(ListBox))] public class MultiComboBox: SelectingItemsControl { + public const string PART_ListBox = "PART_ListBox"; + private ListBox? _listBox; + private static ITemplate _defaultPanel = new FuncTemplate(() => new VirtualizingStackPanel()); public static readonly StyledProperty IsDropDownOpenProperty = @@ -20,6 +25,15 @@ public class MultiComboBox: SelectingItemsControl get => GetValue(IsDropDownOpenProperty); set => SetValue(IsDropDownOpenProperty, value); } + + public new static readonly StyledProperty SelectedItemsProperty = AvaloniaProperty.Register( + nameof(SelectedItems)); + + public new IList? SelectedItems + { + get => GetValue(SelectedItemsProperty); + set => SetValue(SelectedItemsProperty, value); + } static MultiComboBox() { @@ -44,4 +58,22 @@ public class MultiComboBox: SelectingItemsControl dropDownItem.BringIntoView(); } } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _listBox = e.NameScope.Find(PART_ListBox); + if (_listBox != null) + { + _listBox.SelectionChanged += ListBox_SelectionChanged; + } + } + + private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (sender == _listBox) + { + this.SelectedItems = _listBox.SelectedItems; + } + } } \ No newline at end of file From e600937fd7b0f998c333311f93511d0ca93fe05e Mon Sep 17 00:00:00 2001 From: rabbitism Date: Mon, 25 Mar 2024 11:10:11 +0800 Subject: [PATCH 4/9] feat: deal with virtualization. --- .../Controls/MultiComboBox.axaml | 164 ++++++++++++++++-- src/Ursa/Controls/ComboBox/MultiComboBox.cs | 72 +++++--- .../Controls/ComboBox/MultiComboBoxItem.cs | 33 +++- 3 files changed, 228 insertions(+), 41 deletions(-) diff --git a/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml b/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml index 080cbfc..5df1f9b 100644 --- a/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml +++ b/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml @@ -1,32 +1,166 @@ - - - - + + + + + - + - + - - - - + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa/Controls/ComboBox/MultiComboBox.cs b/src/Ursa/Controls/ComboBox/MultiComboBox.cs index 29bee67..8b141f1 100644 --- a/src/Ursa/Controls/ComboBox/MultiComboBox.cs +++ b/src/Ursa/Controls/ComboBox/MultiComboBox.cs @@ -2,19 +2,17 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using Avalonia; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Irihi.Avalonia.Shared.Helpers; namespace Ursa.Controls; -[TemplatePart( PART_ListBox, typeof(ListBox))] public class MultiComboBox: SelectingItemsControl { - public const string PART_ListBox = "PART_ListBox"; - private ListBox? _listBox; - private static ITemplate _defaultPanel = new FuncTemplate(() => new VirtualizingStackPanel()); public static readonly StyledProperty IsDropDownOpenProperty = @@ -26,8 +24,17 @@ public class MultiComboBox: SelectingItemsControl set => SetValue(IsDropDownOpenProperty, value); } + public static readonly StyledProperty MaxDropdownHeightProperty = AvaloniaProperty.Register( + nameof(MaxDropdownHeight)); + + public double MaxDropdownHeight + { + get => GetValue(MaxDropdownHeightProperty); + set => SetValue(MaxDropdownHeightProperty, value); + } + public new static readonly StyledProperty SelectedItemsProperty = AvaloniaProperty.Register( - nameof(SelectedItems)); + nameof(SelectedItems), new AvaloniaList()); public new IList? SelectedItems { @@ -43,14 +50,46 @@ public class MultiComboBox: SelectingItemsControl protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { - return NeedsContainer(item, out recycleKey); + recycleKey = item; + return item is not MultiComboBoxItem; } protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) { return new MultiComboBoxItem(); } - + + private Dictionary _disposables = new Dictionary(); + + protected override void PrepareContainerForItemOverride(Control container, object? item, int index) + { + base.PrepareContainerForItemOverride(container, item, index); + if(_disposables.TryGetValue(index, out var d)) + { + d?.Dispose(); + _disposables.Remove(index); + } + if (container is MultiComboBoxItem comboBoxItem) + { + comboBoxItem.IsSelected = SelectedItems?.Contains(item) ?? false; + var disposable = MultiComboBoxItem.IsSelectedProperty.Changed.Subscribe(a => + { + if (a.Sender == comboBoxItem) + { + if (comboBoxItem.IsSelected) + { + SelectedItems?.Add(item); + } + else + { + SelectedItems?.Remove(item); + } + } + }); + _disposables[index] = disposable; + } + } + internal void ItemFocused(MultiComboBoxItem dropDownItem) { if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) @@ -58,22 +97,5 @@ public class MultiComboBox: SelectingItemsControl dropDownItem.BringIntoView(); } } - - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) - { - base.OnApplyTemplate(e); - _listBox = e.NameScope.Find(PART_ListBox); - if (_listBox != null) - { - _listBox.SelectionChanged += ListBox_SelectionChanged; - } - } - - private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender == _listBox) - { - this.SelectedItems = _listBox.SelectedItems; - } - } + } \ No newline at end of file diff --git a/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs b/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs index 8eaf26a..51202b4 100644 --- a/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs +++ b/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs @@ -1,11 +1,28 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Mixins; +using Avalonia.Input; using Irihi.Avalonia.Shared.Helpers; namespace Ursa.Controls; -public class MultiComboBoxItem: ListBoxItem +public class MultiComboBoxItem: ContentControl { + public static readonly StyledProperty IsSelectedProperty = AvaloniaProperty.Register( + nameof(IsSelected)); + + public bool IsSelected + { + get => GetValue(IsSelectedProperty); + set => SetValue(IsSelectedProperty, value); + } + + static MultiComboBoxItem() + { + IsSelectedProperty.AffectsPseudoClass(":selected"); + PressedMixin.Attach(); + FocusableProperty.OverrideDefaultValue(true); + } public MultiComboBoxItem() { this.GetObservable(IsFocusedProperty).Subscribe(a=> { @@ -15,4 +32,18 @@ public class MultiComboBoxItem: ListBoxItem } }); } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + if (e.Handled) + { + return; + } + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + { + this.IsSelected = !this.IsSelected; + e.Handled = true; + } + } } \ No newline at end of file From 6993cdd7ab29dce81313b256e3fd700eb7481041 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 26 Mar 2024 01:47:53 +0800 Subject: [PATCH 5/9] feat: remove virtualization subscription feature to avoid potential issue of --- src/Ursa/Controls/ComboBox/MultiComboBox.cs | 28 +----------------- .../Controls/ComboBox/MultiComboBoxItem.cs | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/Ursa/Controls/ComboBox/MultiComboBox.cs b/src/Ursa/Controls/ComboBox/MultiComboBox.cs index 8b141f1..075e5d1 100644 --- a/src/Ursa/Controls/ComboBox/MultiComboBox.cs +++ b/src/Ursa/Controls/ComboBox/MultiComboBox.cs @@ -7,6 +7,7 @@ using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Interactivity; using Irihi.Avalonia.Shared.Helpers; namespace Ursa.Controls; @@ -59,35 +60,9 @@ public class MultiComboBox: SelectingItemsControl return new MultiComboBoxItem(); } - private Dictionary _disposables = new Dictionary(); - protected override void PrepareContainerForItemOverride(Control container, object? item, int index) { base.PrepareContainerForItemOverride(container, item, index); - if(_disposables.TryGetValue(index, out var d)) - { - d?.Dispose(); - _disposables.Remove(index); - } - if (container is MultiComboBoxItem comboBoxItem) - { - comboBoxItem.IsSelected = SelectedItems?.Contains(item) ?? false; - var disposable = MultiComboBoxItem.IsSelectedProperty.Changed.Subscribe(a => - { - if (a.Sender == comboBoxItem) - { - if (comboBoxItem.IsSelected) - { - SelectedItems?.Add(item); - } - else - { - SelectedItems?.Remove(item); - } - } - }); - _disposables[index] = disposable; - } } internal void ItemFocused(MultiComboBoxItem dropDownItem) @@ -97,5 +72,4 @@ public class MultiComboBox: SelectingItemsControl dropDownItem.BringIntoView(); } } - } \ No newline at end of file diff --git a/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs b/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs index 51202b4..a340d20 100644 --- a/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs +++ b/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs @@ -2,12 +2,15 @@ using Avalonia.Controls; using Avalonia.Controls.Mixins; using Avalonia.Input; +using Avalonia.LogicalTree; using Irihi.Avalonia.Shared.Helpers; namespace Ursa.Controls; public class MultiComboBoxItem: ContentControl { + private MultiComboBox? _parent; + public static readonly StyledProperty IsSelectedProperty = AvaloniaProperty.Register( nameof(IsSelected)); @@ -22,7 +25,23 @@ public class MultiComboBoxItem: ContentControl IsSelectedProperty.AffectsPseudoClass(":selected"); PressedMixin.Attach(); FocusableProperty.OverrideDefaultValue(true); + IsSelectedProperty.Changed.AddClassHandler((item, args) => + item.OnSelectionChanged(args)); } + + private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs args) + { + var parent = this.FindLogicalAncestorOfType(); + if (args.NewValue.Value) + { + parent?.SelectedItems?.Add(this.DataContext); + } + else + { + parent?.SelectedItems?.Remove(this.DataContext); + } + } + public MultiComboBoxItem() { this.GetObservable(IsFocusedProperty).Subscribe(a=> { @@ -32,7 +51,15 @@ public class MultiComboBoxItem: ContentControl } }); } - + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + _parent = this.FindLogicalAncestorOfType(); + if(this.IsSelected) + _parent?.SelectedItems?.Add(this.DataContext); + } + protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); From bc4262efc85ca395ba4c1101c6225ccca1a6e62d Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 26 Mar 2024 02:56:14 +0800 Subject: [PATCH 6/9] feat: add remove command support. --- .../Controls/MultiComboBox.axaml | 77 ++++++++++++++----- src/Ursa.Themes.Semi/Controls/TagInput.axaml | 4 +- src/Ursa/Controls/ComboBox/MultiComboBox.cs | 37 ++++++++- .../Controls/ComboBox/MultiComboBoxItem.cs | 3 +- .../ComboBox/MultiComboBoxSelectedItemList.cs | 36 +++++++++ 5 files changed, 133 insertions(+), 24 deletions(-) create mode 100644 src/Ursa/Controls/ComboBox/MultiComboBoxSelectedItemList.cs diff --git a/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml b/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml index 5df1f9b..4e4726a 100644 --- a/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml +++ b/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml @@ -5,27 +5,59 @@ + + + + - - - - - - - - - + + + + + + + + + + + + + + IsOpen="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsDropDownOpen, Mode=TwoWay}" + PlacementTarget="PART_RootGrid"> - + @@ -36,7 +68,7 @@ - + @@ -45,7 +77,7 @@ - + @@ -83,7 +115,8 @@ VerticalAlignment="Center" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" - IsVisible="{TemplateBinding Content,Converter={x:Static ObjectConverters.IsNotNull}}" + IsVisible="{TemplateBinding Content, + Converter={x:Static ObjectConverters.IsNotNull}}" RecognizesAccessKey="True" TextWrapping="Wrap" /> @@ -132,10 +165,8 @@ @@ -163,4 +194,12 @@ + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/TagInput.axaml b/src/Ursa.Themes.Semi/Controls/TagInput.axaml index 6829870..4e49cf0 100644 --- a/src/Ursa.Themes.Semi/Controls/TagInput.axaml +++ b/src/Ursa.Themes.Semi/Controls/TagInput.axaml @@ -42,10 +42,10 @@ - - diff --git a/src/Ursa/Controls/ComboBox/MultiComboBox.cs b/src/Ursa/Controls/ComboBox/MultiComboBox.cs index 075e5d1..55115e4 100644 --- a/src/Ursa/Controls/ComboBox/MultiComboBox.cs +++ b/src/Ursa/Controls/ComboBox/MultiComboBox.cs @@ -7,13 +7,18 @@ using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Input; using Avalonia.Interactivity; using Irihi.Avalonia.Shared.Helpers; namespace Ursa.Controls; +[TemplatePart(PART_BackgroundBorder, typeof(Border))] public class MultiComboBox: SelectingItemsControl { + public const string PART_BackgroundBorder = "PART_BackgroundBorder"; + private Border? _rootBorder; + private static ITemplate _defaultPanel = new FuncTemplate(() => new VirtualizingStackPanel()); public static readonly StyledProperty IsDropDownOpenProperty = @@ -60,9 +65,18 @@ public class MultiComboBox: SelectingItemsControl return new MultiComboBoxItem(); } - protected override void PrepareContainerForItemOverride(Control container, object? item, int index) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - base.PrepareContainerForItemOverride(container, item, index); + base.OnApplyTemplate(e); + PointerPressedEvent.RemoveHandler(OnBackgroundPointerPressed, _rootBorder); + _rootBorder = e.NameScope.Find(PART_BackgroundBorder); + PointerPressedEvent.AddHandler(OnBackgroundPointerPressed, _rootBorder); + + } + + private void OnBackgroundPointerPressed(object sender, PointerPressedEventArgs e) + { + SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen); } internal void ItemFocused(MultiComboBoxItem dropDownItem) @@ -72,4 +86,23 @@ public class MultiComboBox: SelectingItemsControl dropDownItem.BringIntoView(); } } + + public void Remove(object? o) + { + if (o is StyledElement s) + { + var data = s.DataContext; + this.SelectedItems?.Remove(data); + var item = this.Items.FirstOrDefault(a => ReferenceEquals(a, data)); + if (item is not null) + { + var container = ContainerFromItem(item); + if (container is MultiComboBoxItem t) + { + t.IsSelected = false; + } + } + } + + } } \ No newline at end of file diff --git a/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs b/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs index a340d20..1c3931d 100644 --- a/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs +++ b/src/Ursa/Controls/ComboBox/MultiComboBoxItem.cs @@ -1,4 +1,5 @@ -using Avalonia; +using System.Windows.Input; +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Mixins; using Avalonia.Input; diff --git a/src/Ursa/Controls/ComboBox/MultiComboBoxSelectedItemList.cs b/src/Ursa/Controls/ComboBox/MultiComboBoxSelectedItemList.cs new file mode 100644 index 0000000..4f23fd3 --- /dev/null +++ b/src/Ursa/Controls/ComboBox/MultiComboBoxSelectedItemList.cs @@ -0,0 +1,36 @@ +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; + +namespace Ursa.Controls; + +public class MultiComboBoxSelectedItemList: ItemsControl +{ + public static readonly StyledProperty RemoveCommandProperty = AvaloniaProperty.Register( + nameof(RemoveCommand)); + + public ICommand? RemoveCommand + { + get => GetValue(RemoveCommandProperty); + set => SetValue(RemoveCommandProperty, value); + } + + 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 ClosableTag(); + } + + protected override void PrepareContainerForItemOverride(Control container, object? item, int index) + { + base.PrepareContainerForItemOverride(container, item, index); + if (container is ClosableTag tag) + { + tag.Command = RemoveCommand; + } + } +} \ No newline at end of file From 5d917d590546f11427083f1e9dc7a155b390f675 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 26 Mar 2024 12:05:16 +0800 Subject: [PATCH 7/9] feat: support clear. --- demo/Ursa.Demo/Pages/MultiComboBoxDemo.axaml | 5 +- .../Controls/MultiComboBox.axaml | 301 ++++++++++-------- src/Ursa/Controls/ComboBox/MultiComboBox.cs | 58 +++- 3 files changed, 229 insertions(+), 135 deletions(-) diff --git a/demo/Ursa.Demo/Pages/MultiComboBoxDemo.axaml b/demo/Ursa.Demo/Pages/MultiComboBoxDemo.axaml index 2fb5a4b..cd1844c 100644 --- a/demo/Ursa.Demo/Pages/MultiComboBoxDemo.axaml +++ b/demo/Ursa.Demo/Pages/MultiComboBoxDemo.axaml @@ -8,7 +8,8 @@ x:DataType="vm:MultiComboBoxDemoViewModel" x:CompileBindings="True" x:Class="Ursa.Demo.Pages.MultiComboBoxDemo"> - - + + + diff --git a/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml b/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml index 4e4726a..45108f9 100644 --- a/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml +++ b/src/Ursa.Themes.Semi/Controls/MultiComboBox.axaml @@ -5,65 +5,160 @@ + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + +