diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 248f94082d..d25de9c1f5 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -14,7 +14,7 @@ - + @@ -22,7 +22,9 @@ - + diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 38cbde9d92..b40fdb4a17 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/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) diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index 868f0df6ad..de9ea34e80 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -16,13 +16,13 @@ Defined in XAML - + - + diff --git a/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs b/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs new file mode 100644 index 0000000000..603a470a93 --- /dev/null +++ b/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 +{ + /// + /// Converts a to a string, formatting it according to the current + /// platform's style guidelines. + /// + 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(); + } + + /// + /// Converts a to a string, formatting it according to the current + /// platform's style guidelines. + /// + /// The gesture. + /// The gesture formatted according to the current platform. + 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), + }; + } + } +} diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index e0baa5e679..3164e56222 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/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 IconProperty = AvaloniaProperty.Register(nameof(Icon)); + /// + /// Defines the property. + /// + public static readonly StyledProperty InputGestureProperty = + AvaloniaProperty.Register(nameof(InputGesture)); + /// /// Defines the property. /// @@ -93,6 +100,7 @@ namespace Avalonia.Controls private ICommand _command; private bool _commandCanExecute = true; private Popup _popup; + private IDisposable _gridHack; /// /// Initializes static members of the class. @@ -194,6 +202,19 @@ namespace Avalonia.Controls set { SetValue(IconProperty, value); } } + /// + /// Gets or sets the input gesture that will be displayed in the menu item. + /// + /// + /// 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. + /// + public KeyGesture InputGesture + { + get { return GetValue(InputGestureProperty); } + set { SetValue(InputGestureProperty, value); } + } + /// /// Gets or sets a value indicating whether the is currently selected. /// @@ -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)); + } } /// diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 7e5c434caf..bed5389cb3 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -160,17 +160,6 @@ namespace Avalonia.Controls.Primitives return base.CreateTextLayout(constraint, StripAccessKey(text)); } - /// - /// Measures the control. - /// - /// The available size for the control. - /// The desired size. - protected override Size MeasureOverride(Size availableSize) - { - var result = base.MeasureOverride(availableSize); - return result.WithHeight(result.Height + 1); - } - /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs index 490c31bef9..32236fe382 100644 --- a/src/Avalonia.Input/KeyGesture.cs +++ b/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(); + var s = new StringBuilder(); - foreach (var flag in Enum.GetValues(typeof(KeyModifiers)).Cast()) + 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; } diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 769ab893bf..314416cda0 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -1,6 +1,10 @@ - + + + + +