feat: add selection list.
This commit is contained in:
@@ -22,6 +22,7 @@ public static class MenuKeys
|
|||||||
public const string MenuKeyNumericUpDown = "NumericUpDown";
|
public const string MenuKeyNumericUpDown = "NumericUpDown";
|
||||||
public const string MenuKeyPagination = "Pagination";
|
public const string MenuKeyPagination = "Pagination";
|
||||||
public const string MenuKeyRangeSlider = "RangeSlider";
|
public const string MenuKeyRangeSlider = "RangeSlider";
|
||||||
|
public const string MenuKeySelectionList = "SelectionList";
|
||||||
public const string MenuKeyTagInput = "TagInput";
|
public const string MenuKeyTagInput = "TagInput";
|
||||||
public const string MenuKeyTimeline = "Timeline";
|
public const string MenuKeyTimeline = "Timeline";
|
||||||
public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon";
|
public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon";
|
||||||
|
|||||||
15
demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml
Normal file
15
demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<UserControl 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"
|
||||||
|
x:DataType="vm:SelectionListDemoViewModel"
|
||||||
|
x:CompileBindings="True"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800"
|
||||||
|
d:DesignHeight="450"
|
||||||
|
x:Class="Ursa.Demo.Pages.SelectionListDemo">
|
||||||
|
<StackPanel HorizontalAlignment="Left">
|
||||||
|
<u:SelectionList ItemsSource="{Binding Items}"></u:SelectionList>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
13
demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml.cs
Normal file
13
demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ public class MainViewViewModel : ViewModelBase
|
|||||||
MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(),
|
MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(),
|
||||||
MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(),
|
MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(),
|
||||||
MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(),
|
MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(),
|
||||||
|
MenuKeys.MenuKeySelectionList => new SelectionListDemoViewModel(),
|
||||||
MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(),
|
MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(),
|
||||||
MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(),
|
MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(),
|
||||||
MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(),
|
MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(),
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public class MenuViewModel: ViewModelBase
|
|||||||
new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown, Status = "New" },
|
new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown, Status = "New" },
|
||||||
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
|
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
|
||||||
new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider, Status = "New"},
|
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 = "TagInput", Key = MenuKeys.MenuKeyTagInput },
|
||||||
new() { MenuHeader = "Theme Toggler", Key = MenuKeys.MenuKeyThemeToggler },
|
new() { MenuHeader = "Theme Toggler", Key = MenuKeys.MenuKeyThemeToggler },
|
||||||
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline, Status = "Updated" },
|
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline, Status = "Updated" },
|
||||||
|
|||||||
17
demo/Ursa.Demo/ViewModels/SelectionListDemoViewModel.cs
Normal file
17
demo/Ursa.Demo/ViewModels/SelectionListDemoViewModel.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace Ursa.Demo.ViewModels;
|
||||||
|
|
||||||
|
public class SelectionListDemoViewModel: ObservableObject
|
||||||
|
{
|
||||||
|
public ObservableCollection<string> Items { get; set; }
|
||||||
|
|
||||||
|
public SelectionListDemoViewModel()
|
||||||
|
{
|
||||||
|
Items = new ObservableCollection<string>()
|
||||||
|
{
|
||||||
|
"Ding", "Otter", "Husky", "Mr. 17", "Cass"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/Ursa.Themes.Semi/Controls/SelectionList.axaml
Normal file
52
src/Ursa.Themes.Semi/Controls/SelectionList.axaml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<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="LightBlue"></Setter>
|
||||||
|
<Setter Property="ListBox.Template">
|
||||||
|
<ControlTemplate TargetType="ListBox">
|
||||||
|
<Border
|
||||||
|
Name="border"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
ClipToBounds="{TemplateBinding ClipToBounds}"
|
||||||
|
CornerRadius="{TemplateBinding CornerRadius}">
|
||||||
|
<Panel>
|
||||||
|
<Border
|
||||||
|
Classes="Shadow"
|
||||||
|
Margin="2"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Name="{x:Static u:SelectionList.PART_Indicator}"
|
||||||
|
Theme="{DynamicResource CardBorder}" />
|
||||||
|
<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="Padding" Value="8" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate TargetType="u:SelectionListItem">
|
||||||
|
<ContentPresenter
|
||||||
|
Name="PART_ContentPresenter"
|
||||||
|
Padding="{TemplateBinding Padding}"
|
||||||
|
Foreground="{TemplateBinding Foreground}"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
Content="{TemplateBinding Content}"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
<Style Selector="^:selected">
|
||||||
|
<Setter Property="Foreground" Value="Blue"></Setter>
|
||||||
|
</Style>
|
||||||
|
</ControlTheme>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
<ResourceInclude Source="NumericUpDown.axaml" />
|
<ResourceInclude Source="NumericUpDown.axaml" />
|
||||||
<ResourceInclude Source="Pagination.axaml" />
|
<ResourceInclude Source="Pagination.axaml" />
|
||||||
<ResourceInclude Source="RangeSlider.axaml" />
|
<ResourceInclude Source="RangeSlider.axaml" />
|
||||||
|
<ResourceInclude Source="SelectionList.axaml" />
|
||||||
<ResourceInclude Source="TagInput.axaml" />
|
<ResourceInclude Source="TagInput.axaml" />
|
||||||
<ResourceInclude Source="ThemeSelector.axaml" />
|
<ResourceInclude Source="ThemeSelector.axaml" />
|
||||||
<ResourceInclude Source="Timeline.axaml" />
|
<ResourceInclude Source="Timeline.axaml" />
|
||||||
|
|||||||
126
src/Ursa/Controls/SelectionList/SelectionList.cs
Normal file
126
src/Ursa/Controls/SelectionList/SelectionList.cs
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Metadata;
|
||||||
|
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(Control))]
|
||||||
|
public class SelectionList: SelectingItemsControl
|
||||||
|
{
|
||||||
|
public const string PART_Indicator = "PART_Indicator";
|
||||||
|
private static readonly FuncTemplate<Panel?> DefaultPanel = new(() => new StackPanel());
|
||||||
|
|
||||||
|
private Control? _indicator;
|
||||||
|
private ImplicitAnimationCollection? _implicitAnimations;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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<Control>(PART_Indicator);
|
||||||
|
_indicator?.Arrange(new Rect());
|
||||||
|
if (_indicator is not null)
|
||||||
|
{
|
||||||
|
_indicator.Opacity = 0;
|
||||||
|
SetUpAnimation();
|
||||||
|
if (ElementComposition.GetElementVisual(_indicator) is { } v)
|
||||||
|
{
|
||||||
|
v.ImplicitAnimations = _implicitAnimations;
|
||||||
|
}
|
||||||
|
_indicator.SizeChanged += OnIndicatorSizeChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnIndicatorSizeChanged(object sender, SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SelectByIndex(int index)
|
||||||
|
{
|
||||||
|
using var operation = Selection.BatchUpdate();
|
||||||
|
Selection.Clear();
|
||||||
|
Selection.Select(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetUpAnimation()
|
||||||
|
{
|
||||||
|
var compositor = ElementComposition.GetElementVisual(this)!.Compositor;
|
||||||
|
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
|
||||||
|
offsetAnimation.Target = "Offset";
|
||||||
|
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
|
||||||
|
offsetAnimation.Duration = TimeSpan.FromSeconds(0.3);
|
||||||
|
var sizeAnimation = compositor.CreateVector2KeyFrameAnimation();
|
||||||
|
sizeAnimation.Target = "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["Offset"] = offsetAnimation;
|
||||||
|
_implicitAnimations["Size"] = sizeAnimation;
|
||||||
|
_implicitAnimations["Opacity"] = opacityAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
38
src/Ursa/Controls/SelectionList/SelectionListItem.cs
Normal file
38
src/Ursa/Controls/SelectionList/SelectionListItem.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user