From acdf599dec7c7932815fa20c58e8f904cbe5f3fb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Jun 2016 20:40:23 +0200 Subject: [PATCH] Initial impl. of scrolling with arrow keys. - Currently only vertical implemented - Doesn't handle partially visible items at end of list --- Avalonia.sln | 4 ++ src/Avalonia.Controls/IVirtualizingPanel.cs | 13 ++++-- .../Presenters/ItemVirtualizer.cs | 17 +++++++ .../Presenters/ItemVirtualizerSimple.cs | 46 +++++++++++++++++-- .../Presenters/ItemsPresenter.cs | 9 +++- .../Presenters/ScrollContentPresenter.cs | 5 +- .../Primitives/ILogicalScrollable.cs | 12 ++++- src/Avalonia.Controls/StackPanel.cs | 12 +++++ .../VirtualizingStackPanel.cs | 18 ++++++++ ...ScrollContentPresenterTests_IScrollable.cs | 8 +++- .../VirtualizingStackPanelTests.cs | 20 ++++++++ 11 files changed, 151 insertions(+), 13 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index 928ba71ec8..9740e31ce2 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -161,6 +161,8 @@ EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4 + src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4 + src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{2f59f3d0-748d-4652-b01e-e0d954756308}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{db070a10-bf39-4752-8456-86e9d5928478}*SharedItemsImports = 4 @@ -168,11 +170,13 @@ Global src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4 samples\TestApplicationShared\TestApplicationShared.projitems*{78345174-5b52-4a14-b9fd-d5f2428137f0}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{54f237d5-a70a-4752-9656-0c70b1a7b047}*SharedItemsImports = 4 + samples\TestApplicationShared\TestApplicationShared.projitems*{ff69b927-c545-49ae-8e16-3d14d621aa12}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{811a76cf-1cf6-440f-963b-bbe31bd72a82}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 + samples\TestApplicationShared\TestApplicationShared.projitems*{8c923867-8a8f-4f6b-8b80-47d9e8436166}*SharedItemsImports = 4 samples\TestApplicationShared\TestApplicationShared.projitems*{e3a1060b-50d0-44e8-88b6-f44ef2e5bd72}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4 diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index ca75517240..5d35fa1ec8 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -1,9 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using Avalonia.Controls.Primitives; - namespace Avalonia.Controls { /// @@ -25,11 +22,21 @@ namespace Avalonia.Controls /// /// Gets a value indicating whether the panel is full. /// + /// + /// This property should return false until enough children are added to fill the space + /// passed into the last measure in the direction of scroll. It should be updated + /// immediately after a child is added or removed. + /// bool IsFull { get; } /// /// Gets the number of items that can be removed while keeping the panel full. /// + /// + /// This property should return the number of children that are completely out of the + /// panel's current bounds in the direction of scroll. It should be updated after an + /// arrange. + /// int OverflowCount { get; } /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 8f77cff32b..d690d4768b 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Specialized; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; +using Avalonia.Input; using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters @@ -145,6 +146,17 @@ namespace Avalonia.Controls.Presenters { } + /// + /// Gets the next control in the specified direction. + /// + /// The movement direction. + /// The control from which movement begins. + /// The control. + public virtual IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + return null; + } + /// /// Called when the items for the presenter change, either because /// has been set, the items collection has been @@ -168,5 +180,10 @@ namespace Avalonia.Controls.Presenters { return false; } + + /// + /// Invalidates the current scroll. + /// + protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll(); } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 78ac75e428..29094789a4 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -5,8 +5,8 @@ using System; using System.Collections; using System.Collections.Specialized; using System.Linq; -using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; +using Avalonia.Input; namespace Avalonia.Controls.Presenters { @@ -87,7 +87,7 @@ namespace Avalonia.Controls.Presenters public override void UpdateControls() { CreateAndRemoveContainers(); - ((ILogicalScrollable)Owner).InvalidateScroll(); + InvalidateScroll(); } /// @@ -134,7 +134,47 @@ namespace Avalonia.Controls.Presenters VirtualizingPanel.Children.Clear(); } - ((ILogicalScrollable)Owner).InvalidateScroll(); + InvalidateScroll(); + } + + public override IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + var generator = Owner.ItemContainerGenerator; + var itemIndex = generator.IndexFromContainer(from); + + if (itemIndex == -1) + { + return null; + } + + var newItemIndex = -1; + + if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) + { + switch (direction) + { + case FocusNavigationDirection.Up: + newItemIndex = itemIndex - 1; + break; + case FocusNavigationDirection.Down: + newItemIndex = itemIndex + 1; + break; + } + } + + if (newItemIndex >= 0 && newItemIndex < ItemCount) + { + + if (newItemIndex < FirstIndex || newItemIndex >= NextIndex) + { + OffsetValue += newItemIndex - itemIndex; + InvalidateScroll(); + } + + return generator.ContainerFromIndex(newItemIndex); + } + + return null; } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index b442c5bc61..21580a7cf2 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Specialized; using Avalonia.Controls.Primitives; using Avalonia.Input; -using Avalonia.VisualTree; using static Avalonia.Utilities.MathUtilities; namespace Avalonia.Controls.Presenters @@ -73,11 +72,17 @@ namespace Avalonia.Controls.Presenters Size ILogicalScrollable.PageScrollSize => new Size(0, 1); /// - bool ILogicalScrollable.BringIntoView(IVisual target, Rect targetRect) + bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect) { return _virtualizer?.BringIntoView(target, targetRect) ?? false; } + /// + IControl ILogicalScrollable.GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + return _virtualizer?.GetControlInDirection(direction, from); + } + /// protected override void PanelCreated(IPanel panel) { diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 0aeb61a529..c7a302f71d 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -118,10 +118,11 @@ namespace Avalonia.Controls.Presenters } var scrollable = Child as ILogicalScrollable; + var control = target as IControl; - if (scrollable?.IsLogicalScrollEnabled == true) + if (scrollable?.IsLogicalScrollEnabled == true && control != null) { - return scrollable.BringIntoView(target, targetRect); + return scrollable.BringIntoView(control, targetRect); } var transform = target.TransformToVisual(Child); diff --git a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs index b8b90f83a9..3fb201affc 100644 --- a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs +++ b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.VisualTree; +using Avalonia.Input; namespace Avalonia.Controls.Primitives { @@ -56,6 +56,14 @@ namespace Avalonia.Controls.Primitives /// The target visual. /// The portion of the target visual to bring into view. /// True if the scroll offset was changed; otherwise false. - bool BringIntoView(IVisual target, Rect targetRect); + bool BringIntoView(IControl target, Rect targetRect); + + /// + /// Gets the next control in the specified direction. + /// + /// The movement direction. + /// The control from which movement begins. + /// The control. + IControl GetControlInDirection(FocusNavigationDirection direction, IControl from); } } diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 0284d84df3..c52b738d8b 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -73,6 +73,18 @@ namespace Avalonia.Controls /// The control from which movement begins. /// The control. IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from) + { + var fromControl = from as IControl; + return (fromControl != null) ? GetControlInDirection(direction, fromControl) : null; + } + + /// + /// Gets the next control in the specified direction. + /// + /// The movement direction. + /// The control from which movement begins. + /// The control. + protected virtual IInputElement GetControlInDirection(FocusNavigationDirection direction, IControl from) { var horiz = Orientation == Orientation.Horizontal; int index = Children.IndexOf((IControl)from); diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 77aae2548d..e4f639bdcb 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -3,6 +3,9 @@ using System; using System.Collections.Specialized; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.VisualTree; @@ -107,6 +110,21 @@ namespace Avalonia.Controls } } + protected override IInputElement GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + var logicalScrollable = Parent as ILogicalScrollable; + var fromControl = from as IControl; + + if (logicalScrollable?.IsLogicalScrollEnabled == true && fromControl != null) + { + return logicalScrollable.GetControlInDirection(direction, fromControl); + } + else + { + return base.GetControlInDirection(direction, from); + } + } + internal override void ArrangeChild( IControl child, Rect rect, diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs index d0c6a386ac..142df9c6eb 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs @@ -5,6 +5,7 @@ using System; using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.VisualTree; using Xunit; @@ -293,7 +294,7 @@ namespace Avalonia.Controls.UnitTests } } - public bool BringIntoView(IVisual target, Rect targetRect) + public bool BringIntoView(IControl target, Rect targetRect) { throw new NotImplementedException(); } @@ -303,6 +304,11 @@ namespace Avalonia.Controls.UnitTests AvailableSize = availableSize; return new Size(150, 150); } + + public IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + throw new NotImplementedException(); + } } } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index d5217e007b..9034076444 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -1,6 +1,9 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.LogicalTree; using Moq; using Xunit; @@ -42,6 +45,7 @@ namespace Avalonia.Controls.UnitTests target.Measure(new Size(100, 100)); + Assert.Equal(new Size(0, 0), target.DesiredSize); Assert.Equal(new Size(0, 0), target.Bounds.Size); Assert.False(target.IsFull); @@ -75,6 +79,22 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(2, target.OverflowCount); } + + [Fact] + public void Passes_Navigation_Request_To_ILogicalScrollable_Parent() + { + var presenter = new Mock().As(); + var scrollable = presenter.As(); + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + var from = new Canvas(); + + scrollable.Setup(x => x.IsLogicalScrollEnabled).Returns(true); + + ((ISetLogicalParent)target).SetParent(presenter.Object); + ((INavigableContainer)target).GetControl(FocusNavigationDirection.Next, from); + + scrollable.Verify(x => x.GetControlInDirection(FocusNavigationDirection.Next, from)); + } } } } \ No newline at end of file