csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
729 lines
29 KiB
729 lines
29 KiB
// This source file is adapted from the WinUI project.
|
|
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|
//
|
|
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Linq;
|
|
using Avalonia.Controls.Templates;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Layout;
|
|
using Avalonia.VisualTree;
|
|
|
|
namespace Avalonia.Controls
|
|
{
|
|
internal sealed class ViewManager
|
|
{
|
|
private const int FirstRealizedElementIndexDefault = int.MaxValue;
|
|
private const int LastRealizedElementIndexDefault = int.MinValue;
|
|
|
|
private readonly ItemsRepeater _owner;
|
|
private readonly List<PinnedElementInfo> _pinnedPool = new List<PinnedElementInfo>();
|
|
private readonly UniqueIdElementPool _resetPool;
|
|
private IControl _lastFocusedElement;
|
|
private bool _isDataSourceStableResetPending;
|
|
private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
|
|
private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
|
|
private bool _eventsSubscribed;
|
|
|
|
public ViewManager(ItemsRepeater owner)
|
|
{
|
|
_owner = owner;
|
|
_resetPool = new UniqueIdElementPool(owner);
|
|
}
|
|
|
|
public IControl GetElement(int index, bool forceCreate, bool suppressAutoRecycle)
|
|
{
|
|
var element = forceCreate ? null : GetElementIfAlreadyHeldByLayout(index);
|
|
if (element == null)
|
|
{
|
|
// check if this is the anchor made through repeater in preparation
|
|
// for a bring into view.
|
|
var madeAnchor = _owner.MadeAnchor;
|
|
if (madeAnchor != null)
|
|
{
|
|
var anchorVirtInfo = ItemsRepeater.TryGetVirtualizationInfo(madeAnchor);
|
|
if (anchorVirtInfo.Index == index)
|
|
{
|
|
element = madeAnchor;
|
|
}
|
|
}
|
|
}
|
|
if (element == null) { element = GetElementFromUniqueIdResetPool(index); };
|
|
if (element == null) { element = GetElementFromPinnedElements(index); }
|
|
if (element == null) { element = GetElementFromElementFactory(index); }
|
|
|
|
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
|
|
if (suppressAutoRecycle)
|
|
{
|
|
virtInfo.AutoRecycleCandidate = false;
|
|
}
|
|
else
|
|
{
|
|
virtInfo.AutoRecycleCandidate = true;
|
|
virtInfo.KeepAlive = true;
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
public void ClearElement(IControl element, bool isClearedDueToCollectionChange)
|
|
{
|
|
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
|
|
var index = virtInfo.Index;
|
|
bool cleared =
|
|
ClearElementToUniqueIdResetPool(element, virtInfo) ||
|
|
ClearElementToPinnedPool(element, virtInfo, isClearedDueToCollectionChange);
|
|
|
|
if (!cleared)
|
|
{
|
|
ClearElementToElementFactory(element);
|
|
}
|
|
|
|
// Both First and Last indices need to be valid or default.
|
|
if (index == _firstRealizedElementIndexHeldByLayout && index == _lastRealizedElementIndexHeldByLayout)
|
|
{
|
|
// First and last were pointing to the same element and that is going away.
|
|
InvalidateRealizedIndicesHeldByLayout();
|
|
}
|
|
else if (index == _firstRealizedElementIndexHeldByLayout)
|
|
{
|
|
// The FirstElement is going away, shrink the range by one.
|
|
++_firstRealizedElementIndexHeldByLayout;
|
|
}
|
|
else if (index == _lastRealizedElementIndexHeldByLayout)
|
|
{
|
|
// Last element is going away, shrink the range by one at the end.
|
|
--_lastRealizedElementIndexHeldByLayout;
|
|
}
|
|
else
|
|
{
|
|
// Index is either outside the range we are keeping track of or inside the range.
|
|
// In both these cases, we just keep the range we have. If this clear was due to
|
|
// a collection change, then in the CollectionChanged event, we will invalidate these guys.
|
|
}
|
|
}
|
|
|
|
public void ClearElementToElementFactory(IControl element)
|
|
{
|
|
_owner.OnElementClearing(element);
|
|
|
|
if (_owner.ItemTemplateShim != null)
|
|
{
|
|
_owner.ItemTemplateShim.RecycleElement(_owner, element);
|
|
}
|
|
else
|
|
{
|
|
// No ItemTemplate to recycle to, remove the element from the children collection.
|
|
if (!_owner.Children.Remove(element))
|
|
{
|
|
throw new InvalidOperationException("ItemsRepeater's child not found in its Children collection.");
|
|
}
|
|
}
|
|
|
|
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
|
|
virtInfo.MoveOwnershipToElementFactory();
|
|
|
|
if (_lastFocusedElement == element)
|
|
{
|
|
// Focused element is going away. Remove the tracked last focused element
|
|
// and pick a reasonable next focus if we can find one within the layout
|
|
// realized elements.
|
|
MoveFocusFromClearedIndex(virtInfo.Index);
|
|
}
|
|
}
|
|
|
|
private void MoveFocusFromClearedIndex(int clearedIndex)
|
|
{
|
|
var focusCandidate = FindFocusCandidate(clearedIndex, out var focusedChild);
|
|
if (focusCandidate != null)
|
|
{
|
|
focusCandidate.Focus();
|
|
_lastFocusedElement = focusedChild;
|
|
|
|
// Add pin to hold the focused element.
|
|
UpdatePin(focusedChild, true /* addPin */);
|
|
}
|
|
else
|
|
{
|
|
// We could not find a candiate.
|
|
_lastFocusedElement = null;
|
|
}
|
|
}
|
|
|
|
IControl FindFocusCandidate(int clearedIndex, out IControl focusedChild)
|
|
{
|
|
// Walk through all the children and find elements with index before and after the cleared index.
|
|
// Note that during a delete the next element would now have the same index.
|
|
int previousIndex = int.MinValue;
|
|
int nextIndex = int.MaxValue;
|
|
IControl nextElement = null;
|
|
IControl previousElement = null;
|
|
|
|
foreach (var child in _owner.Children)
|
|
{
|
|
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child);
|
|
if (virtInfo?.IsHeldByLayout == true)
|
|
{
|
|
int currentIndex = virtInfo.Index;
|
|
if (currentIndex < clearedIndex)
|
|
{
|
|
if (currentIndex > previousIndex)
|
|
{
|
|
previousIndex = currentIndex;
|
|
previousElement = child;
|
|
}
|
|
}
|
|
else if (currentIndex >= clearedIndex)
|
|
{
|
|
// Note that we use >= above because if we deleted the focused element,
|
|
// the next element would have the same index now.
|
|
if (currentIndex < nextIndex)
|
|
{
|
|
nextIndex = currentIndex;
|
|
nextElement = child;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Find the next element if one exists, if not use the previous element.
|
|
// If the container itself is not focusable, find a descendent that is.
|
|
focusedChild = nextElement;
|
|
return nextElement;
|
|
}
|
|
|
|
public int GetElementIndex(VirtualizationInfo virtInfo)
|
|
{
|
|
if (virtInfo == null)
|
|
{
|
|
//Element is not a child of this ItemsRepeater.
|
|
return -1;
|
|
}
|
|
|
|
return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1;
|
|
}
|
|
|
|
public void PrunePinnedElements()
|
|
{
|
|
EnsureEventSubscriptions();
|
|
|
|
// Go through pinned elements and make sure they still have
|
|
// a reason to be pinned.
|
|
for (var i = 0; i < _pinnedPool.Count; ++i)
|
|
{
|
|
var elementInfo = _pinnedPool[i];
|
|
var virtInfo = elementInfo.VirtualizationInfo;
|
|
|
|
if (!virtInfo.IsPinned)
|
|
{
|
|
_pinnedPool.RemoveAt(i);
|
|
--i;
|
|
|
|
// Pinning was the only thing keeping this element alive.
|
|
ClearElementToElementFactory(elementInfo.PinnedElement);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void UpdatePin(IControl element, bool addPin)
|
|
{
|
|
var parent = element.VisualParent;
|
|
var child = (IVisual)element;
|
|
|
|
while (parent != null)
|
|
{
|
|
if (parent is ItemsRepeater repeater)
|
|
{
|
|
var virtInfo = ItemsRepeater.GetVirtualizationInfo((IControl)child);
|
|
if (virtInfo.IsRealized)
|
|
{
|
|
if (addPin)
|
|
{
|
|
virtInfo.AddPin();
|
|
}
|
|
else if (virtInfo.IsPinned)
|
|
{
|
|
if (virtInfo.RemovePin() == 0)
|
|
{
|
|
// ElementFactory is invoked during the measure pass.
|
|
// We will clear the element then.
|
|
repeater.InvalidateMeasure();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
child = parent;
|
|
parent = child.VisualParent;
|
|
}
|
|
}
|
|
|
|
public void OnItemsSourceChanged(object sender, NotifyCollectionChangedEventArgs args)
|
|
{
|
|
// Note: For items that have been removed, the index will not be touched. It will hold
|
|
// the old index before it was removed. It is not valid anymore.
|
|
switch (args.Action)
|
|
{
|
|
case NotifyCollectionChangedAction.Add:
|
|
{
|
|
var newIndex = args.NewStartingIndex;
|
|
var newCount = args.NewItems.Count;
|
|
EnsureFirstLastRealizedIndices();
|
|
if (newIndex <= _lastRealizedElementIndexHeldByLayout)
|
|
{
|
|
_lastRealizedElementIndexHeldByLayout += newCount;
|
|
foreach (var element in _owner.Children)
|
|
{
|
|
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
|
|
var dataIndex = virtInfo.Index;
|
|
|
|
if (virtInfo.IsRealized && dataIndex >= newIndex)
|
|
{
|
|
UpdateElementIndex(element, virtInfo, dataIndex + newCount);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Indices held by layout are not affected
|
|
// We could still have items in the pinned elements that need updates. This is usually a very small vector.
|
|
for (var i = 0; i < _pinnedPool.Count; ++i)
|
|
{
|
|
var elementInfo = _pinnedPool[i];
|
|
var virtInfo = elementInfo.VirtualizationInfo;
|
|
var dataIndex = virtInfo.Index;
|
|
|
|
if (virtInfo.IsRealized && dataIndex >= newIndex)
|
|
{
|
|
var element = elementInfo.PinnedElement;
|
|
UpdateElementIndex(element, virtInfo, dataIndex + newCount);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case NotifyCollectionChangedAction.Replace:
|
|
{
|
|
// Requirement: oldStartIndex == newStartIndex. It is not a replace if this is not true.
|
|
// Two cases here
|
|
// case 1: oldCount == newCount
|
|
// indices are not affected. nothing to do here.
|
|
// case 2: oldCount != newCount
|
|
// Replaced with less or more items. This is like an insert or remove
|
|
// depending on the counts.
|
|
var oldStartIndex = args.OldStartingIndex;
|
|
var newStartingIndex = args.NewStartingIndex;
|
|
var oldCount = args.OldItems.Count;
|
|
var newCount = args.NewItems.Count;
|
|
if (oldStartIndex != newStartingIndex)
|
|
{
|
|
throw new NotSupportedException("Replace is only allowed with OldStartingIndex equals to NewStartingIndex.");
|
|
}
|
|
|
|
if (oldCount == 0)
|
|
{
|
|
throw new NotSupportedException("Replace notification with args.OldItemsCount value of 0 is not allowed. Use Insert action instead.");
|
|
}
|
|
|
|
if (newCount == 0)
|
|
{
|
|
throw new NotSupportedException("Replace notification with args.NewItemCount value of 0 is not allowed. Use Remove action instead.");
|
|
}
|
|
|
|
int countChange = newCount - oldCount;
|
|
if (countChange != 0)
|
|
{
|
|
// countChange > 0 : countChange items were added
|
|
// countChange < 0 : -countChange items were removed
|
|
foreach (var element in _owner.Children)
|
|
{
|
|
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
|
|
var dataIndex = virtInfo.Index;
|
|
|
|
if (virtInfo.IsRealized)
|
|
{
|
|
if (dataIndex >= oldStartIndex + oldCount)
|
|
{
|
|
UpdateElementIndex(element, virtInfo, dataIndex + countChange);
|
|
}
|
|
}
|
|
}
|
|
|
|
EnsureFirstLastRealizedIndices();
|
|
_lastRealizedElementIndexHeldByLayout += countChange;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case NotifyCollectionChangedAction.Remove:
|
|
{
|
|
var oldStartIndex = args.OldStartingIndex;
|
|
var oldCount = args.OldItems.Count;
|
|
foreach (var element in _owner.Children)
|
|
{
|
|
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
|
|
var dataIndex = virtInfo.Index;
|
|
|
|
if (virtInfo.IsRealized)
|
|
{
|
|
if (virtInfo.AutoRecycleCandidate && oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount)
|
|
{
|
|
// If we are doing the mapping, remove the element who's data was removed.
|
|
_owner.ClearElementImpl(element);
|
|
}
|
|
else if (dataIndex >= (oldStartIndex + oldCount))
|
|
{
|
|
UpdateElementIndex(element, virtInfo, dataIndex - oldCount);
|
|
}
|
|
}
|
|
}
|
|
|
|
InvalidateRealizedIndicesHeldByLayout();
|
|
break;
|
|
}
|
|
|
|
case NotifyCollectionChangedAction.Reset:
|
|
if (_owner.ItemsSourceView.HasKeyIndexMapping)
|
|
{
|
|
_isDataSourceStableResetPending = true;
|
|
}
|
|
|
|
// Walk through all the elements and make sure they are cleared, they will go into
|
|
// the stable id reset pool.
|
|
foreach (var element in _owner.Children)
|
|
{
|
|
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
|
|
if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
|
|
{
|
|
_owner.ClearElementImpl(element);
|
|
}
|
|
}
|
|
|
|
InvalidateRealizedIndicesHeldByLayout();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void EnsureFirstLastRealizedIndices()
|
|
{
|
|
if (_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault)
|
|
{
|
|
// This will ensure that the indexes are updated.
|
|
GetElementIfAlreadyHeldByLayout(0);
|
|
}
|
|
}
|
|
|
|
public void OnLayoutChanging()
|
|
{
|
|
if (_owner.ItemsSourceView?.HasKeyIndexMapping == true)
|
|
{
|
|
_isDataSourceStableResetPending = true;
|
|
}
|
|
}
|
|
|
|
public void OnOwnerArranged()
|
|
{
|
|
if (_isDataSourceStableResetPending)
|
|
{
|
|
_isDataSourceStableResetPending = false;
|
|
|
|
foreach (var entry in _resetPool)
|
|
{
|
|
// TODO: Task 14204306: ItemsRepeater: Find better focus candidate when focused element is deleted in the ItemsSource.
|
|
// Focused element is getting cleared. Need to figure out semantics on where
|
|
// focus should go when the focused element is removed from the data collection.
|
|
ClearElement(entry.Value, true /* isClearedDueToCollectionChange */);
|
|
}
|
|
|
|
_resetPool.Clear();
|
|
}
|
|
}
|
|
|
|
// We optimize for the case where index is not realized to return null as quickly as we can.
|
|
// Flow layouts manage containers on their own and will never ask for an index that is already realized.
|
|
// If an index that is realized is requested by the layout, we unfortunately have to walk the
|
|
// children. Not ideal, but a reasonable default to provide consistent behavior between virtualizing
|
|
// and non-virtualizing hosts.
|
|
private IControl GetElementIfAlreadyHeldByLayout(int index)
|
|
{
|
|
IControl element = null;
|
|
|
|
bool cachedFirstLastIndicesInvalid = _firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault;
|
|
bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout);
|
|
|
|
if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange)
|
|
{
|
|
foreach (var child in _owner.Children)
|
|
{
|
|
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child);
|
|
if (virtInfo?.IsHeldByLayout == true)
|
|
{
|
|
// Only give back elements held by layout. If someone else is holding it, they will be served by other methods.
|
|
int childIndex = virtInfo.Index;
|
|
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, childIndex);
|
|
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, childIndex);
|
|
if (virtInfo.Index == index)
|
|
{
|
|
element = child;
|
|
// If we have valid first/last indices, we don't have to walk the rest, but if we
|
|
// do not, then we keep walking through the entire children collection to get accurate
|
|
// indices once.
|
|
if (!cachedFirstLastIndicesInvalid)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
private IControl GetElementFromUniqueIdResetPool(int index)
|
|
{
|
|
IControl element = null;
|
|
// See if you can get it from the reset pool.
|
|
if (_isDataSourceStableResetPending)
|
|
{
|
|
element = _resetPool.Remove(index);
|
|
if (element != null)
|
|
{
|
|
// Make sure that the index is updated to the current one
|
|
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
|
|
virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool();
|
|
UpdateElementIndex(element, virtInfo, index);
|
|
}
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
private IControl GetElementFromPinnedElements(int index)
|
|
{
|
|
IControl element = null;
|
|
|
|
// See if you can find something among the pinned elements.
|
|
for (var i = 0; i < _pinnedPool.Count; ++i)
|
|
{
|
|
var elementInfo = _pinnedPool[i];
|
|
var virtInfo = elementInfo.VirtualizationInfo;
|
|
|
|
if (virtInfo.Index == index)
|
|
{
|
|
_pinnedPool.RemoveAt(i);
|
|
element = elementInfo.PinnedElement;
|
|
elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool();
|
|
break;
|
|
}
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
// There are several cases handled here with respect to which element gets returned and when DataContext is modified.
|
|
//
|
|
// 1. If there is no ItemTemplate:
|
|
// 1.1 If data is an IControl -> the data is returned
|
|
// 1.2 If data is not an IControl -> a default DataTemplate is used to fetch element and DataContext is set to data
|
|
//
|
|
// 2. If there is an ItemTemplate:
|
|
// 2.1 If data is not an IControl -> Element is fetched from ElementFactory and DataContext is set to the data
|
|
// 2.2 If data is an IControl:
|
|
// 2.2.1 If Element returned by the ElementFactory is the same as the data -> Element (a.k.a. data) is returned as is
|
|
// 2.2.2 If Element returned by the ElementFactory is not the same as the data
|
|
// -> Element that is fetched from the ElementFactory is returned and
|
|
// DataContext is set to the data's DataContext (if it exists), otherwise it is set to the data itself
|
|
private IControl GetElementFromElementFactory(int index)
|
|
{
|
|
// The view generator is the provider of last resort.
|
|
var data = _owner.ItemsSourceView.GetAt(index);
|
|
var providedElementFactory = _owner.ItemTemplateShim;
|
|
|
|
ItemTemplateWrapper GetElementFactory()
|
|
{
|
|
if (providedElementFactory == null)
|
|
{
|
|
var factory = FuncDataTemplate.Default;
|
|
_owner.ItemTemplate = factory;
|
|
return _owner.ItemTemplateShim;
|
|
}
|
|
|
|
return providedElementFactory;
|
|
}
|
|
|
|
IControl GetElement()
|
|
{
|
|
if (providedElementFactory == null)
|
|
{
|
|
if (data is IControl dataAsElement)
|
|
{
|
|
return dataAsElement;
|
|
}
|
|
}
|
|
|
|
var elementFactory = GetElementFactory();
|
|
return elementFactory.GetElement(_owner, data);
|
|
}
|
|
|
|
var element = GetElement();
|
|
|
|
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
|
|
if (virtInfo == null)
|
|
{
|
|
virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element);
|
|
}
|
|
|
|
if (data != element)
|
|
{
|
|
// Prepare the element
|
|
element.DataContext = data;
|
|
}
|
|
|
|
virtInfo.MoveOwnershipToLayoutFromElementFactory(
|
|
index,
|
|
/* uniqueId: */
|
|
_owner.ItemsSourceView.HasKeyIndexMapping ?
|
|
_owner.ItemsSourceView.KeyFromIndex(index) :
|
|
string.Empty);
|
|
|
|
// The view generator is the only provider that prepares the element.
|
|
var repeater = _owner;
|
|
|
|
// Add the element to the children collection here before raising OnElementPrepared so
|
|
// that handlers can walk up the tree in case they want to find their IndexPath in the
|
|
// nested case.
|
|
var children = repeater.Children;
|
|
if (element.VisualParent != repeater)
|
|
{
|
|
children.Add(element);
|
|
}
|
|
|
|
repeater.OnElementPrepared(element, index);
|
|
|
|
// Update realized indices
|
|
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
|
|
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
|
|
|
|
return element;
|
|
}
|
|
|
|
private bool ClearElementToUniqueIdResetPool(IControl element, VirtualizationInfo virtInfo)
|
|
{
|
|
if (_isDataSourceStableResetPending)
|
|
{
|
|
_resetPool.Add(element);
|
|
virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
|
|
}
|
|
|
|
return _isDataSourceStableResetPending;
|
|
}
|
|
|
|
private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange)
|
|
{
|
|
bool moveToPinnedPool =
|
|
!isClearedDueToCollectionChange && virtInfo.IsPinned;
|
|
|
|
if (moveToPinnedPool)
|
|
{
|
|
_pinnedPool.Add(new PinnedElementInfo(element));
|
|
virtInfo.MoveOwnershipToPinnedPool();
|
|
}
|
|
|
|
return moveToPinnedPool;
|
|
}
|
|
|
|
private void UpdateFocusedElement()
|
|
{
|
|
IControl focusedElement = null;
|
|
|
|
var child = FocusManager.Instance.Current;
|
|
|
|
if (child != null)
|
|
{
|
|
var parent = child.VisualParent;
|
|
var owner = _owner;
|
|
|
|
// Find out if the focused element belongs to one of our direct
|
|
// children.
|
|
while (parent != null)
|
|
{
|
|
if (parent is ItemsRepeater repeater)
|
|
{
|
|
var element = child as IControl;
|
|
if (repeater == owner && ItemsRepeater.GetVirtualizationInfo(element).IsRealized)
|
|
{
|
|
focusedElement = element;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
child = parent as IInputElement;
|
|
parent = child.VisualParent;
|
|
}
|
|
}
|
|
|
|
// If the focused element has changed,
|
|
// we need to unpin the old one and pin the new one.
|
|
if (_lastFocusedElement != focusedElement)
|
|
{
|
|
if (_lastFocusedElement != null)
|
|
{
|
|
UpdatePin(_lastFocusedElement, false /* addPin */);
|
|
}
|
|
|
|
if (focusedElement != null)
|
|
{
|
|
UpdatePin(focusedElement, true /* addPin */);
|
|
}
|
|
|
|
_lastFocusedElement = focusedElement;
|
|
}
|
|
}
|
|
|
|
private void OnFocusChanged(object sender, RoutedEventArgs e) => UpdateFocusedElement();
|
|
|
|
private void EnsureEventSubscriptions()
|
|
{
|
|
if (!_eventsSubscribed)
|
|
{
|
|
_owner.GotFocus += OnFocusChanged;
|
|
_owner.LostFocus += OnFocusChanged;
|
|
}
|
|
}
|
|
|
|
private void UpdateElementIndex(IControl element, VirtualizationInfo virtInfo, int index)
|
|
{
|
|
var oldIndex = virtInfo.Index;
|
|
if (oldIndex != index)
|
|
{
|
|
virtInfo.UpdateIndex(index);
|
|
_owner.OnElementIndexChanged(element, oldIndex, index);
|
|
}
|
|
}
|
|
|
|
private void InvalidateRealizedIndicesHeldByLayout()
|
|
{
|
|
_firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
|
|
_lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
|
|
}
|
|
|
|
private struct PinnedElementInfo
|
|
{
|
|
public PinnedElementInfo(IControl element)
|
|
{
|
|
PinnedElement = element;
|
|
VirtualizationInfo = ItemsRepeater.GetVirtualizationInfo(element);
|
|
}
|
|
|
|
public IControl PinnedElement { get; }
|
|
public VirtualizationInfo VirtualizationInfo { get; }
|
|
}
|
|
}
|
|
}
|
|
|