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

Implementing IFormattable on KeyGesture #15828

Merged
merged 15 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Android/Avalonia.Android/AndroidPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public static void Initialize()
.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);
Expand Down
35 changes: 28 additions & 7 deletions src/Avalonia.Base/Input/KeyGesture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Avalonia.Input.Platform;
using Avalonia.Utilities;

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>
{
Expand Down Expand Up @@ -95,8 +96,28 @@ public static KeyGesture Parse(string gesture)
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")
};
Comment on lines +112 to +119
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably should consider using CultureInfo passed as IFormatProvider to be used for localization here. And pass it down from IValueConverter. But that's a bigger issue for another day.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, I imagine "Up Arrow" is not going to be universally understood by every language on Earth. I'll take a look into how that sort of thing is done, but as you say it's a bigger problem for another day. I'll just take steps to try and make sure I'm not making that other day any harder than it needs to be.


var s = StringBuilderCache.Acquire();

static void Plus(StringBuilder s)
Expand All @@ -109,29 +130,29 @@ static void Plus(StringBuilder s)

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);
}
Expand Down
133 changes: 133 additions & 0 deletions src/Avalonia.Base/Input/Platform/KeyGestureFormatInfo.cs
Original file line number Diff line number Diff line change
@@ -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
MrJul marked this conversation as resolved.
Show resolved Hide resolved
* so should just return the default ToString() value.
*/
if (platformKeyOverrides == null)
return key.ToString();

return platformKeyOverrides.TryGetValue(key, out string? result) ? result :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can be simplified to platformKeyOverrides.TryGetValue(key, out var result) || commonKeyOverrides.TryGetValue(key, out result) ? result : key.ToString(), avoiding the nested ternary operators.

s_commonKeyOverrides.TryGetValue(key, out string? cresult) ? cresult :
key.ToString() ;

}


}
}
155 changes: 3 additions & 152 deletions src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class PlatformKeyGestureConverter : IValueConverter
}
else if (value is KeyGesture gesture && targetType == typeof(string))
{
return ToPlatformString(gesture);
return gesture.ToString("p", null);
}
else
{
Expand All @@ -41,156 +41,7 @@ public class PlatformKeyGestureConverter : IValueConverter
/// </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 => "⇟",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did think that looked weird, and doing a bit of searching have found other references to the same. I'll fix those up.

Key.Right => "→",
Key.Space => "␣",
Key.Tab => "⇥",
Key.Up => "↑",
_ => ToString(key),
};
}
public static string ToPlatformString(KeyGesture gesture) => gesture.ToString("p", null);

}
}
Loading
Loading