diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs index b2ab8c06ae..e8808ff285 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs @@ -12,6 +12,7 @@ using OmniXaml.Builder; using OmniXaml.TypeConversion; using OmniXaml.Typing; using Perspex.Controls; +using Perspex.Input; using Perspex.Markup.Xaml.Templates; using Perspex.Markup.Xaml.Converters; using Perspex.Markup.Xaml.DataBinding; @@ -88,6 +89,7 @@ namespace Perspex.Markup.Xaml.Context new TypeConverterRegistration(typeof(Thickness), new ThicknessTypeConverter()), new TypeConverterRegistration(typeof(Selector), new SelectorTypeConverter()), new TypeConverterRegistration(typeof(TimeSpan), new TimeSpanTypeConverter()), + new TypeConverterRegistration(typeof(KeyGesture), new KeyGestureConverter()) }; typeConverterProvider.AddAll(converters); diff --git a/src/Markup/Perspex.Markup.Xaml/Converters/KeyGestureConverter.cs b/src/Markup/Perspex.Markup.Xaml/Converters/KeyGestureConverter.cs new file mode 100644 index 0000000000..ebf1159d76 --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Converters/KeyGestureConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; +using OmniXaml.TypeConversion; +using Perspex.Input; + +namespace Perspex.Markup.Xaml.Converters +{ + class KeyGestureConverter : ITypeConverter + { + public bool CanConvertFrom(IXamlTypeConverterContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public bool CanConvertTo(IXamlTypeConverterContext context, Type destinationType) + { + return false; + } + + public object ConvertFrom(IXamlTypeConverterContext context, CultureInfo culture, object value) + { + return KeyGesture.Parse((string)value); + } + + public object ConvertTo(IXamlTypeConverterContext context, CultureInfo culture, object value, Type destinationType) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index 2bce8946ac..3ed9a50f31 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -39,6 +39,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/src/Perspex.Input/IInputElement.cs b/src/Perspex.Input/IInputElement.cs index fa75629f64..d6e5378f02 100644 --- a/src/Perspex.Input/IInputElement.cs +++ b/src/Perspex.Input/IInputElement.cs @@ -3,6 +3,7 @@ using System.Diagnostics.Contracts; using System; +using System.Collections.Generic; using Perspex.Interactivity; namespace Perspex.Input @@ -119,5 +120,8 @@ namespace Perspex.Input /// The position, in control coordinates. /// The at the specified position. IInputElement InputHitTest(Point p); + + + List KeyBindings { get; } } } diff --git a/src/Perspex.Input/InputElement.cs b/src/Perspex.Input/InputElement.cs index 2075bc09f2..1cba2af3c6 100644 --- a/src/Perspex.Input/InputElement.cs +++ b/src/Perspex.Input/InputElement.cs @@ -2,6 +2,7 @@ // 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 Perspex.Interactivity; using Perspex.Rendering; @@ -333,6 +334,8 @@ namespace Perspex.Input set { SetValue(IsEnabledCoreProperty, value); } } + public List KeyBindings { get; } = new List(); + /// /// Returns the input element that can be found within the current control at the specified /// position. diff --git a/src/Perspex.Input/KeyBinding.cs b/src/Perspex.Input/KeyBinding.cs new file mode 100644 index 0000000000..48b0eb3712 --- /dev/null +++ b/src/Perspex.Input/KeyBinding.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace Perspex.Input +{ + public class KeyBinding : PerspexObject + { + public static PerspexProperty CommandProperty = + PerspexProperty.Register("Command"); + + public ICommand Command + { + get { return GetValue(CommandProperty); } + set { SetValue(CommandProperty, value); } + } + + public static PerspexProperty GestureProperty = + PerspexProperty.Register("Gesture"); + + public KeyGesture Gesture + { + get { return GetValue(GestureProperty); } + set { SetValue(GestureProperty, value); } + } + + public void TryHandle(KeyEventArgs args) + { + if (Gesture?.Matches(args) == true) + { + args.Handled = true; + if (Command?.CanExecute(null) == true) + Command.Execute(null); + } + } + } +} diff --git a/src/Perspex.Input/KeyGesture.cs b/src/Perspex.Input/KeyGesture.cs new file mode 100644 index 0000000000..e298d63bcf --- /dev/null +++ b/src/Perspex.Input/KeyGesture.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Perspex.Input +{ + public sealed class KeyGesture : IEquatable + { + public bool Equals(KeyGesture other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Key == other.Key && Modifiers == other.Modifiers; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is KeyGesture && Equals((KeyGesture) obj); + } + + public override int GetHashCode() + { + unchecked + { + return ((int) Key*397) ^ (int) Modifiers; + } + } + + public static bool operator ==(KeyGesture left, KeyGesture right) + { + return Equals(left, right); + } + + public static bool operator !=(KeyGesture left, KeyGesture right) + { + return !Equals(left, right); + } + + public Key Key { get; set; } + + public InputModifiers Modifiers { get; set; } + + + static Dictionary KeySynonims = new Dictionary + { + {"+", Key.OemPlus }, + {"-", Key.OemMinus}, + {".", Key.OemPeriod } + }; + + //TODO: Move that to external key parser + static Key ParseKey(string key) + { + Key rv; + if (KeySynonims.TryGetValue(key.ToLower(), out rv)) + return rv; + return (Key)Enum.Parse(typeof (Key), key, true); + } + + static InputModifiers ParseModifier(string modifier) + { + if (modifier.Equals("ctrl", StringComparison.OrdinalIgnoreCase)) + return InputModifiers.Control; + return (InputModifiers) Enum.Parse(typeof (InputModifiers), modifier, true); + } + + public static KeyGesture Parse(string gesture) + { + //string.Split can't be used here because "Ctrl++" is a perfectly valid key gesture + + var parts = new List(); + + var cstart = 0; + for (var c = 0; c <= gesture.Length; c++) + { + var ch = c == gesture.Length ? '\0' : gesture[c]; + if (c == gesture.Length || (ch == '+' && cstart != c)) + { + parts.Add(gesture.Substring(cstart, c - cstart)); + cstart = c + 1; + } + } + for (var c = 0; c < parts.Count; c++) + parts[c] = parts[c].Trim(); + + var rv = new KeyGesture(); + + for (var c = 0; c < parts.Count; c++) + { + if (c == parts.Count - 1) + rv.Key = ParseKey(parts[c]); + else + rv.Modifiers |= ParseModifier(parts[c]); + } + return rv; + } + + public override string ToString() + { + var parts = new List(); + foreach (var flag in Enum.GetValues(typeof (InputModifiers)).Cast()) + { + if (Modifiers.HasFlag(flag) && flag != InputModifiers.None) + parts.Add(flag.ToString()); + } + parts.Add(Key.ToString()); + return string.Join(" + ", parts); + } + + public bool Matches(KeyEventArgs keyEvent) => keyEvent.Key == Key && keyEvent.Modifiers == Modifiers; + } +} diff --git a/src/Perspex.Input/KeyboardDevice.cs b/src/Perspex.Input/KeyboardDevice.cs index 390e925ce5..8c2154eb8c 100644 --- a/src/Perspex.Input/KeyboardDevice.cs +++ b/src/Perspex.Input/KeyboardDevice.cs @@ -104,6 +104,20 @@ namespace Perspex.Input Source = element, }; + IVisual currentHandler = element; + while (currentHandler != null && !ev.Handled && keyInput.Type == RawKeyEventType.KeyDown) + { + var bindings = (currentHandler as IInputElement)?.KeyBindings; + if(bindings!=null) + foreach (var binding in bindings) + { + if(ev.Handled) + break; + binding.TryHandle(ev); + } + currentHandler = currentHandler.VisualParent; + } + element.RaiseEvent(ev); break; } diff --git a/src/Perspex.Input/Perspex.Input.csproj b/src/Perspex.Input/Perspex.Input.csproj index 2bcf6f1e24..c853ea9bb1 100644 --- a/src/Perspex.Input/Perspex.Input.csproj +++ b/src/Perspex.Input/Perspex.Input.csproj @@ -66,6 +66,8 @@ + + diff --git a/src/Perspex.Input/Properties/AssemblyInfo.cs b/src/Perspex.Input/Properties/AssemblyInfo.cs index 04e306ea17..d83862fb4a 100644 --- a/src/Perspex.Input/Properties/AssemblyInfo.cs +++ b/src/Perspex.Input/Properties/AssemblyInfo.cs @@ -2,5 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Reflection; +using Perspex.Metadata; [assembly: AssemblyTitle("Perspex.Input")] +[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Input")] diff --git a/tests/Perspex.Input.UnitTests/KeyGestureParseTests.cs b/tests/Perspex.Input.UnitTests/KeyGestureParseTests.cs new file mode 100644 index 0000000000..0931c6f81b --- /dev/null +++ b/tests/Perspex.Input.UnitTests/KeyGestureParseTests.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Perspex.Input.UnitTests +{ + public class KeyGestureParseTests + { + private static readonly Dictionary SampleData = new Dictionary + { + {"Ctrl+A", new KeyGesture {Key = Key.A, Modifiers = InputModifiers.Control}}, + {" \tShift\t+Alt +B", new KeyGesture {Key = Key.B, Modifiers = InputModifiers.Shift|InputModifiers.Alt} }, + {"Control++", new KeyGesture {Key = Key.OemPlus, Modifiers = InputModifiers.Control} } + }; + + + + [Fact] + public void Key_Gesture_Is_Able_To_Parse_Sample_Data() + { + foreach (var d in SampleData) + Assert.Equal(d.Value, KeyGesture.Parse(d.Key)); + } + } +} diff --git a/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj b/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj index e63ccac17e..1aff689782 100644 --- a/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj +++ b/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj @@ -57,6 +57,7 @@ +