Browse Source

Merge pull request #3602 from AvaloniaUI/feature/menuitem-inputgesturetext

Add MenuItem.InputGestureText.
pull/3652/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
881c103c6e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      samples/ControlCatalog/MainWindow.xaml
  2. 14
      samples/ControlCatalog/MainWindow.xaml.cs
  3. 4
      samples/ControlCatalog/Pages/MenuPage.xaml
  4. 195
      src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs
  5. 47
      src/Avalonia.Controls/MenuItem.cs
  6. 11
      src/Avalonia.Controls/Primitives/AccessText.cs
  7. 44
      src/Avalonia.Input/KeyGesture.cs
  8. 35
      src/Avalonia.Themes.Default/MenuItem.xaml
  9. 1
      src/Avalonia.Themes.Default/NativeMenuBar.xaml
  10. 31
      tests/Avalonia.Input.UnitTests/KeyGestureTests.cs

6
samples/ControlCatalog/MainWindow.xaml

@ -14,7 +14,7 @@
<NativeMenuItem Header="File">
<NativeMenuItem.Menu>
<NativeMenu>
<NativeMenuItem Header="Open" Clicked="OnOpenClicked"/>
<NativeMenuItem Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
<NativeMenuItemSeperator/>
<NativeMenuItem Header="Recent">
<NativeMenuItem.Menu>
@ -22,7 +22,9 @@
</NativeMenuItem.Menu>
</NativeMenuItem>
<NativeMenuItemSeperator/>
<NativeMenuItem Header="Quit Avalonia" Clicked="OnCloseClicked" Gesture="CMD+Q"/>
<NativeMenuItem Header="{x:Static local:MainWindow.MenuQuitHeader}"
Gesture="{x:Static local:MainWindow.MenuQuitGesture}"
Clicked="OnCloseClicked" />
</NativeMenu>
</NativeMenuItem.Menu>
</NativeMenuItem>

14
samples/ControlCatalog/MainWindow.xaml.cs

@ -1,13 +1,11 @@
using System;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using ControlCatalog.ViewModels;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ControlCatalog
{
@ -35,6 +33,12 @@ namespace ControlCatalog
mainMenu.AttachedToVisualTree += MenuAttached;
}
public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit";
public static KeyGesture MenuQuitGesture => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
new KeyGesture(Key.Q, KeyModifiers.Meta) :
new KeyGesture(Key.F4, KeyModifiers.Alt);
public void MenuAttached(object sender, VisualTreeAttachmentEventArgs e)
{
if (NativeMenu.GetIsNativeMenuExported(this) && sender is Menu mainMenu)

4
samples/ControlCatalog/Pages/MenuPage.xaml

@ -16,13 +16,13 @@
<TextBlock Classes="h3" Margin="4 8">Defined in XAML</TextBlock>
<Menu>
<MenuItem Header="_First">
<MenuItem Header="Standard _Menu Item"/>
<MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A"/>
<Separator/>
<MenuItem Header="Menu with _Submenu">
<MenuItem Header="Submenu _1"/>
<MenuItem Header="Submenu _2"/>
</MenuItem>
<MenuItem Header="Menu Item with _Icon">
<MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
<MenuItem.Icon>
<Image Source="/Assets/github_icon.png"/>
</MenuItem.Icon>

195
src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs

@ -0,0 +1,195 @@
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Data.Converters;
using Avalonia.Input;
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Converts a <see cref="KeyGesture"/> to a string, formatting it according to the current
/// platform's style guidelines.
/// </summary>
public class PlatformKeyGestureConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is null)
{
return null;
}
else if (value is KeyGesture gesture && targetType == typeof(string))
{
return ToPlatformString(gesture);
}
else
{
throw new NotSupportedException();
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
/// <summary>
/// Converts a <see cref="KeyGesture"/> to a string, formatting it according to the current
/// platform's style guidelines.
/// </summary>
/// <param name="gesture">The gesture.</param>
/// <returns>The gesture formatted according to the current platform.</returns>
public static string ToPlatformString(KeyGesture gesture)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return ToString(gesture, "Win");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return ToString(gesture, "Super");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return ToOSXString(gesture);
}
else
{
return gesture.ToString();
}
}
private static string ToString(KeyGesture gesture, string meta)
{
var s = new StringBuilder();
static void Plus(StringBuilder s)
{
if (s.Length > 0)
{
s.Append("+");
}
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
{
s.Append("Ctrl");
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
{
Plus(s);
s.Append("Shift");
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
{
Plus(s);
s.Append("Alt");
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
{
Plus(s);
s.Append(meta);
}
Plus(s);
s.Append(ToString(gesture.Key));
return s.ToString();
}
private static string ToOSXString(KeyGesture gesture)
{
var s = new StringBuilder();
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
{
s.Append('⌃');
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
{
s.Append('⌥');
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
{
s.Append('⇧');
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
{
s.Append('⌘');
}
s.Append(ToOSXString(gesture.Key));
return s.ToString();
}
private static string ToString(Key key)
{
return key switch
{
Key.Add => "+",
Key.Back => "Backspace",
Key.D0 => "0",
Key.D1 => "1",
Key.D2 => "2",
Key.D3 => "3",
Key.D4 => "4",
Key.D5 => "5",
Key.D6 => "6",
Key.D7 => "7",
Key.D8 => "8",
Key.D9 => "9",
Key.Decimal => ".",
Key.Divide => "/",
Key.Down => "Down Arrow",
Key.Left => "Left Arrow",
Key.Multiply => "*",
Key.OemBackslash => "\\",
Key.OemCloseBrackets => "]",
Key.OemComma => ",",
Key.OemMinus => "-",
Key.OemOpenBrackets => "[",
Key.OemPeriod=> ".",
Key.OemPipe => "|",
Key.OemPlus => "+",
Key.OemQuestion => "/",
Key.OemQuotes => "\"",
Key.OemSemicolon => ";",
Key.OemTilde => "`",
Key.Right => "Right Arrow",
Key.Separator => "/",
Key.Subtract => "-",
Key.Up => "Up Arrow",
_ => key.ToString(),
};
}
private static string ToOSXString(Key key)
{
return key switch
{
Key.Back => "⌫",
Key.Down => "↓",
Key.End => "↘",
Key.Escape => "⎋",
Key.Home => "↖",
Key.Left => "←",
Key.Return => "↩",
Key.PageDown => "⇞",
Key.PageUp => "⇟",
Key.Right => "→",
Key.Space => "␣",
Key.Tab => "⇥",
Key.Up => "↑",
_ => ToString(key),
};
}
}
}

47
src/Avalonia.Controls/MenuItem.cs

@ -13,6 +13,7 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -48,6 +49,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<object> IconProperty =
AvaloniaProperty.Register<MenuItem, object>(nameof(Icon));
/// <summary>
/// Defines the <see cref="InputGesture"/> property.
/// </summary>
public static readonly StyledProperty<KeyGesture> InputGestureProperty =
AvaloniaProperty.Register<MenuItem, KeyGesture>(nameof(InputGesture));
/// <summary>
/// Defines the <see cref="IsSelected"/> property.
/// </summary>
@ -93,6 +100,7 @@ namespace Avalonia.Controls
private ICommand _command;
private bool _commandCanExecute = true;
private Popup _popup;
private IDisposable _gridHack;
/// <summary>
/// Initializes static members of the <see cref="MenuItem"/> class.
@ -194,6 +202,19 @@ namespace Avalonia.Controls
set { SetValue(IconProperty, value); }
}
/// <summary>
/// Gets or sets the input gesture that will be displayed in the menu item.
/// </summary>
/// <remarks>
/// Setting this property does not cause the input gesture to be handled by the menu item,
/// it simply displays the gesture text in the menu.
/// </remarks>
public KeyGesture InputGesture
{
get { return GetValue(InputGestureProperty); }
set { SetValue(InputGestureProperty, value); }
}
/// <summary>
/// Gets or sets a value indicating whether the <see cref="MenuItem"/> is currently selected.
/// </summary>
@ -304,6 +325,32 @@ namespace Avalonia.Controls
{
Command.CanExecuteChanged -= CanExecuteChanged;
}
_gridHack?.Dispose();
_gridHack = null;
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (this.GetVisualParent() is IControl parent)
{
// HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached
// property to store SharedSizeGroup state, except property inheritance is done
// down the logical tree. In this case, the control which is setting
// Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing
// the way Grid stores shared size state, the developers of WPF just created a
// binding of the internal state of the visual parent to the menu item. We don't
// have much choice but to do the same for now unless we want to refactor Grid,
// which I honestly am not brave enough to do right now. Here's the same hack in
// the WPF codebase:
//
// https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141
_gridHack = Bind(
DefinitionBase.PrivateSharedSizeScopeProperty,
parent.GetBindingObservable(DefinitionBase.PrivateSharedSizeScopeProperty));
}
}
/// <summary>

11
src/Avalonia.Controls/Primitives/AccessText.cs

@ -160,17 +160,6 @@ namespace Avalonia.Controls.Primitives
return base.CreateTextLayout(constraint, StripAccessKey(text));
}
/// <summary>
/// Measures the control.
/// </summary>
/// <param name="availableSize">The available size for the control.</param>
/// <returns>The desired size.</returns>
protected override Size MeasureOverride(Size availableSize)
{
var result = base.MeasureOverride(availableSize);
return result.WithHeight(result.Height + 1);
}
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{

44
src/Avalonia.Input/KeyGesture.cs

@ -1,9 +1,11 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
namespace Avalonia.Input
{
@ -108,19 +110,43 @@ namespace Avalonia.Input
public override string ToString()
{
var parts = new List<string>();
var s = new StringBuilder();
foreach (var flag in Enum.GetValues(typeof(KeyModifiers)).Cast<KeyModifiers>())
static void Plus(StringBuilder s)
{
if (KeyModifiers.HasFlag(flag) && flag != KeyModifiers.None)
if (s.Length > 0)
{
parts.Add(flag.ToString());
s.Append("+");
}
}
parts.Add(Key.ToString());
if (KeyModifiers.HasFlagCustom(KeyModifiers.Control))
{
s.Append("Ctrl");
}
if (KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
{
Plus(s);
s.Append("Shift");
}
if (KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
{
Plus(s);
s.Append("Alt");
}
if (KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
{
Plus(s);
s.Append("Cmd");
}
Plus(s);
s.Append(Key);
return string.Join(" + ", parts);
return s.ToString();
}
public bool Matches(KeyEventArgs keyEvent) => ResolveNumPadOperationKey(keyEvent.Key) == Key && keyEvent.KeyModifiers == KeyModifiers;
@ -141,7 +167,9 @@ namespace Avalonia.Input
return KeyModifiers.Control;
}
if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase))
if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase) ||
modifier.Equals("win".AsSpan(), StringComparison.OrdinalIgnoreCase) ||
modifier.Equals("⌘".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return KeyModifiers.Meta;
}

35
src/Avalonia.Themes.Default/MenuItem.xaml

@ -1,6 +1,10 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
xmlns:sys="clr-namespace:System;assembly=netstandard">
<Styles.Resources>
<conv:PlatformKeyGestureConverter x:Key="KeyGestureConverter"/>
</Styles.Resources>
<Style Selector="MenuItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="1"/>
@ -11,7 +15,14 @@
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid ColumnDefinitions="20,5,*,20">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemIGT"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<ContentPresenter Name="icon"
Content="{TemplateBinding Icon}"
Width="16"
@ -36,12 +47,16 @@
</DataTemplate>
</ContentPresenter.DataTemplates>
</ContentPresenter>
<TextBlock x:Name="PART_InputGestureText"
Grid.Column="3"
Text="{TemplateBinding InputGesture, Converter={StaticResource KeyGestureConverter}}"
VerticalAlignment="Center"/>
<Path Name="rightArrow"
Data="M0,0L4,3.5 0,7z"
Fill="{DynamicResource ThemeForegroundBrush}"
Margin="10,0,0,0"
VerticalAlignment="Center"
Grid.Column="3"/>
Grid.Column="4"/>
<Popup Name="PART_Popup"
PlacementMode="Right"
StaysOpen="True"
@ -54,7 +69,7 @@
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="4 2"/>
Grid.IsSharedSizeScope="True"/>
</ScrollViewer>
</Border>
</Popup>
@ -100,10 +115,10 @@
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="2"/>
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Grid.IsSharedSizeScope="True"/>
</ScrollViewer>
</Border>
</Popup>
@ -113,6 +128,10 @@
</Setter>
</Style>
<Style Selector="MenuItem /template/ ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Margin" Value="2"/>
</Style>
<Style Selector="MenuItem:selected /template/ Border#root">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>

1
src/Avalonia.Themes.Default/NativeMenuBar.xaml

@ -13,6 +13,7 @@
<Menu.Styles>
<Style Selector="MenuItem">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="InputGesture" Value="{Binding Gesture}"/>
<Setter Property="Items" Value="{Binding Menu.Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>

31
tests/Avalonia.Input.UnitTests/KeyGestureTests.cs

@ -1,19 +1,31 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Xunit;
namespace Avalonia.Input.UnitTests
{
public class KeyGestureTests
{
public static readonly IEnumerable<object[]> SampleData = new object[][]
public static readonly IEnumerable<object[]> ParseData = new object[][]
{
new object[]{"Ctrl+A", new KeyGesture(Key.A, InputModifiers.Control)},
new object[]{" \tShift\t+Alt +B", new KeyGesture(Key.B, InputModifiers.Shift | InputModifiers.Alt) },
new object[]{"Control++", new KeyGesture(Key.OemPlus, InputModifiers.Control) }
new object[]{"Ctrl+A", new KeyGesture(Key.A, KeyModifiers.Control)},
new object[]{" \tShift\t+Alt +B", new KeyGesture(Key.B, KeyModifiers.Shift | KeyModifiers.Alt) },
new object[]{"Control++", new KeyGesture(Key.OemPlus, KeyModifiers.Control) },
new object[]{ "Shift+⌘+A", new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift) },
new object[]{ "Shift+Cmd+A", new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift) },
};
public static readonly IEnumerable<object[]> ToStringData = new object[][]
{
new object[]{new KeyGesture(Key.A), "A"},
new object[]{new KeyGesture(Key.A, KeyModifiers.Control), "Ctrl+A"},
new object[]{new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Shift), "Ctrl+Shift+A"},
new object[]{new KeyGesture(Key.A, KeyModifiers.Alt | KeyModifiers.Shift), "Shift+Alt+A"},
new object[]{new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift), "Ctrl+Shift+Alt+A"},
new object[]{new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift), "Shift+Cmd+A"},
};
[Theory]
[MemberData(nameof(SampleData))]
[MemberData(nameof(ParseData))]
public void Key_Gesture_Is_Able_To_Parse_Sample_Data(string text, KeyGesture gesture)
{
Assert.Equal(gesture, KeyGesture.Parse(text));
@ -32,5 +44,12 @@ namespace Avalonia.Input.UnitTests
Key = pressedKey
}));
}
[Theory]
[MemberData(nameof(ToStringData))]
public void ToString_Produces_Correct_Results(KeyGesture gesture, string expected)
{
Assert.Equal(expected, gesture.ToString());
}
}
}

Loading…
Cancel
Save