feat: update to finish functionality, start to build theme.

This commit is contained in:
rabbitism
2023-06-23 02:46:37 +08:00
parent bacf1a6330
commit c21e571b74
7 changed files with 475 additions and 62 deletions

View File

@@ -1,42 +1,171 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Ursa.Themes.Semi.Converters"
xmlns:u="https://irihi.tech/ursa">
<!-- Add Resources Here -->
<converters:NavigationMenuItemLevelToMarginConverter x:Key="MarginConverter" Indent="8" />
<ControlTheme x:Key="{x:Type u:NavigationMenu}" TargetType="u:NavigationMenu">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate TargetType="u:NavigationMenu">
<Border Background="{TemplateBinding Background}">
<StackPanel>
<ContentPresenter Name="PART_HeaderPresenter" Content="Hello" />
<ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}" />
</StackPanel>
<Border
Name="PART_RootBorder"
Width="260"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Border.Transitions>
<Transitions>
<DoubleTransition Property="Width" Duration="0:0:0.2" />
</Transitions>
</Border.Transitions>
<Grid
Name="PART_RootGrid"
HorizontalAlignment="Stretch"
Background="{TemplateBinding Background}"
RowDefinitions="Auto, *, Auto, Auto">
<StackPanel
Margin="18"
HorizontalAlignment="Center"
Orientation="Horizontal">
<ContentPresenter
Name="PART_IconPresenter"
Margin="0,8"
VerticalAlignment="Center"
Content="{TemplateBinding Icon}" />
<ContentPresenter
Name="PART_HeaderPresenter"
VerticalAlignment="Center"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
FontSize="18" />
</StackPanel>
<ItemsPresenter Grid.Row="1" ItemsPanel="{TemplateBinding ItemsPanel}" />
<ContentPresenter
Name="PART_FooterPresenter"
Grid.Row="2"
Content="{TemplateBinding Footer}"
ContentTemplate="{TemplateBinding FooterTemplate}" />
<ToggleButton
Name="{x:Static u:NavigationMenu.PART_CloseButton}"
Grid.Row="3"
Content="Open"
IsChecked="{TemplateBinding IsClosed,
Mode=TwoWay}"
IsVisible="{TemplateBinding ShowCollapseButton}" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:closed">
<Style Selector="^ /template/ ContentPresenter#PART_HeaderPresenter">
<Setter Property="ContentPresenter.IsVisible" Value="False" />
</Style>
<Style Selector="^ /template/ Border#PART_RootBorder">
<Setter Property="Border.Width" Value="60" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_IconPresenter">
<Setter Property="Grid.HorizontalAlignment" Value="Center" />
</Style>
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:NavigationMenuItem}" TargetType="u:NavigationMenuItem">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<ControlTemplate TargetType="u:NavigationMenuItem">
<Border
Padding="4"
BorderBrush="Black"
BorderThickness="1">
<Border>
<StackPanel>
<ContentPresenter
Name="PART_HeaderPresenter"
Background="{TemplateBinding Background}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}" />
<ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}" />
<Border
Name="PART_HeaderBorder"
Margin="0,2"
Padding="8"
Background="Transparent"
CornerRadius="6">
<Grid
Name="PART_RootStackPanel"
HorizontalAlignment="Stretch"
ColumnDefinitions="Auto, Auto, *, Auto">
<ContentPresenter
Name="PART_IconPresenter"
Grid.Column="1"
MinWidth="32"
Margin="{TemplateBinding Level,
Converter={StaticResource MarginConverter}}"
Content="{TemplateBinding Icon}"
ContentTemplate="{TemplateBinding IconTemplate}" />
<ContentPresenter
Name="PART_HeaderPresenter"
Grid.Column="2"
VerticalAlignment="Center"
Background="{TemplateBinding Background}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}">
<ContentPresenter.Transitions>
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.15" />
</Transitions>
</ContentPresenter.Transitions>
</ContentPresenter>
<PathIcon
Name="PART_ExpandIcon"
Grid.Column="3"
Width="8"
Height="8"
Data="M4.08045 7.59809C4.66624 7.01231 5.61599 7.01231 6.20177 7.59809L11.8586 13.2549L17.5155 7.59809C18.1013 7.01231 19.051 7.01231 19.6368 7.59809C20.2226 8.18388 20.2226 9.13363 19.6368 9.71941L12.9193 16.4369C12.3335 17.0227 11.3838 17.0227 10.798 16.4369L4.08045 9.71941C3.49467 9.13363 3.49467 8.18388 4.08045 7.59809Z">
<PathIcon.Transitions>
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0.1" />
</Transitions>
</PathIcon.Transitions>
</PathIcon>
</Grid>
</Border>
<ItemsPresenter Name="PART_ItemsPresenter" ItemsPanel="{TemplateBinding ItemsPanel}">
<ItemsPresenter.Transitions>
<Transitions>
<DoubleTransition Property="Height" Duration="0:0:0.15" />
<DoubleTransition Property="Opacity" Duration="0:0:0.15" />
</Transitions>
</ItemsPresenter.Transitions>
</ItemsPresenter>
</StackPanel>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:selected">
<Setter Property="u:NavigationMenuItem.Background" Value="Red" />
<Style Selector="^ /template/ Border#PART_HeaderBorder:pointerover">
<Setter Property="Border.Background" Value="LightGray" />
</Style>
<Style Selector="^:highlighted /template/ ContentPresenter#PART_IconPresenter">
<Setter Property="u:NavigationMenuItem.Foreground" Value="Blue" />
</Style>
<Style Selector="^:selected /template/ Border#PART_HeaderBorder">
<Setter Property="u:NavigationMenuItem.Background" Value="LightBlue" />
</Style>
<Style Selector="^:closed /template/ ContentPresenter#PART_HeaderPresenter">
<Setter Property="ContentPresenter.Opacity" Value="0" />
</Style>
<Style Selector="^:closed /template/ StackPanel#PART_RootStackPanel">
<Setter Property="StackPanel.HorizontalAlignment" Value="Center" />
</Style>
<Style Selector="^:closed /template/ ItemsPresenter#PART_ItemsPresenter">
<Setter Property="ItemsPresenter.IsVisible" Value="False" />
</Style>
<Style Selector="^:collapsed /template/ ItemsPresenter#PART_ItemsPresenter">
<Setter Property="ItemsPresenter.Height" Value="0" />
<Setter Property="ItemsPresenter.Opacity" Value="0" />
</Style>
<Style Selector="^:not(:empty):collapsed /template/ PathIcon#PART_ExpandIcon">
<Setter Property="PathIcon.RenderTransform" Value="rotate(180deg)" />
</Style>
<Style Selector="^:empty /template/ PathIcon#PART_ExpandIcon">
<Setter Property="PathIcon.IsVisible" Value="False" />
</Style>
</ControlTheme>
</ResourceDictionary>

View File

@@ -0,0 +1,23 @@
using System.Globalization;
using Avalonia;
using Avalonia.Data.Converters;
namespace Ursa.Themes.Semi.Converters;
public class NavigationMenuItemLevelToMarginConverter: IValueConverter
{
public int Indent { get; set; }
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is int i)
{
return new Thickness((i-1) * Indent, 0, 0, 0);
}
return new Thickness();
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Specialized;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
@@ -12,8 +13,14 @@ using Avalonia.Metadata;
namespace Ursa.Controls;
public class NavigationMenu: HeaderedSelectingItemsControl
[PseudoClasses(PC_Closed)]
[TemplatePart(Name = PART_CloseButton, Type = typeof(ToggleButton))]
public class NavigationMenu: HeaderedItemsControl
{
public const string PC_Closed = ":closed";
public const string PART_CloseButton = "PART_CloseButton";
public static readonly StyledProperty<object?> FooterProperty = AvaloniaProperty.Register<NavigationMenu, object?>(
nameof(Footer));
@@ -32,13 +39,82 @@ public class NavigationMenu: HeaderedSelectingItemsControl
set => SetValue(FooterTemplateProperty, value);
}
public static readonly StyledProperty<object?> SelectedMenuItemProperty = AvaloniaProperty.Register<NavigationMenu, object?>(
nameof(SelectedMenuItem));
public static readonly StyledProperty<object?> IconProperty = AvaloniaProperty.Register<NavigationMenu, object?>(
nameof(Icon));
public object? SelectedMenuItem
public object? Icon
{
get => GetValue(SelectedMenuItemProperty);
set => SetValue(SelectedMenuItemProperty, value);
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public static readonly StyledProperty<object?> SelectedItemProperty = AvaloniaProperty.Register<NavigationMenu, object?>(
nameof(SelectedItem));
public object? SelectedItem
{
get => GetValue(SelectedItemProperty);
set => SetValue(SelectedItemProperty, value);
}
public static readonly StyledProperty<bool> ShowCollapseButtonProperty = AvaloniaProperty.Register<NavigationMenu, bool>(
nameof(ShowCollapseButton));
public bool ShowCollapseButton
{
get => GetValue(ShowCollapseButtonProperty);
set => SetValue(ShowCollapseButtonProperty, value);
}
public static readonly StyledProperty<bool> IsClosedProperty = AvaloniaProperty.Register<NavigationMenu, bool>(
nameof(IsClosed));
public bool IsClosed
{
get => GetValue(IsClosedProperty);
set => SetValue(IsClosedProperty, value);
}
public static readonly StyledProperty<double> OpenedWidthProperty = AvaloniaProperty.Register<NavigationMenu, double>(
nameof(OpenedWidth));
public double OpenedWidth
{
get => GetValue(OpenedWidthProperty);
set => SetValue(OpenedWidthProperty, value);
}
public static readonly StyledProperty<double> ClosedWidthProperty = AvaloniaProperty.Register<NavigationMenu, double>(
nameof(ClosedWidth));
public double ClosedWidth
{
get => GetValue(ClosedWidthProperty);
set => SetValue(ClosedWidthProperty, value);
}
static NavigationMenu()
{
SelectedItemProperty.Changed.AddClassHandler<NavigationMenu>((o, e) => o.OnSelectionItemChanged(e));
IsClosedProperty.Changed.AddClassHandler<NavigationMenu>((o,e)=>o.OnIsClosedChanged(e));
}
private void OnSelectionItemChanged(AvaloniaPropertyChangedEventArgs args)
{
var newItem = args.GetNewValue<object?>();
if (newItem is not null)
{
UpdateSelectionFromSelectedItem(newItem);
}
}
private void OnIsClosedChanged(AvaloniaPropertyChangedEventArgs args)
{
bool newValue = args.GetNewValue<bool>();
PseudoClasses.Set(PC_Closed, newValue);
}
internal void UpdateSelection(NavigationMenuItem source)
@@ -48,15 +124,7 @@ public class NavigationMenu: HeaderedSelectingItemsControl
{
foreach (var child in children)
{
NavigationMenuItem? item = null;
if (child is NavigationMenuItem i)
{
item = i;
}
else if (child is ContentPresenter { Child: NavigationMenuItem i2 })
{
item = i2;
}
NavigationMenuItem? item = NavigationMenuItem.GetMenuItemFromControl(child);
if (item != null)
{
if(Equals(item, source)) continue;
@@ -65,4 +133,18 @@ public class NavigationMenu: HeaderedSelectingItemsControl
}
}
}
private void UpdateSelectionFromSelectedItem(object? o)
{
var children = this.ItemsPanelRoot?.Children;
if (children is not null)
{
foreach (var child in children)
{
NavigationMenuItem? item = NavigationMenuItem.GetMenuItemFromControl(child);
if(item is null) continue;
item.UpdateSelectionFromSelectedItem(o);
}
}
}
}

View File

@@ -1,8 +1,10 @@
using System.Security.Cryptography.X509Certificates;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.LogicalTree;
@@ -12,16 +14,68 @@ using Avalonia.VisualTree;
namespace Ursa.Controls;
[PseudoClasses(PC_Closed, PC_Selected, PC_Empty)]
[PseudoClasses(PC_Closed, PC_Selected, PC_Highlighted, PC_Collapsed)]
public class NavigationMenuItem: HeaderedSelectingItemsControl
{
public const string PC_Closed = ":closed";
public const string PC_Selected = ":selected";
public const string PC_Empty = ":empty";
public const string PC_Highlighted= ":highlighted";
public const string PC_Collapsed = ":collapsed";
private NavigationMenu? _rootMenu;
private IDisposable? _ownerSubscription;
private IDisposable? _itemsBinding;
private bool _isCollapsed;
public static readonly StyledProperty<bool> IsClosedProperty = AvaloniaProperty.Register<NavigationMenuItem, bool>(
nameof(IsClosed));
public bool IsClosed
{
get => GetValue(IsClosedProperty);
set => SetValue(IsClosedProperty, value);
}
public static readonly StyledProperty<object?> IconProperty = AvaloniaProperty.Register<NavigationMenuItem, object?>(
nameof(Icon));
public object? Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public static readonly StyledProperty<IDataTemplate> IconTemplateProperty = AvaloniaProperty.Register<NavigationMenuItem, IDataTemplate>(
nameof(IconTemplate));
public IDataTemplate IconTemplate
{
get => GetValue(IconTemplateProperty);
set => SetValue(IconTemplateProperty, value);
}
private int _level;
public static readonly DirectProperty<NavigationMenuItem, int> LevelProperty = AvaloniaProperty.RegisterDirect<NavigationMenuItem, int>(
nameof(Level), o => o.Level);
public int Level
{
get => _level;
private set => SetAndRaise(LevelProperty, ref _level, value);
}
static NavigationMenuItem()
{
IsClosedProperty.Changed.AddClassHandler<NavigationMenuItem>((o, e) => o.OnIsClosedChanged(e));
}
private void OnIsClosedChanged(AvaloniaPropertyChangedEventArgs args)
{
bool newValue = args.GetNewValue<bool>();
PseudoClasses.Set(PC_Closed, newValue);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
@@ -35,6 +89,11 @@ public class NavigationMenuItem: HeaderedSelectingItemsControl
{
SetCurrentValue(ItemContainerThemeProperty, _rootMenu.ItemContainerTheme);
}
_rootMenu?.GetObservable(NavigationMenu.IsClosedProperty)
.Subscribe(new AnonymousObserver<bool>(a => this.IsClosed = a));
Level = CalculateDistanceFromLogicalParent<NavigationMenu>(this) - 1;
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
@@ -47,31 +106,38 @@ public class NavigationMenuItem: HeaderedSelectingItemsControl
if (_rootMenu is not null && parents.Contains(_rootMenu))
{
object? o = this.DataContext ?? this;
_rootMenu.SelectedMenuItem = o;
_rootMenu.SelectedItem = o;
}
SetSelection(this, true, true);
}
// Non-leaf node, act as a toggle button.
else
{
_isCollapsed = !_isCollapsed;
this.PseudoClasses.Set(PC_Collapsed, _isCollapsed);
}
e.Handled = true;
SetSelection(this, true, true);
}
internal void SetSelection(NavigationMenuItem? source, bool selected, bool propagateToParent = false)
{
this.PseudoClasses.Set(PC_Selected, selected);
if (Equals(this, source) && this.ItemCount == 0)
{
this.PseudoClasses.Set(PC_Highlighted, selected);
this.PseudoClasses.Set(PC_Selected, selected);
}
else
{
this.PseudoClasses.Set(PC_Selected, false);
this.PseudoClasses.Set(PC_Highlighted, selected);
}
var children = this.ItemsPanelRoot?.Children;
if (children is not null)
{
foreach (var child in children)
{
NavigationMenuItem? item = null;
if (child is NavigationMenuItem i)
{
item = i;
}
else if (child is ContentPresenter { Child: NavigationMenuItem i2 })
{
item = i2;
}
NavigationMenuItem? item = GetMenuItemFromControl(child);
if (item != null)
{
if(Equals(item, source)) continue;
@@ -96,4 +162,54 @@ public class NavigationMenuItem: HeaderedSelectingItemsControl
}
}
}
internal void UpdateSelectionFromSelectedItem(object? o)
{
if (o is null)
{
this.SetSelection(this, false, false);
return;
}
if (Equals(this, o) || Equals(this.DataContext, o))
{
this.SetSelection(this, true, true);
}
else
{
var children = this.ItemsPanelRoot?.Children;
if (children is not null)
{
foreach (var child in children)
{
NavigationMenuItem? item = GetMenuItemFromControl(child);
if (item != null)
{
item.UpdateSelectionFromSelectedItem(o);
}
}
}
}
}
private static int CalculateDistanceFromLogicalParent<T>(ILogical? logical, int @default = -1) where T : class
{
var result = 0;
while (logical != null && !(logical is T))
{
++result;
logical = logical.LogicalParent;
}
return logical != null ? result : @default;
}
public static NavigationMenuItem? GetMenuItemFromControl(Control? control)
{
if (control is null) return null;
if (control is NavigationMenuItem item) return item;
if (control is ContentPresenter { Child: NavigationMenuItem item2 }) return item2;
return null;
}
}