Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XY focus #13947

Merged
merged 34 commits into from
Feb 6, 2024
Merged

XY focus #13947

Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7eb8949
Init
maxkatz6 Dec 4, 2023
7be86a6
Remove XY navigation cache as it's no use
maxkatz6 Dec 12, 2023
6355bf2
Use pooled collection for XY navigation
maxkatz6 Dec 12, 2023
6eb271c
Restructure code a bit, fix IScroller handling
maxkatz6 Dec 13, 2023
640189b
Init KeyboardNavigationTests_XY tests
maxkatz6 Dec 13, 2023
d85c97d
Simplify XYFocus.GetNextFocusableElement usage
maxkatz6 Dec 14, 2023
7de2a13
Minor fixes
maxkatz6 Dec 14, 2023
b4600db
Add more tests
maxkatz6 Dec 14, 2023
e484de5
Remove unused NuiKeyboardNavigationHandler
maxkatz6 Dec 14, 2023
11883e2
Finalizing
maxkatz6 Dec 15, 2023
1e4e978
Fix tests
maxkatz6 Dec 15, 2023
24b6d2e
Add TODO12
maxkatz6 Dec 15, 2023
7436ae0
Make XYFocusOptions a class
maxkatz6 Dec 15, 2023
087c8ea
Add TestServices.FocusableWindow and make KeyboardNavigationHandler l…
maxkatz6 Dec 15, 2023
df318d7
Fix KeyboardNavigationHandler events handling, when focus was not act…
maxkatz6 Dec 15, 2023
629a4a3
Add arrow key tests
maxkatz6 Dec 15, 2023
b7759cf
Merge branch 'master' into xy-focus
maxkatz6 Jan 11, 2024
446cea0
Replace XYFocusKeyboardNavigationMode with more flexible XYFocusNavig…
maxkatz6 Jan 11, 2024
6ea95db
Make XY focus navigation less broken, when there is no starting focus…
maxkatz6 Jan 11, 2024
42e3dad
Several Android TV compatibility improvements
maxkatz6 Jan 11, 2024
592d0ac
Remap tizen Back button to Esc
maxkatz6 Jan 11, 2024
48c547a
Merge remote-tracking branch 'origin/master' into xy-focus
maxkatz6 Jan 21, 2024
45b3dba
Merge remote-tracking branch 'origin/master' into xy-focus
maxkatz6 Jan 28, 2024
1737ddf
Merge branch 'master' into xy-focus
maxkatz6 Feb 3, 2024
0e7fd61
Merge branch 'master' into xy-focus
maxkatz6 Feb 3, 2024
d0d3348
Introduce internal XYFocusHelpers
maxkatz6 Feb 3, 2024
514a313
Make ComboBox and AutoCompleteBox handle Key events only when it's ne…
maxkatz6 Feb 3, 2024
27f8681
Make TextBox handle Key events only when it's needed
maxkatz6 Feb 3, 2024
7462c62
Ignore Alt+Down when XY navigation is enabled in CalendarDatePicker a…
maxkatz6 Feb 3, 2024
24d428b
Merge remote-tracking branch 'origin/master' into xy-focus
maxkatz6 Feb 3, 2024
32b2008
Rename IsAllowedXYNavigationMode
maxkatz6 Feb 3, 2024
14037cd
Fix ButtonSpinner with XY navigation
maxkatz6 Feb 3, 2024
758bb46
Implement a very simple focus engagement for GridSplitter and Slider
maxkatz6 Feb 3, 2024
c39a0a8
Merge branch 'master' into xy-focus
maxkatz6 Feb 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions samples/ControlCatalog/MainView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@
<TabItem Header="Flyouts">
<pages:FlyoutsPage />
</TabItem>
<TabItem Header="Focus">
<pages:FocusPage />
</TabItem>
<TabItem Header="Gestures">
<pages:GesturePage />
</TabItem>
Expand Down
50 changes: 50 additions & 0 deletions samples/ControlCatalog/Pages/FocusPage.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<UserControl 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:generic="clr-namespace:System.Collections.Generic;assembly=netstandard"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ControlCatalog.Pages.FocusPage">
<TabControl>
<TabItem Header="XY Focus">
<StackPanel x:Name="TabRoot" XYFocus.KeyboardNavigationEnabled="{Binding #KeyboardNavigation.SelectedItem}">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="Navigation: " />
<ComboBox x:Name="KeyboardNavigation" SelectedIndex="0">
<ComboBox.ItemsSource>
<generic:List x:TypeArguments="XYFocusKeyboardNavigationMode">
<XYFocusKeyboardNavigationMode>Enabled</XYFocusKeyboardNavigationMode>
<XYFocusKeyboardNavigationMode>Disabled</XYFocusKeyboardNavigationMode>
</generic:List>
</ComboBox.ItemsSource>
</ComboBox>
<ComboBox x:Name="NavigationStrategy" SelectedIndex="0">
<ComboBox.ItemsSource>
<generic:List x:TypeArguments="XYFocusNavigationStrategy">
<XYFocusNavigationStrategy>Projection</XYFocusNavigationStrategy>
<XYFocusNavigationStrategy>NavigationDirectionDistance</XYFocusNavigationStrategy>
<XYFocusNavigationStrategy>RectilinearDistance</XYFocusNavigationStrategy>
</generic:List>
</ComboBox.ItemsSource>
</ComboBox>
</StackPanel>

<Canvas HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Width="500">
<Canvas.Styles>
<Style Selector="Button">
<Setter Property="XYFocus.UpNavigationStrategy" Value="{Binding #NavigationStrategy.SelectedItem}" />
<Setter Property="XYFocus.DownNavigationStrategy" Value="{Binding #NavigationStrategy.SelectedItem}" />
<Setter Property="XYFocus.LeftNavigationStrategy" Value="{Binding #NavigationStrategy.SelectedItem}" />
<Setter Property="XYFocus.RightNavigationStrategy" Value="{Binding #NavigationStrategy.SelectedItem}" />
</Style>
</Canvas.Styles>

<Button Canvas.Top="0" Canvas.Left="50" Width="150" Height="150">A</Button>
<Button Canvas.Top="150" Canvas.Left="400" Width="50" Height="50">C</Button>
<Button Canvas.Top="200" Canvas.Left="0" Width="50" Height="50">B</Button>
<Button Canvas.Top="300" Canvas.Left="100" Width="50" Height="50">D</Button>
</Canvas>
</StackPanel>
</TabItem>
</TabControl>
</UserControl>
14 changes: 14 additions & 0 deletions samples/ControlCatalog/Pages/FocusPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace ControlCatalog.Pages;

public partial class FocusPage : UserControl
{
public FocusPage()
{
AvaloniaXamlLoader.Load(this);
}
}

10 changes: 10 additions & 0 deletions src/Avalonia.Base/Controls/IInternalScroller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Runtime.CompilerServices;

namespace Avalonia.Controls.Primitives;

internal interface IInternalScroller
maxkatz6 marked this conversation as resolved.
Show resolved Hide resolved
{
bool CanHorizontallyScroll { get; }

bool CanVerticallyScroll { get; }
}
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Input/FocusManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ internal bool TryMoveFocus(NavigationDirection direction)
/// </summary>
/// <param name="e">The element.</param>
/// <returns>True if the element can be focused.</returns>
private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEffectivelyEnabled && IsVisible(e);
internal static bool CanFocus(IInputElement e) => e.Focusable && e.IsEffectivelyEnabled && IsVisible(e);

/// <summary>
/// Gets the focus scope of the specified control, traversing popups.
Expand Down
20 changes: 19 additions & 1 deletion src/Avalonia.Base/Input/KeyboardNavigationHandler.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Input.Navigation;
using Avalonia.Input;
using Avalonia.Metadata;
using Avalonia.VisualTree;

Expand All @@ -16,7 +17,7 @@ public sealed class KeyboardNavigationHandler : IKeyboardNavigationHandler
/// The window to which the handler belongs.
/// </summary>
private IInputRoot? _owner;

/// <summary>
/// Sets the owner of the keyboard navigation handler.
/// </summary>
Expand Down Expand Up @@ -60,6 +61,9 @@ public void SetOwner(IInputRoot owner)
{
NavigationDirection.Next => TabNavigation.GetNextTab(element, false),
NavigationDirection.Previous => TabNavigation.GetPrevTab(element, null, false),
NavigationDirection.Up or NavigationDirection.Down
or NavigationDirection.Left or NavigationDirection.Right
=> XYFocus.TryDirectionalFocus(direction, element, null),
_ => throw new NotSupportedException(),
};

Expand Down Expand Up @@ -113,6 +117,20 @@ void OnKeyDown(object? sender, KeyEventArgs e)
Move(current, direction, e.KeyModifiers);
e.Handled = true;
}
else if (e.Key is Key.Left or Key.Right or Key.Up or Key.Down)
maxkatz6 marked this conversation as resolved.
Show resolved Hide resolved
{
var current = FocusManager.GetFocusManager(e.Source as IInputElement)?.GetFocusedElement();
var direction = e.Key switch
{
Key.Left => NavigationDirection.Left,
Key.Right => NavigationDirection.Right,
Key.Up => NavigationDirection.Up,
Key.Down => NavigationDirection.Down,
_ => throw new ArgumentOutOfRangeException()
};
Move(current, direction, e.KeyModifiers);
e.Handled = true;
}
}

private static bool HandlePreCustomNavigation(
Expand Down
30 changes: 0 additions & 30 deletions src/Avalonia.Base/Input/Navigation/FocusExtensions.cs

This file was deleted.

165 changes: 165 additions & 0 deletions src/Avalonia.Base/Input/Navigation/XYFocus.Bubbling.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input.Navigation;
using Avalonia.Media;
using Avalonia.VisualTree;

namespace Avalonia.Input;

public partial class XYFocus
{
private static InputElement? GetDirectionOverride(
InputElement element,
InputElement? searchRoot,
NavigationDirection direction,
bool ignoreFocusabililty = false)
{
var index = GetXYFocusPropertyIndex(element, direction);

if (index != null)
{
var overrideElement = element.GetValue(index) as InputElement;

if (overrideElement != null)
{
if ((!ignoreFocusabililty && !FocusManager.CanFocus(overrideElement)))
return null;

// If an override was specified but it is located outside the searchRoot, don't use it as the candidate.
if (searchRoot != null &&
!searchRoot.IsVisualAncestorOf(overrideElement))
return null;

return overrideElement;
}
}

return null;
}

private static InputElement? TryXYFocusBubble(
InputElement element,
InputElement? candidate,
InputElement? searchRoot,
NavigationDirection direction)
{
if (candidate == null)
return null;

var nextFocusableElement = candidate;
var directionOverrideRoot = GetDirectionOverrideRoot(element, searchRoot, direction);

if (directionOverrideRoot != null)
{
var isAncestor = directionOverrideRoot.IsVisualAncestorOf(candidate);
if (!isAncestor)
{
nextFocusableElement = GetDirectionOverride(directionOverrideRoot, searchRoot, direction)
?? nextFocusableElement;
}
}

return nextFocusableElement;
}

private static InputElement? GetDirectionOverrideRoot(
InputElement element,
InputElement? searchRoot,
NavigationDirection direction)
{
var root = element;

while (root != null && GetDirectionOverride(root, searchRoot, direction) == null)
{
root = root.GetVisualParent() as InputElement;
}

return root;
}

private static XYFocusNavigationStrategy GetStrategy(
InputElement element,
NavigationDirection direction,
XYFocusNavigationStrategy? navigationStrategyOverride)
{
var isAutoOverride = navigationStrategyOverride == XYFocusNavigationStrategy.Auto;

if (navigationStrategyOverride.HasValue && !isAutoOverride)
{
// We can cast just by offsetting values because we have ensured that the XYFocusStrategy enums offset as expected
return (XYFocusNavigationStrategy)(int)(navigationStrategyOverride.Value - 1);
}
else if (isAutoOverride && element.GetVisualParent() is InputElement parent)
{
// Skip the element if we have an auto override and look at its parent's strategy
element = parent;
}

var index = GetXYFocusNavigationStrategyPropertyIndex(element, direction);
if (index is not null)
{
var current = element;
while (current != null && current.GetValue(index) is XYFocusNavigationStrategy mode)
{
if (mode != XYFocusNavigationStrategy.Auto)
{
return mode;
}

current = current.GetVisualParent() as InputElement;
}
}

return XYFocusNavigationStrategy.Projection;
}

private static AvaloniaProperty? GetXYFocusPropertyIndex(
InputElement element,
NavigationDirection direction)
{
if (element.FlowDirection == FlowDirection.RightToLeft)
{
if (direction == NavigationDirection.Left) direction = NavigationDirection.Right;
else if (direction == NavigationDirection.Right) direction = NavigationDirection.Left;
}

switch (direction)
{
case NavigationDirection.Left:
return LeftProperty;
case NavigationDirection.Right:
return RightProperty;
case NavigationDirection.Up:
return UpProperty;
case NavigationDirection.Down:
return DownProperty;
}

return null;
}

private static AvaloniaProperty? GetXYFocusNavigationStrategyPropertyIndex(
InputElement element,
NavigationDirection direction)
{
if (element.FlowDirection == FlowDirection.RightToLeft)
{
if (direction == NavigationDirection.Left) direction = NavigationDirection.Right;
else if (direction == NavigationDirection.Right) direction = NavigationDirection.Left;
}

switch (direction)
{
case NavigationDirection.Left:
return LeftNavigationStrategyProperty;
case NavigationDirection.Right:
return RightNavigationStrategyProperty;
case NavigationDirection.Up:
return UpNavigationStrategyProperty;
case NavigationDirection.Down:
return DownNavigationStrategyProperty;
}

return null;
}
}
Loading
Loading