diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Command/KeyGesture.cs b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Command/KeyGesture.cs index f3a4bf70072..b90f78d2658 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Command/KeyGesture.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Command/KeyGesture.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -287,9 +287,9 @@ internal static KeyGesture CreateFromResourceStrings(string keyGestureToken, str { // combine the gesture and the display string, producing a string // that the type converter will recognize - if (!String.IsNullOrEmpty(keyDisplayString)) + if (!string.IsNullOrEmpty(keyDisplayString)) { - keyGestureToken += KeyGestureConverter.DISPLAYSTRING_SEPARATOR + keyDisplayString; + keyGestureToken += DisplayStringSeparator + keyDisplayString; } return _keyGestureConverter.ConvertFromInvariantString(keyGestureToken) as KeyGesture; @@ -307,6 +307,7 @@ internal static KeyGesture CreateFromResourceStrings(string keyGestureToken, str private Key _key = Key.None; private string _displayString; private const char MULTIPLEGESTURE_DELIMITER = ';'; + private const char DisplayStringSeparator = ','; private static TypeConverter _keyGestureConverter = new KeyGestureConverter(); //private static bool _classRegistered = false; #endregion Private Fields diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Command/KeyGestureConverter.cs b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Command/KeyGestureConverter.cs index a092fbb637a..99124293916 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Command/KeyGestureConverter.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Command/KeyGestureConverter.cs @@ -1,187 +1,166 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -// -// -// -// Description: KeyGestureConverter - Converts a KeyGesture string -// to the *Type* that the string represents -// + using System.ComponentModel; // for TypeConverter using System.Globalization; // for CultureInfo namespace System.Windows.Input { /// - /// KeyGesture - Converter class for converting between a string and the Type of a KeyGesture + /// Converter class for converting between a and . /// public class KeyGestureConverter : TypeConverter { - private const char MODIFIERS_DELIMITER = '+' ; - internal const char DISPLAYSTRING_SEPARATOR = ',' ; - - /// - ///CanConvertFrom() - /// - ///ITypeDescriptorContext - ///type to convert from - ///true if the given type can be converted, false otherwise + /// + /// To aid with conversion from to . + /// + private static readonly KeyConverter s_keyConverter = new(); + + /// + /// Returns whether or not this class can convert from a given . + /// + /// The for this call. + /// The being queried for support. + /// + /// if the given can be converted, otherwise. + /// public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { // We can only handle string. - if (sourceType == typeof(string)) - { - return true; - } - else - { - return false; - } + return sourceType == typeof(string); } - - /// - ///TypeConverter method override. - /// - ///ITypeDescriptorContext - ///Type to convert to - ///true if conversion is possible + /// + /// Returns whether or not this class can convert to a given . + /// + /// The for this call. + /// The being queried for support. + /// + /// if this class can convert to , otherwise. + /// public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { // We can convert to an InstanceDescriptor or to a string. - if (destinationType == typeof(string)) - { - // When invoked by the serialization engine we can convert to string only for known type - if (context != null && context.Instance != null) - { - KeyGesture keyGesture = context.Instance as KeyGesture; - if (keyGesture != null) - { - return (ModifierKeysConverter.IsDefinedModifierKeys(keyGesture.Modifiers) - && IsDefinedKey(keyGesture.Key)); - } - } - } - return false; + if (destinationType != typeof(string)) + return false; + + // When invoked by the serialization engine we can convert to string only for known type + if (context?.Instance is not KeyGesture keyGesture) + return false; + + return ModifierKeysConverter.IsDefinedModifierKeys(keyGesture.Modifiers) && IsDefinedKey(keyGesture.Key); } /// - /// ConvertFrom() + /// Converts of type to its represensation. /// - /// - /// - /// - /// + /// This parameter is ignored during the call. + /// This parameter is ignored during the call. + /// The object to convert to a . + /// + /// A new instance of class representing the data contained in . + /// + /// Thrown in case the was not a . public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object source) { - if (source != null && source is string) + if (source is not string sourceString) + throw GetConvertFromException(source); + + ReadOnlySpan trimmedSource = sourceString.AsSpan().Trim(); + if (trimmedSource.IsEmpty) + return new KeyGesture(Key.None); + + ReadOnlySpan keyToken; + ReadOnlySpan modifiersToken; + string displayString; + + // Break apart display string + int index = trimmedSource.IndexOf(','); + if (index >= 0) { - string fullName = ((string)source).Trim(); - if (fullName.Length == 0) - return new KeyGesture(Key.None); - - string keyToken; - string modifiersToken; - string displayString; - - // break apart display string - int index = fullName.IndexOf(DISPLAYSTRING_SEPARATOR); - if (index >= 0) - { - displayString = fullName.Substring(index + 1).Trim(); - fullName = fullName.Substring(0, index).Trim(); - } - else - { - displayString = String.Empty; - } - - // break apart key and modifiers - index = fullName.LastIndexOf(MODIFIERS_DELIMITER); - if (index >= 0) - { // modifiers exists - modifiersToken = fullName.Substring(0, index); - keyToken = fullName.Substring(index + 1); - } - else - { - modifiersToken = String.Empty; - keyToken = fullName; - } - - ModifierKeys modifiers = ModifierKeys.None; - object resultkey = keyConverter.ConvertFrom(context, culture, keyToken); - if (resultkey != null) - { - object temp = modifierKeysConverter.ConvertFrom(context, culture, modifiersToken); - if (temp != null) - { - modifiers = (ModifierKeys)temp; - } - return new KeyGesture((Key)resultkey, modifiers, displayString); - } + displayString = trimmedSource.Slice(index + 1).Trim().ToString(); + trimmedSource = trimmedSource.Slice(0, index).Trim(); } - throw GetConvertFromException(source); + else + { + displayString = string.Empty; + } + + // Break apart key and modifiers + index = trimmedSource.LastIndexOf('+'); + if (index >= 0) + { + modifiersToken = trimmedSource.Slice(0, index); + keyToken = trimmedSource.Slice(index + 1).Trim(); + } + else + { + modifiersToken = ReadOnlySpan.Empty; + keyToken = trimmedSource; + } + + return new KeyGesture(KeyConverter.GetKeyFromString(keyToken), ModifierKeysConverter.ConvertFromImpl(modifiersToken), displayString); } /// - /// ConvertTo() + /// Attempt to convert a class to the . /// - /// - /// - /// - /// - /// + /// This parameter is ignored during the call. + /// This parameter is ignored during the call. + /// The object to convert to a . + /// The to convert to. + /// + /// The formatted to its representation. + /// + /// Thrown in case was . + /// + /// Thrown in case the was not a + /// or was not a . + /// public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { ArgumentNullException.ThrowIfNull(destinationType); - if (destinationType == typeof(string)) - { - if (value != null) - { - KeyGesture keyGesture = value as KeyGesture; - if (keyGesture != null) - { - if (keyGesture.Key == Key.None) - return String.Empty; - - string strBinding = "" ; - string strKey = (string)keyConverter.ConvertTo(context, culture, keyGesture.Key, destinationType) as string; - if (strKey != String.Empty) - { - strBinding += modifierKeysConverter.ConvertTo(context, culture, keyGesture.Modifiers, destinationType) as string; - if (strBinding != String.Empty) - { - strBinding += MODIFIERS_DELIMITER; - } - strBinding += strKey; - - if (!String.IsNullOrEmpty(keyGesture.DisplayString)) - { - strBinding += DISPLAYSTRING_SEPARATOR + keyGesture.DisplayString; - } - } - return strBinding; - } - } - else - { - return String.Empty; - } - } - throw GetConvertToException(value,destinationType); + if (destinationType != typeof(string)) + throw GetConvertToException(value, destinationType); + + // Following checks are here to match the previous behavior + if (value is null) + return string.Empty; + + if (value is not KeyGesture keyGesture) + throw GetConvertToException(value, destinationType); + + // If the key is None, nothing else matters + if (keyGesture.Key is Key.None) + return string.Empty; + + // You will only get string.Empty from KeyConverter for Key.None and we've checked that above + string strKey = (string)s_keyConverter.ConvertTo(context, culture, keyGesture.Key, destinationType); + + // No modifiers, just binding (possibly with with display string) -> "F5,Refresh" + if (keyGesture.Modifiers is ModifierKeys.None) + return string.IsNullOrEmpty(keyGesture.DisplayString) ? strKey : $"{strKey},{keyGesture.DisplayString}"; + + ReadOnlySpan modifierSpan = ModifierKeysConverter.ConvertMultipleModifiers(keyGesture.Modifiers, stackalloc char[22]); + + // Append display string if there's any, like "Ctrl+A,Description" + if (!string.IsNullOrEmpty(keyGesture.DisplayString)) + return string.Create(CultureInfo.InvariantCulture, stackalloc char[168], $"{modifierSpan}+{strKey},{keyGesture.DisplayString}"); + + // We just put together modifiers and key, like "Ctrl+A" + return string.Create(CultureInfo.InvariantCulture, stackalloc char[50], $"{modifierSpan}+{strKey}"); } - // Check for Valid enum, as any int can be casted to the enum. + /// + /// Helper function similar to , just lighter and faster. + /// + /// The value to test against. + /// if falls in enumeration range, otherwise. internal static bool IsDefinedKey(Key key) { - return (key >= Key.None && key <= Key.OemClear); + return key >= Key.None && key <= Key.OemClear; } - - private static KeyConverter keyConverter = new KeyConverter(); - private static ModifierKeysConverter modifierKeysConverter = new ModifierKeysConverter(); } } diff --git a/src/Microsoft.DotNet.Wpf/src/WindowsBase/System/Windows/Input/KeyConverter.cs b/src/Microsoft.DotNet.Wpf/src/WindowsBase/System/Windows/Input/KeyConverter.cs index 1deae00b596..c26971f9607 100644 --- a/src/Microsoft.DotNet.Wpf/src/WindowsBase/System/Windows/Input/KeyConverter.cs +++ b/src/Microsoft.DotNet.Wpf/src/WindowsBase/System/Windows/Input/KeyConverter.cs @@ -8,12 +8,12 @@ namespace System.Windows.Input { /// - /// Converter class for converting between a and . + /// Converter class for converting between a and . /// public class KeyConverter : TypeConverter { /// - /// Used to check whether we can convert a into a . + /// Used to check whether we can convert a into a . /// ///ITypeDescriptorContext ///type to convert from @@ -25,11 +25,11 @@ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceT } /// - /// Used to check whether we can convert specified value to . + /// Used to check whether we can convert specified value to . /// /// ITypeDescriptorContext /// Type to convert to - /// if conversion to is possible, otherwise. + /// if conversion to is possible, otherwise. public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { // We can convert to a string @@ -44,12 +44,12 @@ public override bool CanConvertTo(ITypeDescriptorContext context, Type destinati } /// - /// Converts of type to its representation. + /// Converts of type to its representation. /// /// Parser Context /// Culture Info /// Key String - /// A representing the specified by . + /// A representing the specified by . public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object source) { if (source is not string stringSource) @@ -60,13 +60,13 @@ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo c } /// - /// Converts a of type to its representation. + /// Converts a of type to its representation. /// /// Serialization Context /// Culture Info /// Key value /// Type to Convert - /// A representing the specified by . + /// A representing the specified by . public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { ArgumentNullException.ThrowIfNull(destinationType); @@ -100,8 +100,10 @@ _ when IsDefinedKey(key) => key.ToString(), /// Helper function that performs the conversion of to the enum. /// /// The string to convert from. - /// A value corresponding to the specified string, if was empty. - private static Key GetKeyFromString(ReadOnlySpan keyToken) + /// + /// A value corresponding to the specified string, if was empty. + /// + internal static Key GetKeyFromString(ReadOnlySpan keyToken) { // If the token is empty, we presume "None" as our value but it is a success if (keyToken.IsEmpty) diff --git a/src/Microsoft.DotNet.Wpf/src/WindowsBase/System/Windows/Input/ModifierKeysConverter.cs b/src/Microsoft.DotNet.Wpf/src/WindowsBase/System/Windows/Input/ModifierKeysConverter.cs index 3d2789703f0..28b18c0ed01 100644 --- a/src/Microsoft.DotNet.Wpf/src/WindowsBase/System/Windows/Input/ModifierKeysConverter.cs +++ b/src/Microsoft.DotNet.Wpf/src/WindowsBase/System/Windows/Input/ModifierKeysConverter.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -58,6 +58,18 @@ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo c ReadOnlySpan modifiersToken = stringSource.AsSpan().Trim(); + return ConvertFromImpl(modifiersToken); + } + + /// + /// Converts to its represensation. + /// + /// + /// is expected to have separated with '+', with any amount of whitespace. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ModifierKeys ConvertFromImpl(ReadOnlySpan modifiersToken) + { // Empty token means there were no modifiers, exit early if (modifiersToken.IsEmpty) return ModifierKeys.None; @@ -123,10 +135,32 @@ public override object ConvertTo(ITypeDescriptorContext context, CultureInfo cul }; } - private static string ConvertMultipleModifiers(ModifierKeys modifiers) + /// + /// This is a proxy function for + /// so we do not initialize a stack-allocated buffer when we do not need to. + /// + /// The modifiers bits to format. + /// Formatted in [modifier_1]+[modifier_2] format. + [MethodImpl(MethodImplOptions.NoInlining)] + internal static string ConvertMultipleModifiers(ModifierKeys modifiers) + { + return new string(ConvertMultipleModifiers(modifiers, stackalloc char[22])); + } + + /// + /// Format multiple into -provided buffer. + /// + /// The modifiers bits to format. + /// The buffer to write formatted modifiers into. + /// Formatted in [modifier_1]+[modifier_2] format. + /// + /// Make sure the is at least 22 chars long, otherwise conversion might fail. + /// + internal static ReadOnlySpan ConvertMultipleModifiers(ModifierKeys modifiers, Span modifierSpan) { // Ctrl+Alt+Windows+Shift is the maximum char length, though the composition of such value is improbable - Span modifierSpan = stackalloc char[22]; + Debug.Assert(modifierSpan.Length > 21); + int totalLength = 0; if (modifiers.HasFlag(ModifierKeys.Control)) @@ -141,14 +175,14 @@ private static string ConvertMultipleModifiers(ModifierKeys modifiers) if (modifiers.HasFlag(ModifierKeys.Shift)) AppendWithDelimiter("Shift", ref totalLength, ref modifierSpan); - //Helper function to concatenate modifiers + // Helper function to concatenate modifiers [MethodImpl(MethodImplOptions.AggressiveInlining)] static void AppendWithDelimiter(string literal, ref int totalLength, ref Span modifierSpan) { // If this is not the first modifier in the span, we prepend a delimiter (e.g. Ctrl -> Ctrl+Alt) if (totalLength > 0) { - "+".CopyTo(modifierSpan.Slice(totalLength)); + modifierSpan[totalLength] = '+'; totalLength++; } @@ -156,7 +190,7 @@ static void AppendWithDelimiter(string literal, ref int totalLength, ref Span