1
0
Эх сурвалжийг харах

feat(TabControl): Enable Recognizes AccessKey (#15013)

* feat(TabControl): Enable Recognizes AccessKey

* update sample

* test: Should_TabControl_Recognizes_AccessKey

* fix: test

* fix: using

---------

Co-authored-by: Max Katz <[email protected]>
workgroupengineering 1 жил өмнө
parent
commit
78a7f3836d

+ 4 - 4
samples/ControlCatalog/Pages/TabControlPage.xaml

@@ -29,7 +29,7 @@
             DockPanel.Dock="Top" 
             Classes="h2"
             Text="A tab control that displays a tab strip along with the content of the selected tab"
-            Margin="4">          
+            Margin="4">
         </TextBlock>
         <Grid 
             ColumnDefinitions="*,*" 
@@ -45,19 +45,19 @@
                 <TabControl
                     Margin="0 16"
                     TabStripPlacement="{Binding TabPlacement}">
-                    <TabItem Header="Arch">
+                    <TabItem Header="_Arch">
                         <StackPanel Orientation="Vertical" Spacing="8">
                             <TextBlock>This is the first page in the TabControl.</TextBlock>
                             <Image Source="/Assets/delicate-arch-896885_640.jpg" Width="300"/>
                         </StackPanel>
                     </TabItem>
-                    <TabItem Header="Leaf">                       
+                    <TabItem Header="_Leaf">
                         <StackPanel Orientation="Vertical" Spacing="8">
                             <TextBlock>This is the second page in the TabControl.</TextBlock>
                             <Image Source="/Assets/maple-leaf-888807_640.jpg" Width="300"/>
                         </StackPanel>
                     </TabItem>
-                    <TabItem Header="Disabled" IsEnabled="False">
+                    <TabItem Header="_Disabled" IsEnabled="False">
                         <TextBlock>You should not see this.</TextBlock>
                     </TabItem>
                 </TabControl>

+ 9 - 0
src/Avalonia.Controls/TabItem.cs

@@ -4,6 +4,8 @@ using Avalonia.Automation.Peers;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Interactivity;
 
 namespace Avalonia.Controls
 {
@@ -37,6 +39,7 @@ namespace Avalonia.Controls
             FocusableProperty.OverrideDefaultValue(typeof(TabItem), true);
             DataContextProperty.Changed.AddClassHandler<TabItem>((x, e) => x.UpdateHeader(e));
             AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<TabItem>(AutomationControlType.TabItem);
+            AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<TabItem>((tabItem, args) => tabItem.TabItemActivated(args));
         }
 
         /// <summary>
@@ -91,5 +94,11 @@ namespace Avalonia.Controls
                 }
             }
         }
+
+        private void TabItemActivated(RoutedEventArgs args)
+        {
+            SetCurrentValue(IsSelectedProperty, true);
+            args.Handled = true;
+        }
     }
 }

+ 3 - 1
src/Avalonia.Themes.Fluent/Controls/TabItem.xaml

@@ -40,7 +40,9 @@
                 HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                 VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                 Content="{TemplateBinding Header}"
-                ContentTemplate="{TemplateBinding HeaderTemplate}" />
+                ContentTemplate="{TemplateBinding HeaderTemplate}"
+                RecognizesAccessKey="True"
+                />
             <Border Name="PART_SelectedPipe"
                     Background="{DynamicResource TabItemHeaderSelectedPipeFill}"
                     CornerRadius="{DynamicResource ControlCornerRadius}"

+ 3 - 1
src/Avalonia.Themes.Simple/Controls/TabItem.xaml

@@ -19,7 +19,9 @@
                           BorderThickness="{TemplateBinding BorderThickness}"
                           Content="{TemplateBinding Header}"
                           ContentTemplate="{TemplateBinding HeaderTemplate}"
-                          CornerRadius="{TemplateBinding CornerRadius}" />
+                          CornerRadius="{TemplateBinding CornerRadius}"
+                          RecognizesAccessKey="True"
+                          />
       </ControlTemplate>
     </Setter>
     <Style Selector="^:disabled /template/ ContentPresenter#PART_ContentPresenter">

+ 112 - 8
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@@ -10,10 +10,13 @@ using Avalonia.Controls.Templates;
 using Avalonia.Controls.Utils;
 using Avalonia.Data;
 using Avalonia.Input;
+using Avalonia.Layout;
 using Avalonia.LogicalTree;
 using Avalonia.Markup.Xaml;
+using Avalonia.Platform;
 using Avalonia.Styling;
 using Avalonia.UnitTests;
+using Moq;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests
@@ -226,7 +229,7 @@ namespace Avalonia.Controls.UnitTests
                 Child = (target = new TabControl
                 {
                     Template = TabControlTemplate(),
-                    Items = 
+                    Items =
                     {
                         new TabItem
                         {
@@ -639,7 +642,7 @@ namespace Avalonia.Controls.UnitTests
                 x => Assert.Equal(Dock.Right, x.TabStripPlacement)
             );
         }
-        
+
         [Fact]
         public void TabItem_TabStripPlacement_Should_Be_Correctly_Set_For_New_Items()
         {
@@ -658,7 +661,7 @@ namespace Avalonia.Controls.UnitTests
             ApplyTemplate(target);
 
             target.ItemsSource = items;
-            
+
             var result = target.GetLogicalChildren()
                 .OfType<TabItem>()
                 .ToList();
@@ -678,7 +681,93 @@ namespace Avalonia.Controls.UnitTests
                 x => Assert.Equal(Dock.Right, x.TabStripPlacement)
             );
         }
-        
+
+        [Theory]
+        [InlineData(Key.A, 1)]
+        [InlineData(Key.L, 2)]
+        [InlineData(Key.D, 0)]
+        public void Should_TabControl_Recognizes_AccessKey(Key accessKey, int selectedTabIndex)
+        {
+            var ah = new AccessKeyHandler();
+            using (UnitTestApplication.Start(TestServices.StyledWindow.With(accessKeyHandler: ah)))
+            {
+                var impl = CreateMockTopLevelImpl();
+
+                var tabControl = new TabControl()
+                {
+                    Template = TabControlTemplate(),
+                    Items =
+                    {
+                        new TabItem
+                        {
+                            Header = "General",
+                        },
+                        new TabItem { Header = "_Arch" },
+                        new TabItem { Header = "_Leaf"},
+                        new TabItem { Header = "_Disabled", IsEnabled = false },
+                    }
+                };
+
+                var root = new TestTopLevel(impl.Object)
+                {
+                    Template = CreateTemplate(),
+                    Content = tabControl,
+                };
+
+                root.ApplyTemplate();
+                root.Presenter.UpdateChild();
+                ApplyTemplate(tabControl);
+
+                KeyDown(root, Key.LeftAlt);
+                KeyDown(root, accessKey, KeyModifiers.Alt);
+                KeyUp(root, accessKey, KeyModifiers.Alt);
+                KeyUp(root, Key.LeftAlt);
+
+                Assert.Equal(selectedTabIndex, tabControl.SelectedIndex);
+            }
+
+            static FuncControlTemplate<TestTopLevel> CreateTemplate()
+            {
+                return new FuncControlTemplate<TestTopLevel>((x, scope) =>
+                    new ContentPresenter
+                    {
+                        Name = "PART_ContentPresenter",
+                        [~ContentPresenter.ContentProperty] = new TemplateBinding(ContentControl.ContentProperty),
+                        [~ContentPresenter.ContentTemplateProperty] = new TemplateBinding(ContentControl.ContentTemplateProperty)
+                    }.RegisterInNameScope(scope));
+            }
+
+            static Mock<ITopLevelImpl> CreateMockTopLevelImpl(bool setupProperties = false)
+            {
+                var topLevel = new Mock<ITopLevelImpl>();
+                if (setupProperties)
+                    topLevel.SetupAllProperties();
+                topLevel.Setup(x => x.RenderScaling).Returns(1);
+                topLevel.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
+                return topLevel;
+            }
+
+            static void KeyDown(IInputElement target, Key key, KeyModifiers modifiers = KeyModifiers.None)
+            {
+                target.RaiseEvent(new KeyEventArgs
+                {
+                    RoutedEvent = InputElement.KeyDownEvent,
+                    Key = key,
+                    KeyModifiers = modifiers,
+                });
+            }
+
+            static void KeyUp(IInputElement target, Key key, KeyModifiers modifiers = KeyModifiers.None)
+            {
+                target.RaiseEvent(new KeyEventArgs
+                {
+                    RoutedEvent = InputElement.KeyUpEvent,
+                    Key = key,
+                    KeyModifiers = modifiers,
+                });
+            }
+        }
+
         private static IControlTemplate TabControlTemplate()
         {
             return new FuncControlTemplate<TabControl>((parent, scope) =>
@@ -693,8 +782,8 @@ namespace Avalonia.Controls.UnitTests
                         new ContentPresenter
                         {
                             Name = "PART_SelectedContentHost",
-                            [!ContentPresenter.ContentProperty] = parent[!TabControl.SelectedContentProperty],
-                            [!ContentPresenter.ContentTemplateProperty] = parent[!TabControl.SelectedContentTemplateProperty],
+                            [~ContentPresenter.ContentProperty] = new TemplateBinding(TabControl.SelectedContentProperty),
+                            [~ContentPresenter.ContentTemplateProperty] = new TemplateBinding(TabControl.SelectedContentTemplateProperty),
                         }.RegisterInNameScope(scope)
                     }
                 });
@@ -706,11 +795,26 @@ namespace Avalonia.Controls.UnitTests
                 new ContentPresenter
                 {
                     Name = "PART_ContentPresenter",
-                    [!ContentPresenter.ContentProperty] = parent[!TabItem.HeaderProperty],
-                    [!ContentPresenter.ContentTemplateProperty] = parent[!TabItem.HeaderTemplateProperty]
+                    [~ContentPresenter.ContentProperty] = new TemplateBinding(TabItem.HeaderProperty),
+                    [~ContentPresenter.ContentTemplateProperty] = new TemplateBinding(TabItem.HeaderTemplateProperty),
+                    RecognizesAccessKey = true,
                 }.RegisterInNameScope(scope));
         }
 
+        private class TestTopLevel : TopLevel
+        {
+            private readonly ILayoutManager _layoutManager;
+            public bool IsClosed { get; private set; }
+
+            public TestTopLevel(ITopLevelImpl impl, ILayoutManager layoutManager = null)
+                : base(impl)
+            {
+                _layoutManager = layoutManager ?? new LayoutManager(this);
+            }
+
+            private protected override ILayoutManager CreateLayoutManager() => _layoutManager;
+        }
+
         private static void Prepare(TabControl target)
         {
             ApplyTemplate(target);

+ 10 - 9
tests/Avalonia.UnitTests/TestServices.cs

@@ -1,17 +1,11 @@
 using System;
 using Moq;
 using Avalonia.Input;
-using Avalonia.Layout;
-using Avalonia.Markup.Xaml;
-using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Styling;
 using Avalonia.Themes.Simple;
 using Avalonia.Rendering;
 using System.Reactive.Concurrency;
-using System.Collections.Generic;
-using Avalonia.Controls;
-using System.Reflection;
 using Avalonia.Animation;
 using Avalonia.Headless;
 using Avalonia.Threading;
@@ -129,18 +123,22 @@ namespace Avalonia.UnitTests
             IFontManagerImpl fontManagerImpl = null,
             ITextShaperImpl textShaperImpl = null,
             IWindowImpl windowImpl = null,
-            IWindowingPlatform windowingPlatform = null) : this(assetLoader, focusManager, inputManager, keyboardDevice,
+            IWindowingPlatform windowingPlatform = null,
+            IAccessKeyHandler accessKeyHandler = null
+            ) : this(assetLoader, focusManager, inputManager, keyboardDevice,
             keyboardNavigation,
             mouseDevice, platform, renderInterface, renderLoop, standardCursorFactory, theme,
             dispatcherImpl, fontManagerImpl, textShaperImpl, windowImpl, windowingPlatform)
         {
             GlobalClock = globalClock;
+            AccessKeyHandler = accessKeyHandler;
         }
 
         public IAssetLoader AssetLoader { get; }
         public IInputManager InputManager { get; }
         public IFocusManager FocusManager { get; }
         internal IGlobalClock GlobalClock { get; set; }
+        internal IAccessKeyHandler AccessKeyHandler { get; }
         public Func<IKeyboardDevice> KeyboardDevice { get; }
         public Func<IKeyboardNavigationHandler> KeyboardNavigation { get; }
         public Func<IMouseDevice> MouseDevice { get; }
@@ -172,7 +170,8 @@ namespace Avalonia.UnitTests
             ITextShaperImpl textShaperImpl = null,
             IWindowImpl windowImpl = null,
             IWindowingPlatform windowingPlatform = null,
-            IGlobalClock globalClock = null)
+            IGlobalClock globalClock = null,
+            IAccessKeyHandler accessKeyHandler = null)
         {
             return new TestServices(
                 globalClock ?? GlobalClock,
@@ -190,7 +189,9 @@ namespace Avalonia.UnitTests
                 theme: theme ?? Theme,
                 dispatcherImpl: dispatcherImpl ?? DispatcherImpl,
                 windowingPlatform: windowingPlatform ?? WindowingPlatform,
-                windowImpl: windowImpl ?? WindowImpl);
+                windowImpl: windowImpl ?? WindowImpl,
+                accessKeyHandler: accessKeyHandler ?? AccessKeyHandler
+                );
         }
 
         private static IStyle CreateSimpleTheme()

+ 3 - 4
tests/Avalonia.UnitTests/UnitTestApplication.cs

@@ -1,13 +1,10 @@
 using System;
 using Avalonia.Input;
-using Avalonia.Layout;
 using Avalonia.Platform;
 using Avalonia.Styling;
 using Avalonia.Controls;
-using Avalonia.Rendering;
 using Avalonia.Threading;
 using System.Reactive.Disposables;
-using System.Reactive.Concurrency;
 using System.Threading;
 using Avalonia.Input.Platform;
 using Avalonia.Animation;
@@ -81,7 +78,9 @@ namespace Avalonia.UnitTests
                 .Bind<ICursorFactory>().ToConstant(Services.StandardCursorFactory)
                 .Bind<IWindowingPlatform>().ToConstant(Services.WindowingPlatform)
                 .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
-                .Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>();
+                .Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
+                .Bind<IAccessKeyHandler>().ToConstant(Services.AccessKeyHandler)
+                ;
             
             // This is a hack to make tests work, we need to refactor the way font manager is registered
             // See https://github.com/AvaloniaUI/Avalonia/issues/10081