@@ -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";
|
||||
|
||||
55
demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml
Normal file
55
demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml
Normal 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>
|
||||
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.MenuKeyPagination => new PaginationDemoViewModel(),
|
||||
MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(),
|
||||
MenuKeys.MenuKeySelectionList => new SelectionListDemoViewModel(),
|
||||
MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(),
|
||||
MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(),
|
||||
MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(),
|
||||
|
||||
@@ -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" },
|
||||
|
||||
23
demo/Ursa.Demo/ViewModels/SelectionListDemoViewModel.cs
Normal file
23
demo/Ursa.Demo/ViewModels/SelectionListDemoViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
61
src/Ursa.Themes.Semi/Controls/SelectionList.axaml
Normal file
61
src/Ursa.Themes.Semi/Controls/SelectionList.axaml
Normal 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>
|
||||
@@ -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" />
|
||||
|
||||
153
src/Ursa/Controls/SelectionList/SelectionList.cs
Normal file
153
src/Ursa/Controls/SelectionList/SelectionList.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
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