Merge pull request #101 from irihitech/selection

SelectionList
This commit is contained in:
Dong Bin
2024-02-08 20:18:04 +08:00
committed by GitHub
10 changed files with 347 additions and 0 deletions

View File

@@ -22,6 +22,7 @@ public static class MenuKeys
public const string MenuKeyNumericUpDown = "NumericUpDown";
public const string MenuKeyPagination = "Pagination";
public const string MenuKeyRangeSlider = "RangeSlider";
public const string MenuKeySelectionList = "SelectionList";
public const string MenuKeyTagInput = "TagInput";
public const string MenuKeyTimeline = "Timeline";
public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon";

View File

@@ -0,0 +1,55 @@
<UserControl
x:Class="Ursa.Demo.Pages.SelectionListDemo"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:u="https://irihi.tech/ursa"
xmlns:vm="using:Ursa.Demo.ViewModels"
d:DesignHeight="450"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="vm:SelectionListDemoViewModel"
mc:Ignorable="d">
<StackPanel HorizontalAlignment="Left">
<u:SelectionList ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}">
<u:SelectionList.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</u:SelectionList.ItemsPanel>
</u:SelectionList>
<u:SelectionList ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" />
<u:SelectionList ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}">
<u:SelectionList.Indicator>
<Border Background="Transparent" CornerRadius="4">
<Border
Width="4"
Margin="0,8"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Background="{DynamicResource SemiBlue6}"
CornerRadius="4" />
</Border>
</u:SelectionList.Indicator>
<u:SelectionList.ItemTemplate>
<DataTemplate>
<Panel Height="40">
<TextBlock
Classes.Active="{Binding $parent[u:SelectionListItem].IsSelected, Mode=OneWay}"
Margin="8,0"
VerticalAlignment="Center"
Text="{Binding}">
<TextBlock.Styles>
<Style Selector="TextBlock.Active">
<Setter Property="Foreground" Value="{DynamicResource SemiOrange6}" />
</Style>
</TextBlock.Styles>
</TextBlock>
</Panel>
</DataTemplate>
</u:SelectionList.ItemTemplate>
</u:SelectionList>
<Button Command="{Binding Clear}">Clear</Button>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Ursa.Demo.Pages;
public partial class SelectionListDemo : UserControl
{
public SelectionListDemo()
{
InitializeComponent();
}
}

View File

@@ -44,6 +44,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(),
MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(),
MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(),
MenuKeys.MenuKeySelectionList => new SelectionListDemoViewModel(),
MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(),
MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(),
MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(),

View File

@@ -31,6 +31,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown, Status = "New" },
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider, Status = "New"},
new() { MenuHeader = "Selection List", Key = MenuKeys.MenuKeySelectionList, Status = "New" },
new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput },
new() { MenuHeader = "Theme Toggler", Key = MenuKeys.MenuKeyThemeToggler },
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline, Status = "Updated" },

View File

@@ -0,0 +1,23 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Ursa.Demo.ViewModels;
public partial class SelectionListDemoViewModel: ObservableObject
{
public ObservableCollection<string> Items { get; set; }
[ObservableProperty] private string? _selectedItem;
public SelectionListDemoViewModel()
{
Items = new ObservableCollection<string>()
{
"Ding", "Otter", "Husky", "Mr. 17", "Cass"
};
}
public void Clear()
{
SelectedItem = null;
}
}

View File

@@ -0,0 +1,61 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="https://irihi.tech/ursa">
<ControlTheme x:Key="{x:Type u:SelectionList}" TargetType="u:SelectionList">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Indicator">
<Template>
<Border
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{DynamicResource SemiBlue1}"
Theme="{DynamicResource CardBorder}" />
</Template>
</Setter>
<Setter Property="ListBox.Template">
<ControlTemplate TargetType="u:SelectionList">
<Border
Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
ClipToBounds="{TemplateBinding ClipToBounds}"
CornerRadius="{TemplateBinding CornerRadius}">
<Panel>
<ContentPresenter Name="{x:Static u:SelectionList.PART_Indicator}" Content="{TemplateBinding Indicator}" />
<ItemsPresenter
Name="PART_ItemsPresenter"
Margin="{TemplateBinding Padding}"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</Panel>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:SelectionListItem}" TargetType="u:SelectionListItem">
<Setter Property="Background" Value="Transparent" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="8" />
<Setter Property="Template">
<ControlTemplate TargetType="u:SelectionListItem">
<ContentPresenter
Name="PART_ContentPresenter"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
Content="{TemplateBinding Content}"
FontWeight="{TemplateBinding FontWeight}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Foreground="{TemplateBinding Foreground}" />
</ControlTemplate>
</Setter>
<Style Selector="^:selected">
<Setter Property="FontWeight" Value="Bold" />
</Style>
</ControlTheme>
</ResourceDictionary>

View File

@@ -21,6 +21,7 @@
<ResourceInclude Source="NumericUpDown.axaml" />
<ResourceInclude Source="Pagination.axaml" />
<ResourceInclude Source="RangeSlider.axaml" />
<ResourceInclude Source="SelectionList.axaml" />
<ResourceInclude Source="TagInput.axaml" />
<ResourceInclude Source="ThemeSelector.axaml" />
<ResourceInclude Source="Timeline.axaml" />

View File

@@ -0,0 +1,153 @@
using System.Runtime.CompilerServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Animations;
using Irihi.Avalonia.Shared.Helpers;
namespace Ursa.Controls;
[TemplatePart(PART_Indicator, typeof(ContentPresenter))]
public class SelectionList: SelectingItemsControl
{
public const string PART_Indicator = "PART_Indicator";
private static readonly FuncTemplate<Panel?> DefaultPanel = new(() => new StackPanel());
private ImplicitAnimationCollection? _implicitAnimations;
private ContentPresenter? _indicator;
public static readonly StyledProperty<Control?> IndicatorProperty = AvaloniaProperty.Register<SelectionList, Control?>(
nameof(Indicator));
public Control? Indicator
{
get => GetValue(IndicatorProperty);
set => SetValue(IndicatorProperty, value);
}
static SelectionList()
{
SelectionModeProperty.OverrideMetadata<SelectionList>(
new StyledPropertyMetadata<SelectionMode>(
defaultValue: SelectionMode.Single,
coerce: (o, mode) => SelectionMode.Single)
);
SelectedItemProperty.Changed.AddClassHandler<SelectionList, object?>((list, args) =>
list.OnSelectedItemChanged(args));
}
private void OnSelectedItemChanged(AvaloniaPropertyChangedEventArgs<object?> args)
{
var newValue = args.NewValue.Value;
if (newValue is null)
{
OpacityProperty.SetValue(0d, _indicator);
return;
}
var container = ContainerFromItem(newValue);
if (container is null)
{
OpacityProperty.SetValue(0d, _indicator);
return;
}
OpacityProperty.SetValue(1d, _indicator);
InvalidateMeasure();
InvalidateArrange();
}
protected override Size ArrangeOverride(Size finalSize)
{
var size = base.ArrangeOverride(finalSize);
if(_indicator is not null && SelectedItem is not null)
{
var container = ContainerFromItem(SelectedItem);
if (container is null) return size;
_indicator.Arrange(container.Bounds);
}
else
{
// This is a hack. The indicator is not visible, so we arrange it to a 1x1 rectangle
_indicator?.Arrange(new Rect(new Point(), new Size(1, 1)));
}
return size;
}
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<SelectionListItem>(item, out recycleKey);
}
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new SelectionListItem();
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_indicator= e.NameScope.Find<ContentPresenter>(PART_Indicator);
EnsureIndicatorAnimation();
}
private void EnsureIndicatorAnimation()
{
if (_indicator is not null)
{
_indicator.Opacity = 0;
SetUpAnimation();
if (ElementComposition.GetElementVisual(_indicator) is { } v)
{
v.ImplicitAnimations = _implicitAnimations;
}
}
}
internal void SelectByIndex(int index)
{
using var operation = Selection.BatchUpdate();
Selection.Clear();
Selection.Select(index);
}
private void SetUpAnimation()
{
if (_implicitAnimations != null) return;
var compositorVisual = ElementComposition.GetElementVisual(this);
if (compositorVisual is null) return;
var compositor = ElementComposition.GetElementVisual(this)!.Compositor;
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = nameof(CompositionVisual.Offset);
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromSeconds(0.3);
var sizeAnimation = compositor.CreateVector2KeyFrameAnimation();
sizeAnimation.Target = nameof(CompositionVisual.Size);
sizeAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
sizeAnimation.Duration = TimeSpan.FromSeconds(0.3);
var opacityAnimation = compositor.CreateScalarKeyFrameAnimation();
opacityAnimation.Target = nameof(CompositionVisual.Opacity);
opacityAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
opacityAnimation.Duration = TimeSpan.FromSeconds(0.3);
_implicitAnimations = compositor.CreateImplicitAnimationCollection();
_implicitAnimations[nameof(CompositionVisual.Offset)] = offsetAnimation;
_implicitAnimations[nameof(CompositionVisual.Size)] = sizeAnimation;
_implicitAnimations[nameof(CompositionVisual.Opacity)] = opacityAnimation;
}
protected override void OnKeyDown(KeyEventArgs e)
{
var hotkeys = Application.Current!.PlatformSettings?.HotkeyConfiguration;
if (e.Key.ToNavigationDirection() is { } direction && direction.IsDirectional())
{
e.Handled |= MoveSelection(direction, WrapSelection);
}
base.OnKeyDown(e);
}
}

View File

@@ -0,0 +1,38 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
namespace Ursa.Controls;
public class SelectionListItem: ContentControl, ISelectable
{
static SelectionListItem()
{
SelectableMixin.Attach<SelectionListItem>(IsSelectedProperty);
PressedMixin.Attach<SelectionListItem>();
FocusableProperty.OverrideDefaultValue<SelectionListItem>(true);
}
private static readonly Point s_invalidPoint = new Point(double.NaN, double.NaN);
private Point _pointerDownPoint = s_invalidPoint;
public static readonly StyledProperty<bool> IsSelectedProperty = SelectingItemsControl.IsSelectedProperty.AddOwner<ListBoxItem>();
public bool IsSelected
{
get => GetValue(IsSelectedProperty);
set => SetValue(IsSelectedProperty, value);
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (ItemsControl.ItemsControlFromItemContaner(this) is SelectionList list)
{
int index = list.IndexFromContainer(this);
list.SelectByIndex(index);
}
}
}