From 23725903005e183509ccec3d77e4d38094fb373a Mon Sep 17 00:00:00 2001 From: IanRawley <132860927+IanRawley@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:43:22 +1200 Subject: [PATCH] Implementing IFormattable on KeyGesture (#15828) * Adding KeyGestureFormatInfo to hold platform specific Key/Modifier Strings when performing KeyGesture.ToString() * Implementing KeyGesture IFormattable using new KeyGestureFormatInfo * Adding platform specific registrations of KeyGestureFormatInfo * Using KeyGesture's new IFormattable interface to use platform specific formatting. * Documenting changes. * Documenting changes. * Putting back ToPlatformString() so I don't break the API. * Swapping Page Up and Page Down symbols on Apple platforms. * Changing KeyGestureFormatInfo constructor Dictionary parameter to IReadOnlyDictionary along with moving Arrow key and Backspace key overrides to the common dictionary. Only Apple platforms are now overriding Keys explicitly. * Undoing addition to MenuPage that was never intended to be committed. --- .../Avalonia.Android/AndroidPlatform.cs | 1 + src/Avalonia.Base/Input/KeyGesture.cs | 35 +++- .../Input/Platform/KeyGestureFormatInfo.cs | 133 +++++++++++++++ .../Converters/PlatformKeyGestureConverter.cs | 155 +----------------- src/Avalonia.Native/AvaloniaNativePlatform.cs | 11 +- src/Avalonia.X11/X11Platform.cs | 1 + .../Avalonia.Browser/WindowingPlatform.cs | 2 + .../AvaloniaHeadlessPlatform.cs | 4 +- .../LinuxFramebufferPlatform.cs | 4 +- src/Tizen/Avalonia.Tizen/TizenPlatform.cs | 1 + src/Windows/Avalonia.Win32/Win32Platform.cs | 2 + src/iOS/Avalonia.iOS/Platform.cs | 9 +- 12 files changed, 195 insertions(+), 163 deletions(-) create mode 100644 src/Avalonia.Base/Input/Platform/KeyGestureFormatInfo.cs diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index d9f3419850..0d2ae2c1ca 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -88,6 +88,7 @@ namespace Avalonia.Android .Bind().ToSingleton() .Bind().ToConstant(new ChoreographerTimer()) .Bind().ToSingleton() + .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { })) .Bind().ToConstant(new AndroidActivatableLifetime()); var graphics = InitializeGraphics(Options); diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs index 9ee8ae9711..122176a127 100644 --- a/src/Avalonia.Base/Input/KeyGesture.cs +++ b/src/Avalonia.Base/Input/KeyGesture.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Text; +using Avalonia.Input.Platform; using Avalonia.Utilities; namespace Avalonia.Input @@ -9,7 +10,7 @@ namespace Avalonia.Input /// /// Defines a keyboard input combination. /// - public sealed class KeyGesture : IEquatable + public sealed class KeyGesture : IEquatable, IFormattable { private static readonly Dictionary s_keySynonyms = new Dictionary { @@ -95,8 +96,28 @@ namespace Avalonia.Input return new KeyGesture(key, keyModifiers); } - public override string ToString() + public override string ToString() => ToString(null, null); + + /// + /// Returns the current KeyGesture as a string formatted according to the format string and appropriate IFormatProvider + /// + /// The format to use. + /// + /// null or "" or "g"The Invariant format, uses Enum.ToString() to format Keys. + /// "p"Use platform specific formatting as registerd. + /// + /// The IFormatProvider to use. If null, uses the appropriate provider registered in the Avalonia Locator, or Invariant. + /// The formatted string. + /// Thrown if the format string is not null, "", "g", or "p" + public string ToString(string? format, IFormatProvider? formatProvider) { + var formatInfo = format switch + { + null or "" or "g" => KeyGestureFormatInfo.Invariant, + "p" => KeyGestureFormatInfo.GetInstance(formatProvider), + _ => throw new FormatException("Unknown format specifier") + }; + var s = StringBuilderCache.Acquire(); static void Plus(StringBuilder s) @@ -109,29 +130,29 @@ namespace Avalonia.Input if (KeyModifiers.HasAllFlags(KeyModifiers.Control)) { - s.Append("Ctrl"); + s.Append(formatInfo.Ctrl); } if (KeyModifiers.HasAllFlags(KeyModifiers.Shift)) { Plus(s); - s.Append("Shift"); + s.Append(formatInfo.Shift); } if (KeyModifiers.HasAllFlags(KeyModifiers.Alt)) { Plus(s); - s.Append("Alt"); + s.Append(formatInfo.Alt); } if (KeyModifiers.HasAllFlags(KeyModifiers.Meta)) { Plus(s); - s.Append("Cmd"); + s.Append(formatInfo.Meta); } Plus(s); - s.Append(Key); + s.Append(formatInfo.FormatKey(Key)); return StringBuilderCache.GetStringAndRelease(s); } diff --git a/src/Avalonia.Base/Input/Platform/KeyGestureFormatInfo.cs b/src/Avalonia.Base/Input/Platform/KeyGestureFormatInfo.cs new file mode 100644 index 0000000000..9eb95bf3f8 --- /dev/null +++ b/src/Avalonia.Base/Input/Platform/KeyGestureFormatInfo.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Input.Platform +{ + + /// + /// Provides platform specific formatting information for the KeyGesture class + /// + /// A dictionary of Key to String overrides for specific characters, for example Key.Left to "Left Arrow" or "←" on Mac. + /// A null value is assumed to be the Invariant, so the included set of common overrides will be skipped if this is null. If only the common overrides are + /// desired, pass an empty Dictionary instead. + /// The string to use for the Meta modifier, defaults to "Cmd" + /// The string to use for the Ctrl modifier, defaults to "Ctrl" + /// The string to use for the Alt modifier, defaults to "Alt" + /// The string to use for the Shift modifier, defaults to "Shift" + public sealed class KeyGestureFormatInfo(IReadOnlyDictionary? platformKeyOverrides = null, + string meta = "Cmd", + string ctrl = "Ctrl", + string alt = "Alt", + string shift = "Shift") : IFormatProvider + { + /// + /// The Invariant format. Only uses strings straight from the appropriate Enums. + /// + public static KeyGestureFormatInfo Invariant { get; } = new(); + + /// + /// The string used to represent Meta on the appropriate platform. Defaults to "Cmd". + /// + public string Meta { get; } = meta; + + /// + /// The string used to represent Ctrl on the appropriate platform. Defaults to "Ctrl". + /// + public string Ctrl { get; } = ctrl; + + /// + /// The string used to represent Alt on the appropriate platform. Defaults to "Alt". + /// + public string Alt { get; } = alt; + + /// + /// The string used to represent Shift on the appropriate platform. Defaults to "Shift". + /// + public string Shift { get; } = shift; + + public object? GetFormat(Type? formatType) => formatType == typeof(KeyGestureFormatInfo) ? this : null; + + /// + /// Gets the most appropriate KeyGestureFormatInfo for the IFormatProvider requested. This will be, in order: + /// 1. The provided IFormatProvider as a KeyGestureFormatInfo + /// 2. The currently registered platform specific KeyGestureFormatInfo, if present. + /// 3. The Invariant otherwise. + /// + /// The IFormatProvider to get a KeyGestureFormatInfo for. + /// + public static KeyGestureFormatInfo GetInstance(IFormatProvider? formatProvider) + => formatProvider?.GetFormat(typeof(KeyGestureFormatInfo)) as KeyGestureFormatInfo + ?? AvaloniaLocator.Current.GetService() + ?? Invariant; + + /// + /// A dictionary of the common platform Key overrides. These are used as a fallback + /// if platformKeyOverrides doesn't contain the Key in question. + /// + + private static readonly Dictionary s_commonKeyOverrides = new() + { + { Key.Add , "+" }, + { 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.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.Separator , "/" }, + { Key.Subtract , "-" }, + { Key.Back , "Backspace" }, + { Key.Down , "Down Arrow" }, + { Key.Left , "Left Arrow" }, + { Key.Right , "Right Arrow" }, + { Key.Up , "Up Arrow" } + }; + + /// + /// Checks the platformKeyOverrides and s_commonKeyOverrides Dictionaries, in order, for the appropriate + /// string to represent the given Key on this platform. + /// NOTE: If platformKeyOverrides is null, this is assumed to be the Invariant and the Dictionaries are not checked. + /// The plain Enum string is returned instead. + /// + /// The Key to format. + /// The appropriate platform specific or common override if present, key.ToString() if not or this is the Invariant. + public string FormatKey(Key key) + { + /* + * The absence of an Overrides dictionary indicates this is the invariant, and + * so should just return the default ToString() value. + */ + if (platformKeyOverrides == null) + return key.ToString(); + + return platformKeyOverrides.TryGetValue(key, out string? result) ? result : + s_commonKeyOverrides.TryGetValue(key, out string? cresult) ? cresult : + key.ToString() ; + + } + + + } +} diff --git a/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs b/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs index 0fa43809ac..03b3706588 100644 --- a/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs +++ b/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls.Converters } else if (value is KeyGesture gesture && targetType == typeof(string)) { - return ToPlatformString(gesture); + return gesture.ToString("p", null); } else { @@ -41,156 +41,7 @@ namespace Avalonia.Controls.Converters /// /// 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 = StringBuilderCache.Acquire(); - - static void Plus(StringBuilder s) - { - if (s.Length > 0) - { - s.Append("+"); - } - } - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Control)) - { - s.Append("Ctrl"); - } - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Shift)) - { - Plus(s); - s.Append("Shift"); - } - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Alt)) - { - Plus(s); - s.Append("Alt"); - } - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Meta)) - { - Plus(s); - s.Append(meta); - } - - Plus(s); - s.Append(ToString(gesture.Key)); - - return StringBuilderCache.GetStringAndRelease(s); - } - - private static string ToOSXString(KeyGesture gesture) - { - var s = StringBuilderCache.Acquire(); - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Control)) - { - s.Append('⌃'); - } - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Alt)) - { - s.Append('⌥'); - } - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Shift)) - { - s.Append('⇧'); - } - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Meta)) - { - s.Append('⌘'); - } - - s.Append(ToOSXString(gesture.Key)); - - return StringBuilderCache.GetStringAndRelease(s); - } - - 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), - }; - } + public static string ToPlatformString(KeyGesture gesture) => gesture.ToString("p", null); + } } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index d8ab8363b5..41cc6edd36 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using Avalonia.Compatibility; using Avalonia.Controls.ApplicationLifetimes; @@ -124,6 +125,14 @@ namespace Avalonia.Native AvaloniaLocator.CurrentMutable.Bind().ToConstant(hotkeys); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() + { + { 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 , "↑" } + }, ctrl: "⌃", meta: "⌘", shift: "⇧", alt: "⌥")); + foreach (var mode in _options.RenderingMode) { if (mode == AvaloniaNativeRenderingMode.OpenGl) diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index e89a508820..26046402a1 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -81,6 +81,7 @@ namespace Avalonia.X11 .Bind().ToConstant(new X11PlatformThreading(this)) .Bind().ToConstant(timer) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) + .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { }, meta: "Super")) .Bind().ToFunc(() => KeyboardDevice) .Bind().ToConstant(new X11CursorFactory(Display)) .Bind().ToConstant(new X11Clipboard(this)) diff --git a/src/Browser/Avalonia.Browser/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs index e344db3d73..bcc0a4a801 100644 --- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs +++ b/src/Browser/Avalonia.Browser/WindowingPlatform.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reflection; using System.Threading; using Avalonia.Browser.Interop; @@ -70,6 +71,7 @@ internal class BrowserWindowingPlatform : IWindowingPlatform .Bind().ToConstant(instance) .Bind().ToSingleton() .Bind().ToSingleton() + .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { })) .Bind().ToSingleton(); AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 9bb587c9b1..0dbb9e845e 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -8,6 +8,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.Threading; +using System.Collections.Generic; namespace Avalonia.Headless { @@ -76,7 +77,8 @@ namespace Avalonia.Headless .Bind().ToConstant(new KeyboardDevice()) .Bind().ToConstant(new RenderTimer(60)) .Bind().ToConstant(new HeadlessWindowingPlatform(opts.FrameBufferFormat)) - .Bind().ToSingleton(); + .Bind().ToSingleton() + .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { })); Compositor = new Compositor( null); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index d226a89e7d..2bddc4ce29 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -72,7 +73,8 @@ namespace Avalonia.LinuxFramebuffer .Bind().ToConstant(new KeyboardDevice()) .Bind().ToSingleton() .Bind().ToSingleton() - .Bind().ToSingleton(); + .Bind().ToSingleton() + .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { }, meta: "Super")); Compositor = new Compositor(AvaloniaLocator.Current.GetService()); } diff --git a/src/Tizen/Avalonia.Tizen/TizenPlatform.cs b/src/Tizen/Avalonia.Tizen/TizenPlatform.cs index c3824a04ad..ff083389e3 100644 --- a/src/Tizen/Avalonia.Tizen/TizenPlatform.cs +++ b/src/Tizen/Avalonia.Tizen/TizenPlatform.cs @@ -40,6 +40,7 @@ internal class TizenPlatform .Bind().ToSingleton() .Bind().ToConstant(new TizenRenderTimer()) .Bind().ToSingleton() + .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { })) .Bind().ToConstant(GlPlatform = new NuiGlPlatform()); Compositor = new Compositor(AvaloniaLocator.Current.GetService()); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index d923f38275..85add81344 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -18,6 +18,7 @@ using Avalonia.Utilities; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; +using System.Collections.Generic; namespace Avalonia { @@ -101,6 +102,7 @@ namespace Avalonia.Win32 new KeyGesture(Key.F10, KeyModifiers.Shift) } }) + .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { }, meta: "Win")) .Bind().ToConstant(s_instance) .Bind().ToConstant(NonPumpingWaitHelperImpl.Instance) .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()) diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 8006639406..e2ce25010f 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Avalonia.Controls.ApplicationLifetimes; @@ -83,6 +83,13 @@ namespace Avalonia.iOS .Bind().ToSingleton() .Bind().ToConstant(new PlatformIconLoaderStub()) .Bind().ToSingleton() + .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() + { + { 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 , "↑" } + }, ctrl: "⌃", meta: "⌘", shift: "⇧", alt: "⌥")) .Bind().ToConstant(Timer) .Bind().ToConstant(DispatcherImpl.Instance) .Bind().ToConstant(keyboard);