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

feat(TabControl): Enable Recognizes AccessKey #15013

8 changes: 4 additions & 4 deletions samples/ControlCatalog/Pages/TabControlPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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="*,*"
Expand All @@ -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>
Expand Down
9 changes: 9 additions & 0 deletions src/Avalonia.Controls/TabItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;

namespace Avalonia.Controls
{
Expand Down Expand Up @@ -37,6 +39,7 @@ static TabItem()
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>
Expand Down Expand Up @@ -91,5 +94,11 @@ private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj)
}
}
}

private void TabItemActivated(RoutedEventArgs args)
{
SetCurrentValue(IsSelectedProperty, true);
args.Handled = true;
}
}
}
4 changes: 3 additions & 1 deletion src/Avalonia.Themes.Fluent/Controls/TabItem.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
4 changes: 3 additions & 1 deletion src/Avalonia.Themes.Simple/Controls/TabItem.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
120 changes: 112 additions & 8 deletions tests/Avalonia.Controls.UnitTests/TabControlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
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
Expand Down Expand Up @@ -226,7 +229,7 @@ public void TabItem_Templates_Should_Be_Set_Before_TabItem_ApplyTemplate()
Child = (target = new TabControl
{
Template = TabControlTemplate(),
Items =
Items =
{
new TabItem
{
Expand Down Expand Up @@ -639,7 +642,7 @@ public void TabItem_TabStripPlacement_Should_Be_Correctly_Set()
x => Assert.Equal(Dock.Right, x.TabStripPlacement)
);
}

[Fact]
public void TabItem_TabStripPlacement_Should_Be_Correctly_Set_For_New_Items()
{
Expand All @@ -658,7 +661,7 @@ public void TabItem_TabStripPlacement_Should_Be_Correctly_Set_For_New_Items()
ApplyTemplate(target);

target.ItemsSource = items;

var result = target.GetLogicalChildren()
.OfType<TabItem>()
.ToList();
Expand All @@ -678,7 +681,93 @@ public void TabItem_TabStripPlacement_Should_Be_Correctly_Set_For_New_Items()
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) =>
Expand All @@ -693,8 +782,8 @@ private static IControlTemplate TabControlTemplate()
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)
}
});
Expand All @@ -706,11 +795,26 @@ private static IControlTemplate TabItemTemplate()
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);
Expand Down
19 changes: 10 additions & 9 deletions tests/Avalonia.UnitTests/TestServices.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -129,18 +123,22 @@ internal TestServices(
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; }
Expand Down Expand Up @@ -172,7 +170,8 @@ internal TestServices With(
ITextShaperImpl textShaperImpl = null,
IWindowImpl windowImpl = null,
IWindowingPlatform windowingPlatform = null,
IGlobalClock globalClock = null)
IGlobalClock globalClock = null,
IAccessKeyHandler accessKeyHandler = null)
{
return new TestServices(
globalClock ?? GlobalClock,
Expand All @@ -190,7 +189,9 @@ internal TestServices With(
theme: theme ?? Theme,
dispatcherImpl: dispatcherImpl ?? DispatcherImpl,
windowingPlatform: windowingPlatform ?? WindowingPlatform,
windowImpl: windowImpl ?? WindowImpl);
windowImpl: windowImpl ?? WindowImpl,
accessKeyHandler: accessKeyHandler ?? AccessKeyHandler
);
}

private static IStyle CreateSimpleTheme()
Expand Down
7 changes: 3 additions & 4 deletions tests/Avalonia.UnitTests/UnitTestApplication.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -81,7 +78,9 @@ public override void RegisterServices()
.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
Expand Down