From 40c1d96f2e8a249e5c47a938ea5a09eb0fbd0441 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 11 Apr 2024 23:00:38 +0800 Subject: [PATCH] feat: add demo. update overrides with internal implementations. --- demo/Ursa.Demo/Pages/TreeComboBoxDemo.axaml | 23 ++++ .../Ursa.Demo/Pages/TreeComboBoxDemo.axaml.cs | 13 ++ .../Ursa.Demo/ViewModels/MainViewViewModel.cs | 1 + demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 2 + .../ViewModels/TreeComboBoxDemoViewModel.cs | 8 ++ .../Controls/TreeComboBox.axaml | 41 ++++++ src/Ursa.Themes.Semi/Controls/_index.axaml | 1 + src/Ursa/Controls/ComboBox/TreeComboBox.cs | 128 ++++++++++++++++++ .../Controls/ComboBox/TreeComboBoxItem.cs | 110 +++++++++++++++ src/Ursa/Ursa.csproj | 2 +- 10 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 demo/Ursa.Demo/Pages/TreeComboBoxDemo.axaml create mode 100644 demo/Ursa.Demo/Pages/TreeComboBoxDemo.axaml.cs create mode 100644 demo/Ursa.Demo/ViewModels/TreeComboBoxDemoViewModel.cs create mode 100644 src/Ursa.Themes.Semi/Controls/TreeComboBox.axaml create mode 100644 src/Ursa/Controls/ComboBox/TreeComboBox.cs create mode 100644 src/Ursa/Controls/ComboBox/TreeComboBoxItem.cs diff --git a/demo/Ursa.Demo/Pages/TreeComboBoxDemo.axaml b/demo/Ursa.Demo/Pages/TreeComboBoxDemo.axaml new file mode 100644 index 0000000..2624e7d --- /dev/null +++ b/demo/Ursa.Demo/Pages/TreeComboBoxDemo.axaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/Pages/TreeComboBoxDemo.axaml.cs b/demo/Ursa.Demo/Pages/TreeComboBoxDemo.axaml.cs new file mode 100644 index 0000000..6338466 --- /dev/null +++ b/demo/Ursa.Demo/Pages/TreeComboBoxDemo.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class TreeComboBoxDemo : UserControl +{ + public TreeComboBoxDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 15af99e..a86e947 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -55,6 +55,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeySkeleton => new SkeletonDemoViewModel(), MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(), MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(), + MenuKeys.MenuKeyTreeComboBox => new TreeComboBoxDemoViewModel(), MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(), MenuKeys.MenuKeyThemeToggler => new ThemeTogglerDemoViewModel(), MenuKeys.MenuKeyToolBar => new ToolBarDemoViewModel(), diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index 5a345ef..006bcd6 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -43,6 +43,7 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput }, new() { MenuHeader = "Theme Toggler", Key = MenuKeys.MenuKeyThemeToggler }, new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline }, + new() { MenuHeader = "TreeComboBox", Key = MenuKeys.MenuKeyTreeComboBox }, new() { MenuHeader = "TwoTonePathIcon", Key = MenuKeys.MenuKeyTwoTonePathIcon}, new() { MenuHeader = "ToolBar", Key = MenuKeys.MenuKeyToolBar }, new() { MenuHeader = "Verification Code", Key = MenuKeys.MenuKeyVerificationCode, Status = "New" }, @@ -85,6 +86,7 @@ public static class MenuKeys public const string MenuKeyTimeline = "Timeline"; public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon"; public const string MenuKeyThemeToggler = "ThemeToggler"; + public const string MenuKeyTreeComboBox = "TreeComboBox"; public const string MenuKeyToolBar = "ToolBar"; public const string MenuKeyVerificationCode = "VerificationCode"; diff --git a/demo/Ursa.Demo/ViewModels/TreeComboBoxDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/TreeComboBoxDemoViewModel.cs new file mode 100644 index 0000000..33a9252 --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/TreeComboBoxDemoViewModel.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public class TreeComboBoxDemoViewModel: ObservableObject +{ + +} \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/TreeComboBox.axaml b/src/Ursa.Themes.Semi/Controls/TreeComboBox.axaml new file mode 100644 index 0000000..db57123 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/TreeComboBox.axaml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index 9a138a9..4b325cd 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -32,6 +32,7 @@ + diff --git a/src/Ursa/Controls/ComboBox/TreeComboBox.cs b/src/Ursa/Controls/ComboBox/TreeComboBox.cs new file mode 100644 index 0000000..060dfee --- /dev/null +++ b/src/Ursa/Controls/ComboBox/TreeComboBox.cs @@ -0,0 +1,128 @@ +using System.Data; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.OpenGL.Controls; +using Irihi.Avalonia.Shared.Common; + + +namespace Ursa.Controls; + +[TemplatePart(PartNames.PART_Popup, typeof(Popup))] +public class TreeComboBox: SelectingItemsControl +{ + private static readonly FuncTemplate DefaultPanel = + new FuncTemplate(() => new VirtualizingStackPanel()); + + public static readonly StyledProperty MaxDropDownHeightProperty = + ComboBox.MaxDropDownHeightProperty.AddOwner(); + + public double MaxDropDownHeight + { + get => GetValue(MaxDropDownHeightProperty); + set => SetValue(MaxDropDownHeightProperty, value); + } + + public static readonly StyledProperty WatermarkProperty = + TextBox.WatermarkProperty.AddOwner(); + + public string? Watermark + { + get => GetValue(WatermarkProperty); + set => SetValue(WatermarkProperty, value); + } + + public static readonly StyledProperty IsDropDownOpenProperty = + ComboBox.IsDropDownOpenProperty.AddOwner(); + + public bool IsDropDownOpen + { + get => GetValue(IsDropDownOpenProperty); + set => SetValue(IsDropDownOpenProperty, value); + } + + public static readonly StyledProperty HorizontalContentAlignmentProperty = + ContentControl.HorizontalContentAlignmentProperty.AddOwner(); + + public HorizontalAlignment HorizontalContentAlignment + { + get => GetValue(HorizontalContentAlignmentProperty); + set => SetValue(HorizontalContentAlignmentProperty, value); + } + + public static readonly StyledProperty VerticalContentAlignmentProperty = + ContentControl.VerticalContentAlignmentProperty.AddOwner(); + + public VerticalAlignment VerticalContentAlignment + { + get => GetValue(VerticalContentAlignmentProperty); + set => SetValue(VerticalContentAlignmentProperty, value); + } + + public static readonly StyledProperty SelectedItemTemplateProperty = + AvaloniaProperty.Register(nameof(SelectedItemTemplate)); + + public IDataTemplate? SelectedItemTemplate + { + get => GetValue(SelectedItemTemplateProperty); + set => SetValue(SelectedItemTemplateProperty, value); + } + + public static readonly DirectProperty SelectionBoxItemProperty = AvaloniaProperty.RegisterDirect( + nameof(SelectionBoxItem), o => o.SelectionBoxItem); + private object? _selectionBoxItem; + public object? SelectionBoxItem + { + get => _selectionBoxItem; + protected set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); + } + + static TreeComboBox() + { + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + FocusableProperty.OverrideDefaultValue(true); + } + + protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + return NeedsContainer(item, out recycleKey); + } + + internal bool NeedsContainerInternal(object? item, int index, out object? recycleKey) + { + return NeedsContainerOverride(item, index, out recycleKey); + } + + protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return new TreeComboBoxItem(); + } + + internal Control CreateContainerForItemInternal(object? item, int index, object? recycleKey) + { + return CreateContainerForItemOverride(item, index, recycleKey); + } + + protected override void ContainerForItemPreparedOverride(Control container, object? item, int index) + { + base.ContainerForItemPreparedOverride(container, item, index); + } + + internal void ContainerForItemPreparedInternal(Control container, object? item, int index) + { + ContainerForItemPreparedOverride(container, item, index); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + if (e.InitialPressMouseButton == MouseButton.Left) + { + IsDropDownOpen = !IsDropDownOpen; + } + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/ComboBox/TreeComboBoxItem.cs b/src/Ursa/Controls/ComboBox/TreeComboBoxItem.cs new file mode 100644 index 0000000..3908c00 --- /dev/null +++ b/src/Ursa/Controls/ComboBox/TreeComboBoxItem.cs @@ -0,0 +1,110 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.LogicalTree; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Irihi.Avalonia.Shared.Common; +using Irihi.Avalonia.Shared.Helpers; + +namespace Ursa.Controls; + +[TemplatePart(PartNames.PART_Header, typeof(Control))] +public class TreeComboBoxItem: HeaderedItemsControl, ISelectable +{ + private Control? _header; + private TreeComboBox? _treeComboBox; + + public static readonly StyledProperty IsSelectedProperty = TreeViewItem.IsSelectedProperty.AddOwner(); + + public bool IsSelected + { + get => GetValue(IsSelectedProperty); + set => SetValue(IsSelectedProperty, value); + } + + public static readonly StyledProperty IsExpandedProperty = TreeViewItem.IsExpandedProperty.AddOwner(); + + public bool IsExpanded + { + get => GetValue(IsExpandedProperty); + set => SetValue(IsExpandedProperty, value); + } + + + + public static readonly DirectProperty LevelProperty = AvaloniaProperty.RegisterDirect( + nameof(Level), o => o.Level, (o, v) => o.Level = v); + private int _level; + public int Level + { + get => _level; + protected set => SetAndRaise(LevelProperty, ref _level, value); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + DoubleTappedEvent.RemoveHandler(OnDoubleTapped, _header); + _header = e.NameScope.Find(PartNames.PART_Header); + DoubleTappedEvent.AddHandler(OnDoubleTapped, _header); + } + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + _treeComboBox = this.FindLogicalAncestorOfType(); + Level = CalculateDistanceFromLogicalParent(this); + if (this.ItemTemplate is null && this._treeComboBox?.ItemTemplate is not null) + { + SetCurrentValue(ItemTemplateProperty, this._treeComboBox.ItemTemplate); + } + + + + } + + private void OnDoubleTapped(object sender, TappedEventArgs e) + { + if (this.ItemCount <= 0) return; + this.SetCurrentValue(IsExpandedProperty, !IsExpanded); + e.Handled = true; + } + + protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + return EnsureParent().NeedsContainerInternal(item, index, out recycleKey); + } + + protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return EnsureParent().CreateContainerForItemInternal(item, index, recycleKey); + } + + protected override void ContainerForItemPreparedOverride(Control container, object? item, int index) + { + EnsureParent().ContainerForItemPreparedInternal(container, item, index); + } + + // TODO replace with helper method from shared library. + private static int CalculateDistanceFromLogicalParent(ILogical? logical, int @default = -1) where T: ILogical + { + int distance = 0; + ILogical? parent = logical; + while (parent is not null) + { + if (parent is T) return distance; + parent = parent.LogicalParent; + distance++; + } + return @default; + } + + private TreeComboBox EnsureParent() + { + return this._treeComboBox ?? + throw new InvalidOperationException("TreeComboBoxItem must be a part of TreeComboBox"); + } +} \ No newline at end of file diff --git a/src/Ursa/Ursa.csproj b/src/Ursa/Ursa.csproj index 8c86bf6..853e121 100644 --- a/src/Ursa/Ursa.csproj +++ b/src/Ursa/Ursa.csproj @@ -17,7 +17,7 @@ - +