Skip to content

Commit 6ef2300

Browse files
authored
Fixed issues with keyboard focus in Details layout (#7390)
1 parent bc19772 commit 6ef2300

File tree

10 files changed

+373
-186
lines changed

10 files changed

+373
-186
lines changed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.Toolkit.Uwp.UI;
6+
using Microsoft.Toolkit.Uwp.UI.Behaviors;
7+
using Microsoft.Toolkit.Uwp.UI.Animations.Expressions;
8+
using Windows.Foundation;
9+
using Windows.UI.Composition;
10+
using Windows.UI.Xaml;
11+
using Windows.UI.Xaml.Controls;
12+
using Windows.UI.Xaml.Hosting;
13+
using Windows.UI.Xaml.Input;
14+
using Windows.UI.Xaml.Media;
15+
using Windows.Foundation.Metadata;
16+
using System.Linq;
17+
18+
namespace Files.Behaviors
19+
{
20+
/// <summary>
21+
/// Performs an animation on a ListView or GridView Header to make it sticky using composition.
22+
/// </summary>
23+
/// <seealso>
24+
/// <cref>Microsoft.Xaml.Interactivity.Behavior{Windows.UI.Xaml.UIElement}</cref>
25+
/// </seealso>
26+
public class StickyHeaderBehavior : BehaviorBase<FrameworkElement>
27+
{
28+
public static bool IsXamlRootAvailable { get; } = ApiInformation.IsPropertyPresent("Windows.UI.Xaml.UIElement", "XamlRoot");
29+
30+
/// <summary>
31+
/// Attaches the behavior to the associated object.
32+
/// </summary>
33+
/// <returns>
34+
/// <c>true</c> if attaching succeeded; otherwise <c>false</c>.
35+
/// </returns>
36+
protected override bool Initialize()
37+
{
38+
var result = AssignAnimation();
39+
return result;
40+
}
41+
42+
/// <summary>
43+
/// Detaches the behavior from the associated object.
44+
/// </summary>
45+
/// <returns>
46+
/// <c>true</c> if detaching succeeded; otherwise <c>false</c>.
47+
/// </returns>
48+
protected override bool Uninitialize()
49+
{
50+
RemoveAnimation();
51+
return true;
52+
}
53+
54+
/// <summary>
55+
/// The UIElement that will be faded.
56+
/// </summary>
57+
public static readonly DependencyProperty HeaderElementProperty = DependencyProperty.Register(
58+
nameof(HeaderElement), typeof(UIElement), typeof(StickyHeaderBehavior), new PropertyMetadata(null, PropertyChangedCallback));
59+
60+
private ScrollViewer _scrollViewer;
61+
private CompositionPropertySet _scrollProperties;
62+
private CompositionPropertySet _animationProperties;
63+
private Visual _headerVisual, _itemsPanelVisual;
64+
private InsetClip _contentClip;
65+
66+
/// <summary>
67+
/// Gets or sets the target element for the ScrollHeader behavior.
68+
/// </summary>
69+
/// <remarks>
70+
/// Set this using the header of a ListView or GridView.
71+
/// </remarks>
72+
public UIElement HeaderElement
73+
{
74+
get { return (UIElement)GetValue(HeaderElementProperty); }
75+
set { SetValue(HeaderElementProperty, value); }
76+
}
77+
78+
/// <summary>
79+
/// If any of the properties are changed then the animation is automatically started.
80+
/// </summary>
81+
/// <param name="d">The dependency object.</param>
82+
/// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
83+
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
84+
{
85+
var b = d as StickyHeaderBehavior;
86+
b?.AssignAnimation();
87+
}
88+
89+
/// <summary>
90+
/// Uses Composition API to get the UIElement and sets an ExpressionAnimation
91+
/// The ExpressionAnimation uses the height of the UIElement to calculate an opacity value
92+
/// for the Header as it is scrolling off-screen. The opacity reaches 0 when the Header
93+
/// is entirely scrolled off.
94+
/// </summary>
95+
/// <returns><c>true</c> if the assignment was successful; otherwise, <c>false</c>.</returns>
96+
private bool AssignAnimation()
97+
{
98+
StopAnimation();
99+
100+
if (AssociatedObject == null)
101+
{
102+
return false;
103+
}
104+
105+
if (_scrollViewer == null)
106+
{
107+
_scrollViewer = AssociatedObject as ScrollViewer ?? AssociatedObject.FindDescendant<ScrollViewer>();
108+
}
109+
110+
if (_scrollViewer == null)
111+
{
112+
return false;
113+
}
114+
115+
var listView = AssociatedObject as ListViewBase ?? AssociatedObject.FindDescendant<ListViewBase>();
116+
117+
if (listView != null && listView.ItemsPanelRoot != null)
118+
{
119+
Canvas.SetZIndex(listView.ItemsPanelRoot, -1);
120+
}
121+
122+
if (_scrollProperties == null)
123+
{
124+
_scrollProperties = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(_scrollViewer);
125+
}
126+
127+
if (_scrollProperties == null)
128+
{
129+
return false;
130+
}
131+
132+
// Implicit operation: Find the Header object of the control if it uses ListViewBase
133+
if (HeaderElement == null && listView != null)
134+
{
135+
HeaderElement = listView.Header as UIElement;
136+
}
137+
138+
var headerElement = HeaderElement as FrameworkElement;
139+
if (headerElement == null || headerElement.RenderSize.Height == 0)
140+
{
141+
return false;
142+
}
143+
144+
if (_headerVisual == null)
145+
{
146+
_headerVisual = ElementCompositionPreview.GetElementVisual(headerElement);
147+
}
148+
149+
if (_headerVisual == null)
150+
{
151+
return false;
152+
}
153+
154+
headerElement.SizeChanged -= ScrollHeader_SizeChanged;
155+
headerElement.SizeChanged += ScrollHeader_SizeChanged;
156+
157+
_scrollViewer.GotFocus -= ScrollViewer_GotFocus;
158+
_scrollViewer.GotFocus += ScrollViewer_GotFocus;
159+
160+
var compositor = _scrollProperties.Compositor;
161+
162+
if (_animationProperties == null)
163+
{
164+
_animationProperties = compositor.CreatePropertySet();
165+
_animationProperties.InsertScalar("OffsetY", 0.0f);
166+
}
167+
168+
var propSetOffset = _animationProperties.GetReference().GetScalarProperty("OffsetY");
169+
var scrollPropSet = _scrollProperties.GetSpecializedReference<ManipulationPropertySetReferenceNode>();
170+
var expressionAnimation = ExpressionFunctions.Max(propSetOffset - scrollPropSet.Translation.Y, 0);
171+
172+
_headerVisual.StartAnimation("Offset.Y", expressionAnimation);
173+
174+
// Mod: clip items panel below header
175+
var itemsPanel = listView.ItemsPanelRoot;
176+
177+
if (itemsPanel == null)
178+
{
179+
return true;
180+
}
181+
182+
if (_itemsPanelVisual == null)
183+
{
184+
_itemsPanelVisual = ElementCompositionPreview.GetElementVisual(itemsPanel);
185+
_contentClip = compositor.CreateInsetClip();
186+
_itemsPanelVisual.Clip = _contentClip;
187+
}
188+
189+
var expressionClipAnimation = ExpressionFunctions.Max(-scrollPropSet.Translation.Y, 0);
190+
_contentClip.StartAnimation("TopInset", expressionClipAnimation);
191+
192+
return true;
193+
}
194+
195+
/// <summary>
196+
/// Remove the animation from the UIElement.
197+
/// </summary>
198+
private void RemoveAnimation()
199+
{
200+
if (HeaderElement is FrameworkElement element)
201+
{
202+
element.SizeChanged -= ScrollHeader_SizeChanged;
203+
}
204+
205+
if (_scrollViewer != null)
206+
{
207+
_scrollViewer.GotFocus -= ScrollViewer_GotFocus;
208+
}
209+
210+
StopAnimation();
211+
}
212+
213+
/// <summary>
214+
/// Stop the animation of the UIElement.
215+
/// </summary>
216+
private void StopAnimation()
217+
{
218+
_headerVisual?.StopAnimation("Offset.Y");
219+
220+
_animationProperties?.InsertScalar("OffsetY", 0.0f);
221+
222+
_contentClip?.StopAnimation("TopInset");
223+
224+
if (_headerVisual != null)
225+
{
226+
var offset = _headerVisual.Offset;
227+
offset.Y = 0.0f;
228+
_headerVisual.Offset = offset;
229+
}
230+
}
231+
232+
private void ScrollHeader_SizeChanged(object sender, SizeChangedEventArgs e)
233+
{
234+
AssignAnimation();
235+
}
236+
237+
private void ScrollViewer_GotFocus(object sender, RoutedEventArgs e)
238+
{
239+
var scroller = (ScrollViewer)sender;
240+
241+
object focusedElement;
242+
if (IsXamlRootAvailable && scroller.XamlRoot != null)
243+
{
244+
focusedElement = FocusManager.GetFocusedElement(scroller.XamlRoot);
245+
}
246+
else
247+
{
248+
focusedElement = FocusManager.GetFocusedElement();
249+
}
250+
251+
// To prevent Popups (Flyouts...) from triggering the autoscroll, we check if the focused element has a valid parent.
252+
// Popups have no parents, whereas a normal Item would have the ListView as a parent.
253+
if (focusedElement is UIElement element && VisualTreeHelper.GetParent(element) != null)
254+
{
255+
// Mod: ignore if element is child of header
256+
if (!element.FindAscendants().Any(x => x == HeaderElement))
257+
{
258+
FrameworkElement header = (FrameworkElement)HeaderElement;
259+
260+
var point = element.TransformToVisual(scroller).TransformPoint(new Point(0, 0));
261+
262+
if (point.Y < header.ActualHeight)
263+
{
264+
// Mod: do not change scroller horizontal offset
265+
scroller.ChangeView(scroller.HorizontalOffset, scroller.VerticalOffset - (header.ActualHeight - point.Y), 1, false);
266+
}
267+
}
268+
}
269+
}
270+
}
271+
}

src/Files/Events/ContextItemsChangedEventArgs.cs

Lines changed: 0 additions & 16 deletions
This file was deleted.

src/Files/Files.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
<DependentUpon>App.xaml</DependentUpon>
119119
</Compile>
120120
<Compile Include="BaseLayout.cs" />
121+
<Compile Include="Behaviors\StickyHeaderBehavior.cs" />
121122
<Compile Include="CommandLine\CommandLineParser.cs" />
122123
<Compile Include="CommandLine\ParsedCommand.cs" />
123124
<Compile Include="CommandLine\ParsedCommands.cs" />
@@ -167,7 +168,6 @@
167168
<Compile Include="EventArguments\BaseJsonSettingsModelEventArgs.cs" />
168169
<Compile Include="EventArguments\Bundles\BundlesOpenPathEventArgs.cs" />
169170
<Compile Include="EventArguments\LayoutPreferenceEventArgs.cs" />
170-
<Compile Include="Events\ContextItemsChangedEventArgs.cs" />
171171
<Compile Include="Extensions\DateTimeExtensions.cs" />
172172
<Compile Include="Extensions\EnumExtensions.cs" />
173173
<Compile Include="Extensions\ImageSourceExtensions.cs" />
@@ -432,7 +432,6 @@
432432
<Compile Include="Filesystem\StorageHistory\StorageHistory.cs" />
433433
<Compile Include="Filesystem\StorageHistory\StorageHistoryOperations.cs" />
434434
<Compile Include="Filesystem\StorageHistory\StorageHistoryWrapper.cs" />
435-
<Compile Include="Helpers\AcrylicTheme.cs" />
436435
<Compile Include="Helpers\Convert\ErrorCodeConverter.cs" />
437436
<Compile Include="Helpers\StorageHelpers.cs" />
438437
<Compile Include="Helpers\DialogDisplayHelper.cs" />

src/Files/Helpers/AcrylicTheme.cs

Lines changed: 0 additions & 56 deletions
This file was deleted.

src/Files/Helpers/ThemeHelper.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,16 @@ private static void ApplyTheme()
8888
switch (rootTheme)
8989
{
9090
case ElementTheme.Default:
91-
App.AppSettings.AcrylicTheme.SetDefault();
9291
titleBar.ButtonHoverBackgroundColor = (Color)Application.Current.Resources["SystemBaseLowColor"];
9392
titleBar.ButtonForegroundColor = (Color)Application.Current.Resources["SystemBaseHighColor"];
9493
break;
9594

9695
case ElementTheme.Light:
97-
App.AppSettings.AcrylicTheme.SetLightTheme();
9896
titleBar.ButtonHoverBackgroundColor = Color.FromArgb(51, 0, 0, 0);
9997
titleBar.ButtonForegroundColor = Colors.Black;
10098
break;
10199

102100
case ElementTheme.Dark:
103-
App.AppSettings.AcrylicTheme.SetDarkTheme();
104101
titleBar.ButtonHoverBackgroundColor = Color.FromArgb(51, 255, 255, 255);
105102
titleBar.ButtonForegroundColor = Colors.White;
106103
break;

0 commit comments

Comments
 (0)