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 @@
+