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">
<converters:TreeLevelToMarginConverter x:Key="LevelToMarginConverter" />
<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">
<ControlTemplate>
<Panel>
<Rectangle
Width="1"
Name="PART_Pipe"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Fill="{DynamicResource SemiColorBorder}" />
@@ -21,23 +24,30 @@
</Panel>
</ControlTemplate>
</Setter>
<Style Selector="^.Mute /template/ Rectangle#PART_Pipe">
<Setter Property="Fill" Value="Transparent" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:AnchorItem}" TargetType="u:AnchorItem">
<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">
<ControlTemplate TargetType="u:AnchorItem">
<StackPanel>
<Panel>
<Rectangle
<Panel Background="{TemplateBinding Background}" >
<Border
Name="PART_Pipe"
Width="2"
CornerRadius="1"
HorizontalAlignment="Left"
VerticalAlignment="Stretch" />
<Panel Margin="8,0,0,0">
<ContentPresenter
Name="{x:Static iri:PartNames.PART_HeaderPresenter}"
VerticalAlignment="Center"
Foreground="{DynamicResource SemiColorText2}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}">
<ContentPresenter.Margin>
@@ -53,8 +63,24 @@
</StackPanel>
</ControlTemplate>
</Setter>
<Style Selector="^:selected /template/ Rectangle#PART_Pipe">
<Setter Property="Fill" Value="{DynamicResource SemiBlue5}" />
<Style Selector="^ /template/ ContentPresenter#PART_HeaderPresenter">
<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>
</ControlTheme>
</ResourceDictionary>

View File

@@ -1,4 +1,6 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<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>

View File

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

View File

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