diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs index 085be94..b7b1a76 100644 --- a/demo/Ursa.Demo/Models/MenuKeys.cs +++ b/demo/Ursa.Demo/Models/MenuKeys.cs @@ -6,6 +6,7 @@ public static class MenuKeys public const string MenuKeyBadge = "Badge"; public const string MenuKeyBanner = "Banner"; public const string MenuKeyButtonGroup = "ButtonGroup"; + public const string MenuKeyBreadcrumb = "Breadcrumb"; public const string MenuKeyClassInput = "Class Input"; public const string MenuKeyDialog = "Dialog"; public const string MenuKeyDivider = "Divider"; diff --git a/demo/Ursa.Demo/Pages/BreadcrumbDemo.axaml b/demo/Ursa.Demo/Pages/BreadcrumbDemo.axaml new file mode 100644 index 0000000..101542e --- /dev/null +++ b/demo/Ursa.Demo/Pages/BreadcrumbDemo.axaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/Pages/BreadcrumbDemo.axaml.cs b/demo/Ursa.Demo/Pages/BreadcrumbDemo.axaml.cs new file mode 100644 index 0000000..c119fc7 --- /dev/null +++ b/demo/Ursa.Demo/Pages/BreadcrumbDemo.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class BreadcrumbDemo : UserControl +{ + public BreadcrumbDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/BreadcrumbDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/BreadcrumbDemoViewModel.cs new file mode 100644 index 0000000..d5dbbee --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/BreadcrumbDemoViewModel.cs @@ -0,0 +1,41 @@ +using System.Collections.ObjectModel; +using System.Windows.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Ursa.Controls; + +namespace Ursa.Demo.ViewModels; + +public class BreadcrumbDemoViewModel: ObservableObject +{ + public ObservableCollection Items1 { get; set; } + + public BreadcrumbDemoViewModel() + { + Items1 = new ObservableCollection() + { + new BreadcrumbDemoItem() { Section = "Home", Icon = "Home" }, + new BreadcrumbDemoItem() { Section = "Page 1", Icon = "Page" }, + new BreadcrumbDemoItem() { Section = "Page 2", Icon = "Page" }, + new BreadcrumbDemoItem() { Section = "Page 3", Icon = "Page" }, + new BreadcrumbDemoItem() { Section = "Page 4", Icon = "Page", IsReadOnly = true}, + }; + } +} + +public partial class BreadcrumbDemoItem: ObservableObject +{ + public string Section { get; set; } + public string Icon { get; set; } + [ObservableProperty] private bool _isReadOnly; + + public ICommand Command { get; set; } + + public BreadcrumbDemoItem() + { + Command = new RelayCommand(() => + { + MessageBox.ShowOverlayAsync(Section); + }); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 8cc13e6..d987128 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -28,6 +28,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyBadge => new BadgeDemoViewModel(), MenuKeys.MenuKeyBanner => new BannerDemoViewModel(), MenuKeys.MenuKeyButtonGroup => new ButtonGroupDemoViewModel(), + MenuKeys.MenuKeyBreadcrumb => new BreadcrumbDemoViewModel(), MenuKeys.MenuKeyClassInput => new ClassInputDemoViewModel(), MenuKeys.MenuKeyDialog => new DialogDemoViewModel(), MenuKeys.MenuKeyDivider => new DividerDemoViewModel(), diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index a25e8d1..a4eaa14 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -14,6 +14,7 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "Controls", IsSeparator = true }, new() { MenuHeader = "Badge", Key = MenuKeys.MenuKeyBadge, Status = "Updated"}, new() { MenuHeader = "Banner", Key = MenuKeys.MenuKeyBanner }, + new() { MenuHeader = "Breadcrumb", Key = MenuKeys.MenuKeyBreadcrumb, Status = "New" }, new() { MenuHeader = "Button Group", Key = MenuKeys.MenuKeyButtonGroup}, new() { MenuHeader = "Class Input", Key = MenuKeys.MenuKeyClassInput, Status = "New" }, new() { MenuHeader = "Dialog", Key = MenuKeys.MenuKeyDialog, Status = "Updated"}, diff --git a/src/Ursa.Themes.Semi/Controls/Breadcrumb.axaml b/src/Ursa.Themes.Semi/Controls/Breadcrumb.axaml new file mode 100644 index 0000000..4df2747 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/Breadcrumb.axaml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index 21d3909..6be2414 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -4,6 +4,7 @@ + diff --git a/src/Ursa.Themes.Semi/Styles/Breadcrumb.axaml b/src/Ursa.Themes.Semi/Styles/Breadcrumb.axaml new file mode 100644 index 0000000..66e6706 --- /dev/null +++ b/src/Ursa.Themes.Semi/Styles/Breadcrumb.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Styles/_index.axaml b/src/Ursa.Themes.Semi/Styles/_index.axaml index e3cc666..baa0d9b 100644 --- a/src/Ursa.Themes.Semi/Styles/_index.axaml +++ b/src/Ursa.Themes.Semi/Styles/_index.axaml @@ -4,6 +4,7 @@ + diff --git a/src/Ursa/Controls/BackTop/BackTop.cs b/src/Ursa/Controls/BackTop/BackTop.cs new file mode 100644 index 0000000..3506807 --- /dev/null +++ b/src/Ursa/Controls/BackTop/BackTop.cs @@ -0,0 +1,15 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Media; + +namespace Ursa.Controls.BackTop; + +public class BackTop: Control +{ + public static readonly AttachedProperty AttachProperty = + AvaloniaProperty.RegisterAttached("Attach"); + + public static void SetAttach(Control obj, bool value) => obj.SetValue(AttachProperty, value); + public static bool GetAttach(Control obj) => obj.GetValue(AttachProperty); +} \ No newline at end of file diff --git a/src/Ursa/Controls/Breadcrumb/Breadcrumb.cs b/src/Ursa/Controls/Breadcrumb/Breadcrumb.cs new file mode 100644 index 0000000..4818d40 --- /dev/null +++ b/src/Ursa/Controls/Breadcrumb/Breadcrumb.cs @@ -0,0 +1,128 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Layout; +using Avalonia.Metadata; + +namespace Ursa.Controls; + +public class Breadcrumb: ItemsControl +{ + private static readonly ITemplate _defaultPanel = + new FuncTemplate(() => new StackPanel() { Orientation = Orientation.Horizontal }); + + + public static readonly StyledProperty IconBindingProperty = AvaloniaProperty.Register( + nameof(IconBinding)); + + [AssignBinding] + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IBinding? IconBinding + { + get => GetValue(IconBindingProperty); + set => SetValue(IconBindingProperty, value); + } + + public static readonly StyledProperty CommandBindingProperty = AvaloniaProperty.Register( + nameof(CommandBinding)); + + [AssignBinding] + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IBinding? CommandBinding + { + get => GetValue(CommandBindingProperty); + set => SetValue(CommandBindingProperty, value); + } + + public static readonly StyledProperty SeparatorProperty = AvaloniaProperty.Register( + nameof(Separator)); + + /// + /// Separator between items. + /// Usage: Separator can only be raw string or ITemplate<Control>. + /// + public object? Separator + { + get => GetValue(SeparatorProperty); + set => SetValue(SeparatorProperty, value); + } + + public static readonly StyledProperty IconTemplateProperty = AvaloniaProperty.Register( + nameof(IconTemplate)); + + public IDataTemplate? IconTemplate + { + get => GetValue(IconTemplateProperty); + set => SetValue(IconTemplateProperty, value); + } + + static Breadcrumb() + { + 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 BreadcrumbItem(); + } + + protected override void PrepareContainerForItemOverride(Control container, object? item, int index) + { + // base.PrepareContainerForItemOverride(container, item, index); + if (container is not BreadcrumbItem breadcrumbItem) return; + if (!breadcrumbItem.IsSet(BreadcrumbItem.SeparatorProperty)) + { + if (GetSeparatorInstance(Separator) is { } a) + { + breadcrumbItem.Separator = a; + } + SeparatorProperty.Changed.AddClassHandler((_, args) => + { + if (GetSeparatorInstance(args.NewValue.Value) is { } b) + breadcrumbItem.Separator = b; + }); + } + + PseudolassesExtensions.Set(container.Classes, BreadcrumbItem.PC_Last, index == ItemCount - 1); + + if (container == item) return; + if(!breadcrumbItem.IsSet(ContentControl.ContentProperty)) + { + breadcrumbItem.SetCurrentValue(ContentControl.ContentProperty, item); + if (DisplayMemberBinding is not null) + { + breadcrumbItem[!ContentControl.ContentProperty] = DisplayMemberBinding; + } + } + if (!breadcrumbItem.IsSet(ContentControl.ContentTemplateProperty) && this.ItemTemplate != null) + { + breadcrumbItem.SetCurrentValue(ContentControl.ContentTemplateProperty, this.ItemTemplate); + } + if (!breadcrumbItem.IsSet(BreadcrumbItem.IconProperty) && IconBinding != null) + { + breadcrumbItem[!BreadcrumbItem.IconProperty] = IconBinding; + } + if (!breadcrumbItem.IsSet(BreadcrumbItem.CommandProperty) && CommandBinding != null) + { + breadcrumbItem[!BreadcrumbItem.CommandProperty] = CommandBinding; + } + if (!breadcrumbItem.IsSet(BreadcrumbItem.IconTemplateProperty) && IconTemplate != null) + { + breadcrumbItem.IconTemplate = IconTemplate; + } + } + + private static object? GetSeparatorInstance(object? separator) => separator switch + { + null => null, + string s => s, + ITemplate t => t.Build(), + _ => separator.ToString() + }; +} \ No newline at end of file diff --git a/src/Ursa/Controls/Breadcrumb/BreadcrumbItem.cs b/src/Ursa/Controls/Breadcrumb/BreadcrumbItem.cs new file mode 100644 index 0000000..64aaf9f --- /dev/null +++ b/src/Ursa/Controls/Breadcrumb/BreadcrumbItem.cs @@ -0,0 +1,69 @@ +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.LogicalTree; + +namespace Ursa.Controls; + +[PseudoClasses(PC_Last)] +public class BreadcrumbItem: ContentControl +{ + public const string PC_Last = ":last"; + public static readonly StyledProperty SeparatorProperty = + AvaloniaProperty.Register( + nameof(Separator)); + + public object? Separator + { + get => GetValue(SeparatorProperty); + set => SetValue(SeparatorProperty, value); + } + + public static readonly StyledProperty IconProperty = AvaloniaProperty.Register( + nameof(Icon)); + + public object? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + public static readonly StyledProperty CommandProperty = AvaloniaProperty.Register( + nameof(Command)); + + public ICommand? Command + { + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + public static readonly StyledProperty IconTemplateProperty = AvaloniaProperty.Register( + nameof(IconTemplate)); + + public IDataTemplate? IconTemplate + { + get => GetValue(IconTemplateProperty); + set => SetValue(IconTemplateProperty, value); + } + + public static readonly StyledProperty IsReadOnlyProperty = AvaloniaProperty.Register( + nameof(IsReadOnly)); + + public bool IsReadOnly + { + get => GetValue(IsReadOnlyProperty); + set => SetValue(IsReadOnlyProperty, value); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + if (!IsReadOnly) + { + Command?.Execute(null); + } + } +} \ No newline at end of file