13 changed files with 7 additions and 3217 deletions
@ -1,31 +0,0 @@ |
|||
using Avalonia; |
|||
using System; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public sealed class DatePickerPresenterItem : AvaloniaObject |
|||
{ |
|||
internal DatePickerPresenterItem(DateTimeOffset date) |
|||
{ |
|||
_date = date; |
|||
} |
|||
|
|||
public static readonly StyledProperty<string> DisplayTextProperty = |
|||
AvaloniaProperty.Register<DatePickerPresenterItem, string>("DisplayText"); |
|||
|
|||
public string DisplayText |
|||
{ |
|||
get => GetValue(DisplayTextProperty); |
|||
set => SetValue(DisplayTextProperty, value); |
|||
} |
|||
|
|||
internal DateTimeOffset GetStoredDate() => _date; |
|||
internal void UpdateStoredDate(DateTimeOffset newDate, string text) |
|||
{ |
|||
_date = newDate; |
|||
DisplayText = text; |
|||
} |
|||
|
|||
private DateTimeOffset _date; |
|||
} |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the argument passed when the Date changes on the <see cref="DatePickerPresenter"/>
|
|||
/// </summary>
|
|||
public class DatePickerValueChangedEventArgs |
|||
{ |
|||
public DateTimeOffset NewDate { get; } |
|||
public DateTimeOffset OldDate { get; } |
|||
|
|||
public DatePickerValueChangedEventArgs(DateTimeOffset oldDate, DateTimeOffset newDate) |
|||
{ |
|||
NewDate = newDate; |
|||
OldDate = OldDate; |
|||
} |
|||
} |
|||
} |
|||
@ -1,259 +0,0 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Input; |
|||
using System; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the Panel used by the <see cref="LoopingSelector"/>
|
|||
/// </summary>
|
|||
public sealed class LoopingPanel : Panel, ILogicalScrollable |
|||
{ |
|||
public LoopingPanel(LoopingSelector owner) |
|||
{ |
|||
_owner = owner; |
|||
} |
|||
|
|||
protected override Size MeasureOverride(Size availableSize) |
|||
{ |
|||
//It's assumed here that availableSize will have finite values
|
|||
//If used in the DatePickerPresenter or TimePickerPresenter, this
|
|||
//is met and works fine. If used elsewhere, ensure this is met
|
|||
//Width is required for the Items & height is required for the viewportsize
|
|||
if (double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height)) |
|||
throw new InvalidOperationException("LoopingPanel needs finite bounds"); |
|||
|
|||
//For the measure pass, remember we only have a subset of the total items
|
|||
//available. So we need to ask the LoopingSelector for it's Item count
|
|||
//return a size based on the extent of all items
|
|||
|
|||
var itmHgt = _owner.ItemHeight; |
|||
var itemCt = _owner.ItemCount; |
|||
var children = Children; |
|||
|
|||
var itemWid = availableSize.Width; |
|||
for (int i = 0; i < children.Count; i++) |
|||
{ |
|||
//Ensure we have a proper size when measuring
|
|||
(children[i] as LoopingSelectorItem).Width = itemWid; |
|||
(children[i] as LoopingSelectorItem).Height = itmHgt; |
|||
children[i].Measure(availableSize); |
|||
} |
|||
|
|||
var hei = itmHgt * itemCt; |
|||
|
|||
if (_owner.ShouldLoop) |
|||
{ |
|||
//WinUI preps for somewhere around 1000 items? in loop mode, then positions the offset in the middle
|
|||
//based on the SelectedItem index, that way scrolling is enabled in both directions
|
|||
//Here we prep for 10 * ItemCount & position in middle to start
|
|||
_extent = new Size(0, 10 * (itmHgt * itemCt) + (availableSize.Height - itmHgt)); |
|||
_viewport = new Size(0, availableSize.Height); |
|||
|
|||
if (!_hasInitLoop) |
|||
{ |
|||
var selIndex = _owner.SelectedIndex; |
|||
selIndex = selIndex == -1 ? 0 : selIndex; |
|||
|
|||
//We know we are measuring for 10x items,
|
|||
//so our init index is the selecteditems' index * 5
|
|||
if (double.IsNaN(initOffset)) |
|||
{ |
|||
_offset = new Vector(0, (selIndex * itmHgt) * 5); |
|||
} |
|||
else |
|||
{ |
|||
_offset = new Vector(0, initOffset); |
|||
initOffset = double.NaN; |
|||
} |
|||
_hasInitLoop = true; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
//SelectedItem is in the middle of the LoopingSelector, so we need to account for that
|
|||
//so all items can end up in this position
|
|||
_extent = new Size(0, hei + (availableSize.Height - itmHgt)); |
|||
_viewport = new Size(0, availableSize.Height); |
|||
|
|||
if (!double.IsNaN(initOffset)) |
|||
{ |
|||
_offset = new Vector(0, initOffset); |
|||
initOffset = double.NaN; |
|||
} |
|||
} |
|||
|
|||
//Total items visible, whether fully or partially visible
|
|||
_totalItemsInViewport = (int)Math.Ceiling(_viewport.Height / itmHgt); |
|||
|
|||
RaiseScrollInvalidated(null); |
|||
return _extent; |
|||
} |
|||
|
|||
protected override Size ArrangeOverride(Size finalSize) |
|||
{ |
|||
if (_owner == null || Children.Count == 0) |
|||
return base.ArrangeOverride(finalSize); |
|||
|
|||
var itemWid = finalSize.Width; |
|||
var itemHgt = _owner.ItemHeight; |
|||
var initY = (finalSize.Height / 2.0) - (itemHgt / 2.0); |
|||
var children = Children; |
|||
var offY = Offset.Y; |
|||
|
|||
//When not looping, currentSet will always be 0
|
|||
//When looping, we measure for 10x _owner.ItemCount, so we need to figure out
|
|||
//which "set" of items we're in based on the offset so we know where to properly
|
|||
//place items
|
|||
var singleExtent = _owner.ItemCount * itemHgt; |
|||
var currentSet = Math.Truncate(offY / singleExtent); |
|||
|
|||
int selIndex = _owner.SelectedIndex; |
|||
selIndex = selIndex == -1 ? 0 : selIndex; |
|||
|
|||
//When looping, the selected item should always be the middle item, equivalent in index
|
|||
//to _totalItemsInViewport
|
|||
//When not looping, if we're near the beginning of the list, our first item may be less than
|
|||
//_totalItemsInViewport, so we need to make sure in that case to make the selected item, the
|
|||
//actual selected index
|
|||
var childIndexOfSelected = _totalItemsInViewport; |
|||
if (!_owner.ShouldLoop && selIndex < _totalItemsInViewport) |
|||
childIndexOfSelected = selIndex; |
|||
|
|||
//Our selected container forms our "anchor" and all other containers are placed around this
|
|||
IControl containerOfSelected = children[childIndexOfSelected]; |
|||
|
|||
//Then we need to know how many containers are above and below the selected item
|
|||
//Should be _totalItemsInViewport for both, unless not looping & near items start
|
|||
//# containers above is just the index of the selected item container
|
|||
var numContainersAboveSelected = childIndexOfSelected; |
|||
|
|||
//Move initY to where we actually want to start placing the items
|
|||
initY += (singleExtent * currentSet) + (selIndex * itemHgt); |
|||
|
|||
//We first arrange the selected item
|
|||
Rect rc = new Rect(0, initY - offY, itemWid, itemHgt); |
|||
containerOfSelected.Arrange(rc); |
|||
|
|||
//Arrange all items above
|
|||
var prevY = initY - itemHgt; |
|||
for (int i = numContainersAboveSelected - 1; i >= 0; i--) |
|||
{ |
|||
rc = new Rect(0, prevY - offY, itemWid, itemHgt); |
|||
children[i].Arrange(rc); |
|||
prevY -= itemHgt; |
|||
} |
|||
|
|||
//Finally arrange all items below
|
|||
var nextY = initY + itemHgt; |
|||
for (int i = childIndexOfSelected + 1; i < children.Count; i++) |
|||
{ |
|||
rc = new Rect(0, nextY - offY, itemWid, itemHgt); |
|||
children[i].Arrange(rc); |
|||
nextY += itemHgt; |
|||
} |
|||
|
|||
return new Size(itemWid, _extent.Height); |
|||
} |
|||
|
|||
public bool CanHorizontallyScroll { get => false; set => _ = value; } |
|||
|
|||
public bool CanVerticallyScroll { get => true; set => _ = value; } |
|||
|
|||
public bool IsLogicalScrollEnabled => true; |
|||
|
|||
public Size ScrollSize => new Size(0, _owner?.ItemHeight ?? 32); |
|||
|
|||
//4 items
|
|||
public Size PageScrollSize => new Size(0, _owner?.ItemHeight * 4 ?? 128); |
|||
|
|||
public Size Extent => _extent; |
|||
|
|||
public Vector Offset |
|||
{ |
|||
get => _offset; |
|||
set |
|||
{ |
|||
if (Extent.Height == 0) |
|||
{ |
|||
initOffset = value.Y; |
|||
return; |
|||
} |
|||
|
|||
var old = _offset.Y; |
|||
_offset = value; |
|||
|
|||
if (Children.Count == 0) |
|||
return; |
|||
|
|||
var itemHgt = _owner.ItemHeight; |
|||
var totalItemCount = _owner.ItemCount; |
|||
|
|||
if (_owner.ShouldLoop) |
|||
{ |
|||
//If we're looping, we need to detect when we're approaching
|
|||
//the min/max bounds of the scrollviewer & reset to the otherside
|
|||
//to make sure we always have scrolling
|
|||
//To do this, since we plan for 10x total items, we move if we're
|
|||
//in the first or last "block" of items & return it to somewhere near the middle
|
|||
if (value.Y > old) //Scrolling Down
|
|||
{ |
|||
var extentOne = totalItemCount * itemHgt; |
|||
var scrollableHeight = (_extent.Height - _viewport.Height); |
|||
if (value.Y >= scrollableHeight - extentOne) |
|||
{ |
|||
_offset = new Vector(0, value.Y - (extentOne * 5)); |
|||
old = old - (extentOne * 5); |
|||
} |
|||
} |
|||
else if (value.Y < old) //Scrolling Up
|
|||
{ |
|||
var extentOne = totalItemCount * itemHgt; |
|||
if (value.Y < extentOne) |
|||
{ |
|||
_offset = new Vector(0, value.Y + (extentOne * 5)); |
|||
old = old + (extentOne * 5); |
|||
} |
|||
} |
|||
} |
|||
|
|||
_owner.SetSelectedIndexFromOffset(old, _offset.Y); |
|||
|
|||
RaiseScrollInvalidated(EventArgs.Empty); |
|||
InvalidateArrange(); |
|||
} |
|||
} |
|||
|
|||
public Size Viewport => _viewport; |
|||
|
|||
//Not used
|
|||
public bool BringIntoView(IControl target, Rect targetRect) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
//Not used
|
|||
public IControl GetControlInDirection(NavigationDirection direction, IControl from) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
public void RaiseScrollInvalidated(EventArgs e) |
|||
{ |
|||
ScrollInvalidated?.Invoke(this, e); |
|||
} |
|||
|
|||
|
|||
private double initOffset = double.NaN; |
|||
private LoopingSelector _owner; |
|||
private Size _extent; |
|||
private Size _viewport; |
|||
private Vector _offset; |
|||
private bool _hasInitLoop; |
|||
private int _totalItemsInViewport; |
|||
|
|||
public event EventHandler ScrollInvalidated; |
|||
} |
|||
} |
|||
@ -1,711 +0,0 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Controls.Utils; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
|
|||
public delegate void SelectionChangedEventHandler(object sender, SelectionChangedEventArgs e); |
|||
|
|||
/// <summary>
|
|||
/// An items control with the ability for infinite looping
|
|||
/// <para>
|
|||
/// Supports UI virtualization, though doesn't inherit from ItemsControl so we manage containers and
|
|||
/// realized items ourselves
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// In WinUI, this control isn't usable outside of the Date/Time Pickers, but here it technically can be
|
|||
/// Note, it's default behavior is for the pickers though.
|
|||
/// </para>
|
|||
/// </summary>
|
|||
public class LoopingSelector : TemplatedControl |
|||
{ |
|||
public LoopingSelector() |
|||
{ |
|||
_panel = new LoopingPanel(this); |
|||
LogicalChildren.Add(_panel); |
|||
AddHandler(LoopingSelectorItem.SelectedEvent, OnItemSelected); |
|||
this.GetObservable(BoundsProperty).Subscribe(x => OnBoundsChanged(x)); |
|||
} |
|||
|
|||
static LoopingSelector() |
|||
{ |
|||
FocusableProperty.OverrideDefaultValue<LoopingSelector>(true); |
|||
ItemsProperty.Changed.AddClassHandler<LoopingSelector>((x, v) => x.OnItemsChanged(v)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Items"/> Property
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<LoopingSelector, IEnumerable> ItemsProperty = |
|||
AvaloniaProperty.RegisterDirect<LoopingSelector, IEnumerable>("Items", |
|||
x => x.Items, (x, v) => x.Items = v); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="ItemCount"/> Property
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<LoopingSelector, int> ItemCountProperty = |
|||
AvaloniaProperty.RegisterDirect<LoopingSelector, int>("ItemCount", |
|||
x => x.ItemCount); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="SelectedItem"/> Property
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<LoopingSelector, object> SelectedItemProperty = |
|||
AvaloniaProperty.RegisterDirect<LoopingSelector, object>("SelectedItem", |
|||
x => x.SelectedItem, (x, v) => x.SelectedItem = v); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="SelectedIndex"/> Property
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<LoopingSelector, int> SelectedIndexProperty = |
|||
AvaloniaProperty.RegisterDirect<LoopingSelector, int>("SelectedIndex", |
|||
x => x.SelectedIndex, (x, v) => x.SelectedIndex = v); |
|||
|
|||
//UWP/WinUI has ItemWidth, ignoring, will have items just fill the width of the container
|
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="ItemHeight"/> Property
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<LoopingSelector, double> ItemHeightProperty = |
|||
AvaloniaProperty.RegisterDirect<LoopingSelector, double>("ItemHeight", |
|||
x => x.ItemHeight, (x, v) => x.ItemHeight = v); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="ItemTemplate"/> Property
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty = |
|||
AvaloniaProperty.Register<LoopingSelector, IDataTemplate>("ItemTemplate"); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="ShouldLoop"/> Property
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<LoopingSelector, bool> ShouldLoopProperty = |
|||
AvaloniaProperty.RegisterDirect<LoopingSelector, bool>("ShouldLoop", |
|||
x => x.ShouldLoop, (x, v) => x.ShouldLoop = v); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the Items
|
|||
/// </summary>
|
|||
public IEnumerable Items |
|||
{ |
|||
get => _items; |
|||
set => SetAndRaise(ItemsProperty, ref _items, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of items
|
|||
/// </summary>
|
|||
public int ItemCount |
|||
{ |
|||
get => _itemCount; |
|||
private set => SetAndRaise(ItemCountProperty, ref _itemCount, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the SelectedIndex
|
|||
/// </summary>
|
|||
public int SelectedIndex |
|||
{ |
|||
get => _selectedIndex; |
|||
set |
|||
{ |
|||
if (Items == null || ItemCount == 0) |
|||
return; |
|||
var old = _selectedIndex; |
|||
SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value); |
|||
|
|||
var oldItem = _selectedItem; |
|||
if (value == -1) |
|||
{ |
|||
_selectedItem = null; |
|||
} |
|||
else |
|||
{ |
|||
if (Items is IList l) |
|||
_selectedItem = l[value]; |
|||
else |
|||
_selectedItem = Items.ElementAt(value); |
|||
} |
|||
RaisePropertyChanged(SelectedItemProperty, oldItem, _selectedItem); |
|||
if (!_preventMovingScrollWhenSelecting) |
|||
UpdateOffset(); |
|||
|
|||
SelectionChangedEventArgs args = new SelectionChangedEventArgs(null, new object[] { oldItem }, new object[] { _selectedItem }); |
|||
OnSelectionChanged(this, args); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the SelectedItem
|
|||
/// </summary>
|
|||
public object SelectedItem |
|||
{ |
|||
get |
|||
{ |
|||
if (SelectedIndex == -1) |
|||
return null; |
|||
if (Items is IList l) |
|||
return l[SelectedIndex]; |
|||
else |
|||
return Items.ElementAt(SelectedIndex); |
|||
} |
|||
set |
|||
{ |
|||
if (value == null) |
|||
{ |
|||
SelectedIndex = -1; |
|||
} |
|||
else |
|||
{ |
|||
if (Items is IList l) |
|||
SelectedIndex = l.IndexOf(value); |
|||
else |
|||
SelectedIndex = Items.IndexOf(value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the height of the items
|
|||
/// </summary>
|
|||
public double ItemHeight |
|||
{ |
|||
get => _itemHeight; |
|||
set |
|||
{ |
|||
SetAndRaise(ItemHeightProperty, ref _itemHeight, value); |
|||
|
|||
_totalItemsInViewport = (int)Math.Ceiling(Bounds.Height / (value == 0 ? 1 : value)); |
|||
if (_totalItemsInViewport % 2 == 0) |
|||
_totalItemsInViewport += 1; |
|||
|
|||
UpdateOffset(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the item template
|
|||
/// </summary>
|
|||
public IDataTemplate ItemTemplate |
|||
{ |
|||
get => GetValue(ItemTemplateProperty); |
|||
set => SetValue(ItemTemplateProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets whether the items should loop
|
|||
/// </summary>
|
|||
public bool ShouldLoop |
|||
{ |
|||
get => _shouldLoop; |
|||
set |
|||
{ |
|||
SetAndRaise(ShouldLoopProperty, ref _shouldLoop, value); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Raised when the SelectedItem/Index changes
|
|||
/// </summary>
|
|||
public event SelectionChangedEventHandler SelectionChanged; |
|||
|
|||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) |
|||
{ |
|||
if (_scroller != null) |
|||
{ |
|||
_scroller.Content = null; |
|||
} |
|||
base.OnApplyTemplate(e); |
|||
_scroller = e.NameScope.Find<ScrollViewer>("Scroller"); |
|||
|
|||
_scroller.Content = _panel; |
|||
|
|||
_upButton = e.NameScope.Find<RepeatButton>("UpButton"); |
|||
if (_upButton != null) |
|||
{ |
|||
_upButton.Click += OnUpButtonClick; |
|||
} |
|||
|
|||
_downButton = e.NameScope.Find<RepeatButton>("DownButton"); |
|||
if (_downButton != null) |
|||
{ |
|||
_downButton.Click += OnDownButtonClick; |
|||
} |
|||
} |
|||
|
|||
protected override void OnKeyDown(KeyEventArgs e) |
|||
{ |
|||
switch (e.Key) |
|||
{ |
|||
case Key.Up: |
|||
if (ShouldLoop) |
|||
{ |
|||
var selIndex = SelectedIndex; |
|||
selIndex--; |
|||
if (selIndex < 0) |
|||
selIndex += ItemCount; |
|||
SelectedIndex = selIndex; |
|||
} |
|||
else |
|||
{ |
|||
SelectedIndex = Math.Max(0, SelectedIndex - 1); |
|||
} |
|||
e.Handled = true; |
|||
break; |
|||
case Key.Down: |
|||
if (ShouldLoop) |
|||
{ |
|||
var selIndex = SelectedIndex; |
|||
selIndex++; |
|||
if (selIndex >= ItemCount) |
|||
selIndex -= ItemCount; |
|||
SelectedIndex = selIndex; |
|||
} |
|||
else |
|||
{ |
|||
SelectedIndex = Math.Min(ItemCount, SelectedIndex + 1); |
|||
} |
|||
e.Handled = true; |
|||
break; |
|||
case Key.PageUp: |
|||
if (ShouldLoop) |
|||
{ |
|||
var selIndex = SelectedIndex; |
|||
selIndex -= 4; |
|||
if (selIndex < 0) |
|||
selIndex += ItemCount; |
|||
SelectedIndex = selIndex; |
|||
} |
|||
else |
|||
{ |
|||
SelectedIndex = Math.Max(0, SelectedIndex - 4); |
|||
} |
|||
e.Handled = true; |
|||
break; |
|||
} |
|||
base.OnKeyDown(e); |
|||
} |
|||
|
|||
protected override void OnPointerPressed(PointerPressedEventArgs e) |
|||
{ |
|||
base.OnPointerPressed(e); |
|||
KeyboardDevice.Instance.SetFocusedElement(this, NavigationMethod.Pointer, KeyModifiers.None); |
|||
FocusManager.Instance.Focus(this, NavigationMethod.Pointer, KeyModifiers.None); |
|||
} |
|||
|
|||
private void OnDownButtonClick(object sender, RoutedEventArgs e) |
|||
{ |
|||
var selIndex = SelectedIndex; |
|||
if (selIndex == ItemCount - 1) |
|||
{ |
|||
if (ShouldLoop) |
|||
SelectedIndex = 0; |
|||
} |
|||
else |
|||
{ |
|||
SelectedIndex++; |
|||
} |
|||
|
|||
e.Handled = true; |
|||
} |
|||
|
|||
private void OnUpButtonClick(object sender, RoutedEventArgs e) |
|||
{ |
|||
var selIndex = SelectedIndex; |
|||
if (selIndex == 0) |
|||
{ |
|||
if (ShouldLoop) |
|||
SelectedIndex = ItemCount - 1; |
|||
} |
|||
else |
|||
{ |
|||
SelectedIndex--; |
|||
} |
|||
|
|||
e.Handled = true; |
|||
} |
|||
|
|||
private void OnItemsChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (e.OldValue is INotifyCollectionChanged oldC) |
|||
{ |
|||
oldC.CollectionChanged -= OnItemsCollectionChanged; |
|||
} |
|||
if (e.NewValue is INotifyCollectionChanged newC) |
|||
{ |
|||
newC.CollectionChanged += OnItemsCollectionChanged; |
|||
} |
|||
|
|||
//When the entire items list changes, reset selection quietly
|
|||
_selectedIndex = -1; |
|||
_selectedItem = null; |
|||
|
|||
if (Items is IList l) |
|||
{ |
|||
ItemCount = l.Count; |
|||
} |
|||
else |
|||
{ |
|||
ItemCount = Items.Count(); |
|||
} |
|||
|
|||
EnsureContainers(); |
|||
UpdateOffset(); |
|||
} |
|||
|
|||
private void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
switch (e.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
ItemCount += e.NewItems.Count; |
|||
var index = e.NewStartingIndex; |
|||
|
|||
EnsureContainers(); |
|||
UpdateOffset(); |
|||
break; |
|||
case NotifyCollectionChangedAction.Remove: |
|||
ItemCount -= e.OldItems.Count; |
|||
EnsureContainers(); |
|||
UpdateOffset(); |
|||
break; |
|||
case NotifyCollectionChangedAction.Reset: |
|||
_selectedIndex = 0; |
|||
_selectedItem = 0; |
|||
ItemCount = 0; |
|||
_panel.Children.Clear(); |
|||
UpdateOffset(); |
|||
break; |
|||
|
|||
//TODO - items source should be static anyway
|
|||
case NotifyCollectionChangedAction.Replace: |
|||
case NotifyCollectionChangedAction.Move: |
|||
throw new NotSupportedException("Can't move or replace items in ItemsCollection"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Ensures we have the correct number of containers in the LoopingSelectorPanel
|
|||
/// This will add, remove, or clear the panel as necessary
|
|||
/// </summary>
|
|||
private void EnsureContainers(bool setContent = true) |
|||
{ |
|||
if (Bounds.Height == 0) |
|||
return; |
|||
|
|||
int itemCount = ItemCount; |
|||
//How many containers we ideally want
|
|||
int desiredItemsLoaded = (_totalItemsInViewport * 2) + 1; |
|||
|
|||
var realizedContainerCount = _panel.Children.Count; |
|||
|
|||
if (ShouldLoop) |
|||
{ |
|||
//When looping we should ALWAYS have desiredItemsLoaded number of containers
|
|||
//available
|
|||
var delta = Math.Abs(realizedContainerCount - desiredItemsLoaded); |
|||
if (realizedContainerCount < desiredItemsLoaded) //Add more containers
|
|||
{ |
|||
List<LoopingSelectorItem> panelItems = new List<LoopingSelectorItem>(); |
|||
for (int i = 0; i < delta; i++) |
|||
{ |
|||
LoopingSelectorItem lsi = new LoopingSelectorItem(); |
|||
lsi.Height = ItemHeight; |
|||
lsi.ContentTemplate = ItemTemplate; |
|||
panelItems.Add(lsi); |
|||
} |
|||
_panel.Children.AddRange(panelItems); |
|||
} |
|||
else if (realizedContainerCount > desiredItemsLoaded) //Remove extra containers
|
|||
{ |
|||
//Technically not needed now as we don't move the containers when looping, just
|
|||
//swap content, but may be called if resized
|
|||
_panel.Children.RemoveRange(realizedContainerCount - delta, delta); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
//When we're not looping, things are a little trickier, if we're near the bounds of scrolling,
|
|||
//We may not have desiredItemsLoaded containers, so we need to account for that
|
|||
//NumContainers here should be in range [_totalItemsInViewport, desiredItemsLoaded]
|
|||
|
|||
//First index of realized items
|
|||
int selIndex = SelectedIndex; |
|||
selIndex = selIndex == -1 ? 0 : selIndex; |
|||
|
|||
int numItemsAboveSelected = _totalItemsInViewport; |
|||
if (selIndex - numItemsAboveSelected < 0) |
|||
numItemsAboveSelected = selIndex; |
|||
|
|||
int numItemsBelowSelected = _totalItemsInViewport; |
|||
if (selIndex + _totalItemsInViewport >= ItemCount) |
|||
numItemsBelowSelected = ItemCount - selIndex - 1; |
|||
|
|||
int neededContainers = numItemsBelowSelected + numItemsAboveSelected + 1; |
|||
int currentCount = _panel.Children.Count; |
|||
|
|||
//Do we need containers?
|
|||
var numContsToAddRemove = neededContainers - currentCount; |
|||
|
|||
if (numContsToAddRemove > 0) //Add Containers
|
|||
{ |
|||
List<LoopingSelectorItem> panelItems = new List<LoopingSelectorItem>(); |
|||
|
|||
for (int i = 0; i < numContsToAddRemove; i++) |
|||
{ |
|||
LoopingSelectorItem lsi = new LoopingSelectorItem(); |
|||
lsi.Height = ItemHeight; |
|||
lsi.ContentTemplate = ItemTemplate; |
|||
panelItems.Add(lsi); |
|||
} |
|||
_panel.Children.AddRange(panelItems); |
|||
} |
|||
else if (numContsToAddRemove < 0) //Remove containers
|
|||
{ |
|||
numContsToAddRemove = Math.Abs(numContsToAddRemove); |
|||
_panel.Children.RemoveRange(currentCount - numContsToAddRemove, numContsToAddRemove); |
|||
} |
|||
} |
|||
|
|||
if (setContent && ItemCount > 0) |
|||
SetItemContent(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the content of the loaded containers
|
|||
/// </summary>
|
|||
private void SetItemContent() |
|||
{ |
|||
int itemCount = ItemCount; |
|||
|
|||
if (ShouldLoop) |
|||
{ |
|||
var selIndex = SelectedIndex; |
|||
var panelItems = _panel.Children; |
|||
|
|||
int c = 0; |
|||
int index = selIndex == -1 ? -_totalItemsInViewport : selIndex - _totalItemsInViewport; |
|||
|
|||
while (c < panelItems.Count) |
|||
{ |
|||
if (index >= ItemCount) |
|||
index -= ItemCount; |
|||
if (index < 0) |
|||
index += ItemCount; |
|||
|
|||
if (index == selIndex) |
|||
(panelItems[c] as LoopingSelectorItem).IsSelected = true; |
|||
else |
|||
(panelItems[c] as LoopingSelectorItem).IsSelected = false; |
|||
|
|||
(panelItems[c] as LoopingSelectorItem).Content = GetElementAt(index); |
|||
c++; |
|||
index++; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
//We first need the first item in the realized items...
|
|||
var selIndex = SelectedIndex; |
|||
var firstIndex = Math.Max(0, selIndex - _totalItemsInViewport); |
|||
var panelItems = _panel.Children; |
|||
|
|||
for (int i = 0; i < panelItems.Count; i++) |
|||
{ |
|||
if (firstIndex == selIndex) |
|||
(panelItems[i] as LoopingSelectorItem).IsSelected = true; |
|||
else |
|||
(panelItems[i] as LoopingSelectorItem).IsSelected = false; |
|||
|
|||
(panelItems[i] as LoopingSelectorItem).Content = GetElementAt(firstIndex); |
|||
firstIndex++; |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Handles recycling of containers
|
|||
/// </summary>
|
|||
private void RecycleContainersIfNecessaryOnScroll(double newOffset, double oldOffset) |
|||
{ |
|||
var children = _panel.Children; |
|||
var initY = (Bounds.Height / 2.0) - (ItemHeight / 2.0); |
|||
var recThresTop = initY - (_totalItemsInViewport * ItemHeight); |
|||
var recThresBot = initY + (_totalItemsInViewport * ItemHeight); |
|||
var scrollChange = newOffset - oldOffset; |
|||
|
|||
int numContsAbove = 0; |
|||
for(int i = 0; i < children.Count; i++) |
|||
{ |
|||
if (children[i].Bounds.Bottom - scrollChange <= recThresTop) |
|||
numContsAbove++; |
|||
} |
|||
|
|||
int numContsBelow = 0; |
|||
for (int i = 0; i < children.Count; i++) |
|||
{ |
|||
if (children[i].Bounds.Bottom - scrollChange >= recThresBot) |
|||
numContsBelow++; |
|||
} |
|||
|
|||
//var numContsAbove = children.Where(x => (x.Bounds.Bottom - scrollChange) <= recThresTop).Count();
|
|||
//var numContsBelow = children.Where(x => (x.Bounds.Top - scrollChange) >= recThresBot).Count();
|
|||
|
|||
if (numContsAbove > 0) |
|||
{ |
|||
var recycleCount = numContsAbove; |
|||
var lastItemContent = (children[children.Count - 1] as LoopingSelectorItem).Content; |
|||
var index = Items.IndexOf(lastItemContent); |
|||
_panel.Children.MoveRange(0, recycleCount, children.Count); |
|||
} |
|||
else if (numContsBelow > 0) |
|||
{ |
|||
var recycleCount = numContsBelow; |
|||
var firstItemContent = (children[0] as LoopingSelectorItem).Content; |
|||
var index = Items.IndexOf(firstItemContent); |
|||
var paneItemCount = _panel.Children.Count; |
|||
|
|||
_panel.Children.MoveRange(paneItemCount - recycleCount, recycleCount, 0); |
|||
} |
|||
|
|||
//Probably not ideal to re-set every item's content, but trying to set
|
|||
//the content of just the recycled items was doing weird things
|
|||
//We have a small number of containers loaded, so this shouldn't have
|
|||
//too big of impact
|
|||
SetItemContent(); |
|||
} |
|||
|
|||
private object GetElementAt(int index) |
|||
{ |
|||
if (index < 0 || index >= ItemCount) |
|||
return null; |
|||
|
|||
if (Items is IList l) |
|||
return l[index]; |
|||
else |
|||
return Items.Cast<object>().ToList()[index]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Updates the scrollviewer offset when the selectedindex changed
|
|||
/// </summary>
|
|||
private void UpdateOffset() |
|||
{ |
|||
if (_panel == null || ItemCount == 0) |
|||
return; |
|||
|
|||
_preventUpdateSelection = true; |
|||
|
|||
var oldOffY = _panel.Offset.Y; |
|||
if (ShouldLoop) |
|||
{ |
|||
//We measure for 10x as many items, so when we set the SelectedIndex
|
|||
//and need to change the offset, should set it towards the middle
|
|||
//so we preserve scrolling in both directions
|
|||
int selIndex = SelectedIndex; |
|||
selIndex = selIndex == -1 ? 0 : selIndex; |
|||
var extent = ItemCount * ItemHeight; |
|||
_panel.Offset = new Vector(0, (selIndex * ItemHeight) + (extent * 5)); |
|||
} |
|||
else |
|||
{ |
|||
//Not looping, just convert the SelectedIndex to an offset
|
|||
//if -1, set to 0;
|
|||
int selIndex = SelectedIndex; |
|||
if (ItemCount == 0 || SelectedIndex == -1) |
|||
_panel.Offset = new Vector(0, 0); |
|||
else |
|||
_panel.Offset = new Vector(0, selIndex * ItemHeight); |
|||
|
|||
EnsureContainers(false); |
|||
} |
|||
|
|||
RecycleContainersIfNecessaryOnScroll(_panel.Offset.Y, oldOffY); |
|||
|
|||
_preventUpdateSelection = false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Updates the SelectedIndex when scrolling occurs
|
|||
/// </summary>
|
|||
/// <param name="offsetY"></param>
|
|||
internal void SetSelectedIndexFromOffset(double oldOffsetY, double offsetY) |
|||
{ |
|||
if (_preventUpdateSelection) |
|||
return; |
|||
|
|||
_preventMovingScrollWhenSelecting = true; |
|||
|
|||
if (ShouldLoop) |
|||
{ |
|||
var extent = ItemCount * ItemHeight; |
|||
var numExtents = offsetY / extent; |
|||
numExtents = numExtents < 0 ? 0 : Math.Truncate(numExtents); |
|||
var pixelOffset = offsetY - extent * numExtents; |
|||
|
|||
SelectedIndex = (int)(pixelOffset / ItemHeight); |
|||
RecycleContainersIfNecessaryOnScroll(offsetY, oldOffsetY); |
|||
} |
|||
else |
|||
{ |
|||
SelectedIndex = (int)(offsetY / ItemHeight); |
|||
EnsureContainers(false); |
|||
RecycleContainersIfNecessaryOnScroll(offsetY, oldOffsetY); |
|||
} |
|||
|
|||
_preventMovingScrollWhenSelecting = false; |
|||
} |
|||
|
|||
private void OnItemSelected(object sender, RoutedEventArgs e) |
|||
{ |
|||
var item = (e.Source as LoopingSelectorItem).Content; |
|||
SelectedItem = item; |
|||
} |
|||
|
|||
protected virtual void OnSelectionChanged(object sender, SelectionChangedEventArgs e) |
|||
{ |
|||
SelectionChanged?.Invoke(sender, e); |
|||
} |
|||
|
|||
private void OnBoundsChanged(Rect x) |
|||
{ |
|||
//Ideally we always want this to be odd, since the selected item is placed in the middle,
|
|||
//so we have the same number of items above and below at all times
|
|||
var itmHgt = ItemHeight; |
|||
_totalItemsInViewport = (int)Math.Ceiling(x.Height / (itmHgt == 0 ? 1 : itmHgt)); |
|||
if (_totalItemsInViewport % 2 == 0) |
|||
_totalItemsInViewport += 1; |
|||
|
|||
EnsureContainers(); |
|||
} |
|||
|
|||
|
|||
//TemplateItems
|
|||
private RepeatButton _downButton; |
|||
private RepeatButton _upButton; |
|||
private ScrollViewer _scroller; |
|||
|
|||
private LoopingPanel _panel; |
|||
|
|||
private int _totalItemsInViewport; |
|||
private IEnumerable _items; |
|||
private int _itemCount; |
|||
private int _selectedIndex = -1; |
|||
private object _selectedItem; |
|||
private double _itemHeight = 32; |
|||
private bool _shouldLoop = true; |
|||
private bool _preventUpdateSelection; |
|||
private bool _preventMovingScrollWhenSelecting; |
|||
} |
|||
} |
|||
@ -1,58 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Controls.Mixins; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the containers used by the <see cref="LoopingSelector"/>
|
|||
/// </summary>
|
|||
public sealed class LoopingSelectorItem : ContentControl |
|||
{ |
|||
static LoopingSelectorItem() |
|||
{ |
|||
PressedMixin.Attach<LoopingSelectorItem>(); |
|||
IsSelectedProperty.Changed.AddClassHandler<LoopingSelectorItem>((x, e) => x.OnIsSelectedChanged(e)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="IsSelected"/> Property
|
|||
/// </summary>
|
|||
internal static readonly StyledProperty<bool> IsSelectedProperty = |
|||
AvaloniaProperty.Register<LoopingSelectorItem, bool>(nameof(IsSelected)); |
|||
|
|||
|
|||
public static readonly RoutedEvent<RoutedEventArgs> SelectedEvent = |
|||
RoutedEvent.Register<LoopingSelectorItem, RoutedEventArgs>(nameof(Selected), RoutingStrategies.Bubble); |
|||
|
|||
internal bool IsSelected |
|||
{ |
|||
get => GetValue(IsSelectedProperty); |
|||
set => SetValue(IsSelectedProperty, value); |
|||
} |
|||
|
|||
protected override void OnPointerReleased(PointerReleasedEventArgs e) |
|||
{ |
|||
base.OnPointerReleased(e); |
|||
|
|||
if (e.GetCurrentPoint(this).Properties.PointerUpdateKind == PointerUpdateKind.LeftButtonReleased) |
|||
{ |
|||
//The selection event only raises when invoked by pointer events
|
|||
RaiseEvent(new RoutedEventArgs(SelectedEvent, this)); |
|||
} |
|||
} |
|||
|
|||
private void OnIsSelectedChanged(AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
var newValue = (bool)e.NewValue; |
|||
PseudoClasses.Set(":selected", newValue); |
|||
} |
|||
|
|||
public event EventHandler<RoutedEventArgs> Selected |
|||
{ |
|||
add => AddHandler(SelectedEvent, value); |
|||
remove => RemoveHandler(SelectedEvent, value); |
|||
} |
|||
} |
|||
} |
|||
@ -1,27 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public sealed class TimePickerPresenterItem : AvaloniaObject |
|||
{ |
|||
internal TimePickerPresenterItem(TimeSpan date) |
|||
{ |
|||
_date = date; |
|||
} |
|||
|
|||
public static readonly StyledProperty<string> DisplayTextProperty = |
|||
AvaloniaProperty.Register<TimePickerPresenterItem, string>("DisplayText"); |
|||
|
|||
public string DisplayText |
|||
{ |
|||
get => GetValue(DisplayTextProperty); |
|||
set => SetValue(DisplayTextProperty, value); |
|||
} |
|||
|
|||
internal TimeSpan GetStoredTime() => _date; |
|||
|
|||
private TimeSpan _date; |
|||
} |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class TimePickerValueChangedEventArgs |
|||
{ |
|||
public TimeSpan OldTime { get; } |
|||
public TimeSpan NewTime { get; } |
|||
public TimePickerValueChangedEventArgs(TimeSpan old, TimeSpan newT) |
|||
{ |
|||
OldTime = old; |
|||
NewTime = newT; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue