Browse Source

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 <maxkatz6@outlook.com>
release/11.1.0-beta2
workgroupengineering 2 years ago
committed by Max Katz
parent
commit
d958f02387
  1. 8
      samples/ControlCatalog/Pages/TabControlPage.xaml
  2. 9
      src/Avalonia.Controls/TabItem.cs
  3. 4
      src/Avalonia.Themes.Fluent/Controls/TabItem.xaml
  4. 4
      src/Avalonia.Themes.Simple/Controls/TabItem.xaml
  5. 120
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  6. 19
      tests/Avalonia.UnitTests/TestServices.cs
  7. 7
      tests/Avalonia.UnitTests/UnitTestApplication.cs

8
samples/ControlCatalog/Pages/TabControlPage.xaml

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

9
src/Avalonia.Controls/TabItem.cs

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

4
src/Avalonia.Themes.Fluent/Controls/TabItem.xaml

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

4
src/Avalonia.Themes.Simple/Controls/TabItem.xaml

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

120
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -10,10 +10,13 @@ using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Moq;
using Xunit; using Xunit;
namespace Avalonia.Controls.UnitTests namespace Avalonia.Controls.UnitTests
@ -226,7 +229,7 @@ namespace Avalonia.Controls.UnitTests
Child = (target = new TabControl Child = (target = new TabControl
{ {
Template = TabControlTemplate(), Template = TabControlTemplate(),
Items = Items =
{ {
new TabItem new TabItem
{ {
@ -639,7 +642,7 @@ namespace Avalonia.Controls.UnitTests
x => Assert.Equal(Dock.Right, x.TabStripPlacement) x => Assert.Equal(Dock.Right, x.TabStripPlacement)
); );
} }
[Fact] [Fact]
public void TabItem_TabStripPlacement_Should_Be_Correctly_Set_For_New_Items() public void TabItem_TabStripPlacement_Should_Be_Correctly_Set_For_New_Items()
{ {
@ -658,7 +661,7 @@ namespace Avalonia.Controls.UnitTests
ApplyTemplate(target); ApplyTemplate(target);
target.ItemsSource = items; target.ItemsSource = items;
var result = target.GetLogicalChildren() var result = target.GetLogicalChildren()
.OfType<TabItem>() .OfType<TabItem>()
.ToList(); .ToList();
@ -678,7 +681,93 @@ namespace Avalonia.Controls.UnitTests
x => Assert.Equal(Dock.Right, x.TabStripPlacement) 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() private static IControlTemplate TabControlTemplate()
{ {
return new FuncControlTemplate<TabControl>((parent, scope) => return new FuncControlTemplate<TabControl>((parent, scope) =>
@ -693,8 +782,8 @@ namespace Avalonia.Controls.UnitTests
new ContentPresenter new ContentPresenter
{ {
Name = "PART_SelectedContentHost", Name = "PART_SelectedContentHost",
[!ContentPresenter.ContentProperty] = parent[!TabControl.SelectedContentProperty], [~ContentPresenter.ContentProperty] = new TemplateBinding(TabControl.SelectedContentProperty),
[!ContentPresenter.ContentTemplateProperty] = parent[!TabControl.SelectedContentTemplateProperty], [~ContentPresenter.ContentTemplateProperty] = new TemplateBinding(TabControl.SelectedContentTemplateProperty),
}.RegisterInNameScope(scope) }.RegisterInNameScope(scope)
} }
}); });
@ -706,11 +795,26 @@ namespace Avalonia.Controls.UnitTests
new ContentPresenter new ContentPresenter
{ {
Name = "PART_ContentPresenter", Name = "PART_ContentPresenter",
[!ContentPresenter.ContentProperty] = parent[!TabItem.HeaderProperty], [~ContentPresenter.ContentProperty] = new TemplateBinding(TabItem.HeaderProperty),
[!ContentPresenter.ContentTemplateProperty] = parent[!TabItem.HeaderTemplateProperty] [~ContentPresenter.ContentTemplateProperty] = new TemplateBinding(TabItem.HeaderTemplateProperty),
RecognizesAccessKey = true,
}.RegisterInNameScope(scope)); }.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) private static void Prepare(TabControl target)
{ {
ApplyTemplate(target); ApplyTemplate(target);

19
tests/Avalonia.UnitTests/TestServices.cs

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

7
tests/Avalonia.UnitTests/UnitTestApplication.cs

@ -1,13 +1,10 @@
using System; using System;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Rendering;
using Avalonia.Threading; using Avalonia.Threading;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Concurrency;
using System.Threading; using System.Threading;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.Animation; using Avalonia.Animation;
@ -81,7 +78,9 @@ namespace Avalonia.UnitTests
.Bind<ICursorFactory>().ToConstant(Services.StandardCursorFactory) .Bind<ICursorFactory>().ToConstant(Services.StandardCursorFactory)
.Bind<IWindowingPlatform>().ToConstant(Services.WindowingPlatform) .Bind<IWindowingPlatform>().ToConstant(Services.WindowingPlatform)
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>() .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 // 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 // See https://github.com/AvaloniaUI/Avalonia/issues/10081

Loading…
Cancel
Save