feat: add inheritance.

This commit is contained in:
Dong Bin
2025-07-08 14:58:58 +08:00
parent fb799636d9
commit 85016c9e37
4 changed files with 82 additions and 58 deletions

View File

@@ -6,11 +6,14 @@
xmlns:u="https://irihi.tech/ursa"> xmlns:u="https://irihi.tech/ursa">
<converters:TreeLevelToMarginConverter x:Key="LevelToMarginConverter" /> <converters:TreeLevelToMarginConverter x:Key="LevelToMarginConverter" />
<ControlTheme x:Key="{x:Type u:Anchor}" TargetType="{x:Type u:Anchor}"> <ControlTheme x:Key="{x:Type u:Anchor}" TargetType="{x:Type u:Anchor}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Panel> <Panel>
<Rectangle <Rectangle
Width="1" Width="1"
Name="PART_Pipe"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Fill="{DynamicResource SemiColorBorder}" /> Fill="{DynamicResource SemiColorBorder}" />
@@ -21,23 +24,30 @@
</Panel> </Panel>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
<Style Selector="^.Mute /template/ Rectangle#PART_Pipe">
<Setter Property="Fill" Value="Transparent" />
</Style>
</ControlTheme> </ControlTheme>
<ControlTheme x:Key="{x:Type u:AnchorItem}" TargetType="u:AnchorItem"> <ControlTheme x:Key="{x:Type u:AnchorItem}" TargetType="u:AnchorItem">
<Setter Property="Background" Value="Transparent" /> <Setter Property="Background" Value="Transparent" />
<Setter Property="MinHeight" Value="20" /> <Setter Property="Cursor" Value="Hand" />
<Setter Property="iri:ClassHelper.ClassSource" Value="{Binding $parent[u:Anchor]}" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="u:AnchorItem"> <ControlTemplate TargetType="u:AnchorItem">
<StackPanel> <StackPanel>
<Panel> <Panel Background="{TemplateBinding Background}" >
<Rectangle <Border
Name="PART_Pipe" Name="PART_Pipe"
Width="2" Width="2"
CornerRadius="1"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Stretch" /> VerticalAlignment="Stretch" />
<Panel Margin="8,0,0,0"> <Panel Margin="8,0,0,0">
<ContentPresenter <ContentPresenter
Name="{x:Static iri:PartNames.PART_HeaderPresenter}" Name="{x:Static iri:PartNames.PART_HeaderPresenter}"
VerticalAlignment="Center"
Foreground="{DynamicResource SemiColorText2}"
Content="{TemplateBinding Header}" Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"> ContentTemplate="{TemplateBinding HeaderTemplate}">
<ContentPresenter.Margin> <ContentPresenter.Margin>
@@ -53,8 +63,24 @@
</StackPanel> </StackPanel>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
<Style Selector="^:selected /template/ Rectangle#PART_Pipe"> <Style Selector="^ /template/ ContentPresenter#PART_HeaderPresenter">
<Setter Property="Fill" Value="{DynamicResource SemiBlue5}" /> <Setter Property="MinHeight" Value="{DynamicResource AnchorDefaultHeight}" />
</Style>
<Style Selector="^.Small /template/ ContentPresenter#PART_HeaderPresenter">
<Setter Property="MinHeight" Value="{DynamicResource AnchorSmallHeight}" />
<Setter Property="FontSize" Value="{DynamicResource SemiFontSizeSmall}" />
</Style>
<Style Selector="^:selected /template/ Border#PART_Pipe">
<Setter Property="Background" Value="{DynamicResource SemiColorTertiary}" />
</Style>
<Style Selector="^.Primary:selected /template/ Border#PART_Pipe">
<Setter Property="Background" Value="{DynamicResource SemiColorPrimary}" />
</Style>
<Style Selector="^.Mute:selected /template/ Border#PART_Pipe">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="^:selected /template/ ContentPresenter#PART_HeaderPresenter">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style> </Style>
</ControlTheme> </ControlTheme>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -1,4 +1,6 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" <ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<x:Double x:Key="AnchorIndent">12</x:Double> <x:Double x:Key="AnchorIndent">12</x:Double>
<x:Double x:Key="AnchorDefaultHeight">20</x:Double>
<x:Double x:Key="AnchorSmallHeight">16</x:Double>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -11,15 +11,27 @@ using Ursa.Common;
namespace Ursa.Controls; namespace Ursa.Controls;
/// <summary> /// <summary>
/// Some basic assumptions: This should not be a regular SelectingItemsControl, because it does not support multiple selections. /// Some basic assumptions: This should not be a regular SelectingItemsControl, because it does not support multiple
/// Selection should not be exposed to the user, it is only used to determine which item is currently selected. /// selections.
/// The manipulation of container selection should be simplified. /// Selection should not be exposed to the user, it is only used to determine which item is currently selected.
/// Scroll event of TargetContainer also triggers selection change. /// The manipulation of container selection should be simplified.
/// Scroll event of TargetContainer also triggers selection change.
/// </summary> /// </summary>
public class Anchor: ItemsControl public class Anchor : ItemsControl
{ {
public static readonly StyledProperty<ScrollViewer?> TargetContainerProperty = AvaloniaProperty.Register<Anchor, ScrollViewer?>( public static readonly StyledProperty<ScrollViewer?> TargetContainerProperty =
nameof(TargetContainer)); AvaloniaProperty.Register<Anchor, ScrollViewer?>(
nameof(TargetContainer));
public static readonly AttachedProperty<string?> AnchorIdProperty =
AvaloniaProperty.RegisterAttached<Anchor, Visual, string?>("AnchorId");
private CancellationTokenSource _cts = new();
private List<(string, double)> _positions = [];
private bool _scrollingFromSelection;
private AnchorItem? _selectedContainer;
public ScrollViewer? TargetContainer public ScrollViewer? TargetContainer
{ {
@@ -27,11 +39,15 @@ public class Anchor: ItemsControl
set => SetValue(TargetContainerProperty, value); set => SetValue(TargetContainerProperty, value);
} }
public static readonly AttachedProperty<string?> AnchorIdProperty = public static void SetAnchorId(Visual obj, string? value)
AvaloniaProperty.RegisterAttached<Anchor, Visual, string?>("AnchorId"); {
obj.SetValue(AnchorIdProperty, value);
}
public static void SetAnchorId(Visual obj, string? value) => obj.SetValue(AnchorIdProperty, value); public static string? GetAnchorId(Visual obj)
public static string? GetAnchorId(Visual obj) => obj.GetValue(AnchorIdProperty); {
return obj.GetValue(AnchorIdProperty);
}
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
@@ -45,9 +61,6 @@ public class Anchor: ItemsControl
return i; return i;
} }
private CancellationTokenSource _cts = new();
private bool _scrollingFromSelection;
private void ScrollToAnchor(Visual target) private void ScrollToAnchor(Visual target)
{ {
if (TargetContainer is null) if (TargetContainer is null)
@@ -58,38 +71,30 @@ public class Anchor: ItemsControl
{ {
var from = TargetContainer.Offset.Y; var from = TargetContainer.Offset.Y;
var to = TargetContainer.Offset.Y + targetPosition.Value.Y; var to = TargetContainer.Offset.Y + targetPosition.Value.Y;
if(to > TargetContainer.Extent.Height - TargetContainer.Bounds.Height) if (to > TargetContainer.Extent.Height - TargetContainer.Bounds.Height)
{
to = TargetContainer.Extent.Height - TargetContainer.Bounds.Height; to = TargetContainer.Extent.Height - TargetContainer.Bounds.Height;
}
if (from == to) return; if (from == to) return;
Animation animation = new Animation() var animation = new Animation
{ {
Duration = TimeSpan.FromSeconds(0.3), Duration = TimeSpan.FromSeconds(0.3),
Easing = new QuadraticEaseOut(), Easing = new QuadraticEaseOut(),
Children = Children =
{ {
new KeyFrame(){ new KeyFrame
Setters = {
{ Setters = { new Setter(ScrollViewer.OffsetProperty, new Vector(0, from)) },
new Setter(ScrollViewer.OffsetProperty, new Vector(0, from)),
},
Cue = new Cue(0.0) Cue = new Cue(0.0)
}, },
new KeyFrame() new KeyFrame
{ {
Setters = Setters = { new Setter(ScrollViewer.OffsetProperty, new Vector(0, to)) },
{
new Setter(ScrollViewer.OffsetProperty, new Vector(0, to))
},
Cue = new Cue(1.0) Cue = new Cue(1.0)
} }
} }
}; };
_cts.Cancel(); _cts.Cancel();
_cts = new CancellationTokenSource(); _cts = new CancellationTokenSource();
var token = _cts.Token; var token = _cts.Token;
token.Register(_ => _scrollingFromSelection = false, null); token.Register(_ => _scrollingFromSelection = false, null);
_scrollingFromSelection = true; _scrollingFromSelection = true;
animation.RunAsync(TargetContainer, token).ContinueWith(_ => _scrollingFromSelection = false, token); animation.RunAsync(TargetContainer, token).ContinueWith(_ => _scrollingFromSelection = false, token);
@@ -102,23 +107,19 @@ public class Anchor: ItemsControl
MarkSelectedContainerByPosition(); MarkSelectedContainerByPosition();
} }
private List<(string, double)> _positions = [];
internal void InvalidateAnchorPositions() internal void InvalidateAnchorPositions()
{ {
if (TargetContainer is null) return; if (TargetContainer is null) return;
var items = TargetContainer.GetVisualDescendants().Where(a => GetAnchorId(a) is not null); var items = TargetContainer.GetVisualDescendants().Where(a => GetAnchorId(a) is not null);
List<(string, double)> positions = new List<(string, double)>(); var positions = new List<(string, double)>();
foreach (var item in items) foreach (var item in items)
{ {
var anchorId = GetAnchorId(item); var anchorId = GetAnchorId(item);
if (anchorId is null) continue; if (anchorId is null) continue;
var position = item.TransformToVisual(TargetContainer)?.M32 + TargetContainer.Offset.Y; var position = item.TransformToVisual(TargetContainer)?.M32 + TargetContainer.Offset.Y;
if (position.HasValue) if (position.HasValue) positions.Add((anchorId, position.Value));
{
positions.Add((anchorId, position.Value));
}
} }
positions.Sort((a, b) => a.Item2.CompareTo(b.Item2)); positions.Sort((a, b) => a.Item2.CompareTo(b.Item2));
_positions = positions; _positions = positions;
} }
@@ -126,14 +127,11 @@ public class Anchor: ItemsControl
protected override void OnLoaded(RoutedEventArgs e) protected override void OnLoaded(RoutedEventArgs e)
{ {
base.OnLoaded(e); base.OnLoaded(e);
var target = this.TargetContainer; var target = TargetContainer;
if (target is null) return; if (target is null) return;
TargetContainer?.AddHandler(ScrollViewer.ScrollChangedEvent, OnScrollChanged); TargetContainer?.AddHandler(ScrollViewer.ScrollChangedEvent, OnScrollChanged);
TargetContainer?.AddHandler(LoadedEvent, OnTargetContainerLoaded); TargetContainer?.AddHandler(LoadedEvent, OnTargetContainerLoaded);
if (TargetContainer?.IsLoaded == true) if (TargetContainer?.IsLoaded == true) InvalidateAnchorPositions();
{
InvalidateAnchorPositions();
}
MarkSelectedContainerByPosition(); MarkSelectedContainerByPosition();
} }
@@ -150,13 +148,13 @@ public class Anchor: ItemsControl
if (source is null) return; if (source is null) return;
MarkSelectedContainer(source); MarkSelectedContainer(source);
var target = TargetContainer?.GetVisualDescendants() var target = TargetContainer?.GetVisualDescendants()
.FirstOrDefault(a => Anchor.GetAnchorId(a) == source.AnchorId); .FirstOrDefault(a => GetAnchorId(a) == source.AnchorId);
if (target is null) return; if (target is null) return;
ScrollToAnchor(target); ScrollToAnchor(target);
} }
/// <summary> /// <summary>
/// This method is used to expose the protected CreateContainerForItemOverride method to the AnchorItem class. /// This method is used to expose the protected CreateContainerForItemOverride method to the AnchorItem class.
/// </summary> /// </summary>
internal Control CreateContainerForItemOverride_INTERNAL(object? item, int index, object? recycleKey) internal Control CreateContainerForItemOverride_INTERNAL(object? item, int index, object? recycleKey)
{ {
@@ -178,8 +176,6 @@ public class Anchor: ItemsControl
ContainerForItemPreparedOverride(container, item, index); ContainerForItemPreparedOverride(container, item, index);
} }
private AnchorItem? _selectedContainer;
internal void MarkSelectedContainer(AnchorItem? item) internal void MarkSelectedContainer(AnchorItem? item)
{ {
var oldValue = _selectedContainer; var oldValue = _selectedContainer;
@@ -197,7 +193,7 @@ public class Anchor: ItemsControl
var topAnchorId = _positions.LastOrDefault(a => a.Item2 <= top).Item1; var topAnchorId = _positions.LastOrDefault(a => a.Item2 <= top).Item1;
if (topAnchorId is null) return; if (topAnchorId is null) return;
var item = this.GetVisualDescendants().OfType<AnchorItem>() var item = this.GetVisualDescendants().OfType<AnchorItem>()
.FirstOrDefault(a => a.AnchorId == topAnchorId); .FirstOrDefault(a => a.AnchorId == topAnchorId);
if (item is null) return; if (item is null) return;
MarkSelectedContainer(item); MarkSelectedContainer(item);
} }

View File

@@ -31,7 +31,7 @@ public class AnchorItem : HeaderedItemsControl, ISelectable
{ {
SelectableMixin.Attach<AnchorItem>(IsSelectedProperty); SelectableMixin.Attach<AnchorItem>(IsSelectedProperty);
PressedMixin.Attach<AnchorItem>(); PressedMixin.Attach<AnchorItem>();
ItemsPanelProperty.OverrideDefaultValue<TreeViewItem>(DefaultPanel); ItemsPanelProperty.OverrideDefaultValue<AnchorItem>(DefaultPanel);
} }
public int Level public int Level