Browse Source

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.
pull/15907/head
IanRawley 2 years ago
committed by GitHub
parent
commit
2372590300
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      src/Android/Avalonia.Android/AndroidPlatform.cs
  2. 35
      src/Avalonia.Base/Input/KeyGesture.cs
  3. 133
      src/Avalonia.Base/Input/Platform/KeyGestureFormatInfo.cs
  4. 155
      src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs
  5. 11
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  6. 1
      src/Avalonia.X11/X11Platform.cs
  7. 2
      src/Browser/Avalonia.Browser/WindowingPlatform.cs
  8. 4
      src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  9. 4
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  10. 1
      src/Tizen/Avalonia.Tizen/TizenPlatform.cs
  11. 2
      src/Windows/Avalonia.Win32/Win32Platform.cs
  12. 9
      src/iOS/Avalonia.iOS/Platform.cs

1
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -88,6 +88,7 @@ namespace Avalonia.Android
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }))
.Bind<IActivatableLifetime>().ToConstant(new AndroidActivatableLifetime());
var graphics = InitializeGraphics(Options);

35
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
/// <summary>
/// Defines a keyboard input combination.
/// </summary>
public sealed class KeyGesture : IEquatable<KeyGesture>
public sealed class KeyGesture : IEquatable<KeyGesture>, IFormattable
{
private static readonly Dictionary<string, Key> s_keySynonyms = new Dictionary<string, Key>
{
@ -95,8 +96,28 @@ namespace Avalonia.Input
return new KeyGesture(key, keyModifiers);
}
public override string ToString()
public override string ToString() => ToString(null, null);
/// <summary>
/// Returns the current KeyGesture as a string formatted according to the format string and appropriate IFormatProvider
/// </summary>
/// <param name="format">The format to use.
/// <list type="bullet">
/// <item><term>null or "" or "g"</term><description>The Invariant format, uses Enum.ToString() to format Keys.</description></item>
/// <item><term>"p"</term><description>Use platform specific formatting as registerd.</description></item>
/// </list></param>
/// <param name="formatProvider">The IFormatProvider to use. If null, uses the appropriate provider registered in the Avalonia Locator, or Invariant.</param>
/// <returns>The formatted string.</returns>
/// <exception cref="FormatException">Thrown if the format string is not null, "", "g", or "p"</exception>
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);
}

133
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
{
/// <summary>
/// Provides platform specific formatting information for the KeyGesture class
/// </summary>
/// <param name="platformKeyOverrides">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.</param>
/// <param name="meta">The string to use for the Meta modifier, defaults to "Cmd"</param>
/// <param name="ctrl">The string to use for the Ctrl modifier, defaults to "Ctrl"</param>
/// <param name="alt">The string to use for the Alt modifier, defaults to "Alt"</param>
/// <param name="shift">The string to use for the Shift modifier, defaults to "Shift"</param>
public sealed class KeyGestureFormatInfo(IReadOnlyDictionary<Key, string>? platformKeyOverrides = null,
string meta = "Cmd",
string ctrl = "Ctrl",
string alt = "Alt",
string shift = "Shift") : IFormatProvider
{
/// <summary>
/// The Invariant format. Only uses strings straight from the appropriate Enums.
/// </summary>
public static KeyGestureFormatInfo Invariant { get; } = new();
/// <summary>
/// The string used to represent Meta on the appropriate platform. Defaults to "Cmd".
/// </summary>
public string Meta { get; } = meta;
/// <summary>
/// The string used to represent Ctrl on the appropriate platform. Defaults to "Ctrl".
/// </summary>
public string Ctrl { get; } = ctrl;
/// <summary>
/// The string used to represent Alt on the appropriate platform. Defaults to "Alt".
/// </summary>
public string Alt { get; } = alt;
/// <summary>
/// The string used to represent Shift on the appropriate platform. Defaults to "Shift".
/// </summary>
public string Shift { get; } = shift;
public object? GetFormat(Type? formatType) => formatType == typeof(KeyGestureFormatInfo) ? this : null;
/// <summary>
/// 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.
/// </summary>
/// <param name="formatProvider">The IFormatProvider to get a KeyGestureFormatInfo for.</param>
/// <returns></returns>
public static KeyGestureFormatInfo GetInstance(IFormatProvider? formatProvider)
=> formatProvider?.GetFormat(typeof(KeyGestureFormatInfo)) as KeyGestureFormatInfo
?? AvaloniaLocator.Current.GetService<KeyGestureFormatInfo>()
?? Invariant;
/// <summary>
/// A dictionary of the common platform Key overrides. These are used as a fallback
/// if platformKeyOverrides doesn't contain the Key in question.
/// </summary>
private static readonly Dictionary<Key, string> 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" }
};
/// <summary>
/// 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.
/// </summary>
/// <param name="key">The Key to format.</param>
/// <returns>The appropriate platform specific or common override if present, key.ToString() if not or this is the Invariant.</returns>
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() ;
}
}
}

155
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
/// </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 = 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);
}
}

11
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<PlatformHotkeyConfiguration>().ToConstant(hotkeys);
AvaloniaLocator.CurrentMutable.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>()
{
{ 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)

1
src/Avalonia.X11/X11Platform.cs

@ -81,6 +81,7 @@ namespace Avalonia.X11
.Bind<IDispatcherImpl>().ToConstant(new X11PlatformThreading(this))
.Bind<IRenderTimer>().ToConstant(timer)
.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control))
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }, meta: "Super"))
.Bind<IKeyboardDevice>().ToFunc(() => KeyboardDevice)
.Bind<ICursorFactory>().ToConstant(new X11CursorFactory(Display))
.Bind<IClipboard>().ToConstant(new X11Clipboard(this))

2
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<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }))
.Bind<IActivatableLifetime>().ToSingleton<BrowserActivatableLifetime>();
AvaloniaLocator.CurrentMutable.Bind<IDispatcherImpl>().ToSingleton<BrowserDispatcherImpl>();

4
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<IKeyboardDevice>().ToConstant(new KeyboardDevice())
.Bind<IRenderTimer>().ToConstant(new RenderTimer(60))
.Bind<IWindowingPlatform>().ToConstant(new HeadlessWindowingPlatform(opts.FrameBufferFormat))
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }));
Compositor = new Compositor( null);
}

4
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<IKeyboardDevice>().ToConstant(new KeyboardDevice())
.Bind<IPlatformIconLoader>().ToSingleton<LinuxFramebufferIconLoaderStub>()
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }, meta: "Super"));
Compositor = new Compositor(AvaloniaLocator.Current.GetService<IPlatformGraphics>());
}

1
src/Tizen/Avalonia.Tizen/TizenPlatform.cs

@ -40,6 +40,7 @@ internal class TizenPlatform
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new TizenRenderTimer())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }))
.Bind<IPlatformGraphics>().ToConstant(GlPlatform = new NuiGlPlatform());
Compositor = new Compositor(AvaloniaLocator.Current.GetService<IPlatformGraphics>());

2
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<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }, meta: "Win"))
.Bind<IPlatformIconLoader>().ToConstant(s_instance)
.Bind<NonPumpingLockHelper.IHelperImpl>().ToConstant(NonPumpingWaitHelperImpl.Instance)
.Bind<IMountedVolumeInfoProvider>().ToConstant(new WindowsMountedVolumeInfoProvider())

9
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<IPlatformSettings>().ToSingleton<PlatformSettings>()
.Bind<IPlatformIconLoader>().ToConstant(new PlatformIconLoaderStub())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>()
{
{ 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<IRenderTimer>().ToConstant(Timer)
.Bind<IDispatcherImpl>().ToConstant(DispatcherImpl.Instance)
.Bind<IKeyboardDevice>().ToConstant(keyboard);

Loading…
Cancel
Save