34 changed files with 698 additions and 48 deletions
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,117 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using System.Windows.Input; |
|||
using Perspex.Controls; |
|||
using Perspex.Controls.Utils; |
|||
using Perspex.Input; |
|||
|
|||
namespace Perspex.Controls |
|||
{ |
|||
public class HotKeyManager |
|||
{ |
|||
public static PerspexProperty<KeyGesture> HotKeyProperty |
|||
= PerspexProperty.RegisterAttached<Control, KeyGesture>("HotKey", typeof (HotKeyManager)); |
|||
|
|||
class HotkeyCommandWrapper : ICommand |
|||
{ |
|||
public HotkeyCommandWrapper(IControl control) |
|||
{ |
|||
Control = control; |
|||
} |
|||
|
|||
public IControl Control; |
|||
|
|||
private ICommand GetCommand() => Control.GetValue(Button.CommandProperty); |
|||
|
|||
public bool CanExecute(object parameter) => GetCommand()?.CanExecute(parameter) ?? false; |
|||
|
|||
public void Execute(object parameter) => GetCommand()?.Execute(parameter); |
|||
|
|||
//Implementation isn't needed in this case
|
|||
public event EventHandler CanExecuteChanged; |
|||
} |
|||
|
|||
|
|||
class Manager |
|||
{ |
|||
private readonly IControl _control; |
|||
private TopLevel _root; |
|||
private IDisposable _parentSub; |
|||
private IDisposable _hotkeySub; |
|||
private KeyGesture _hotkey; |
|||
private HotkeyCommandWrapper _wrapper; |
|||
private KeyBinding _binding; |
|||
|
|||
public Manager(IControl control) |
|||
{ |
|||
_control = control; |
|||
_wrapper = new HotkeyCommandWrapper(_control); |
|||
} |
|||
|
|||
public void Init() |
|||
{ |
|||
_hotkeySub = _control.GetObservable(HotKeyProperty).Subscribe(OnHotkeyChanged); |
|||
_parentSub = AncestorFinder.Create(_control, typeof (TopLevel)).Subscribe(OnParentChanged); |
|||
} |
|||
|
|||
private void OnParentChanged(IControl control) |
|||
{ |
|||
Unregister(); |
|||
_root = (TopLevel) control; |
|||
Register(); |
|||
} |
|||
|
|||
private void OnHotkeyChanged(KeyGesture hotkey) |
|||
{ |
|||
if (hotkey == null) |
|||
//Subscription will be recreated by static property watcher
|
|||
Stop(); |
|||
else |
|||
{ |
|||
Unregister(); |
|||
_hotkey = hotkey; |
|||
Register(); |
|||
} |
|||
} |
|||
|
|||
void Unregister() |
|||
{ |
|||
if (_root != null && _binding != null) |
|||
_root.KeyBindings.Remove(_binding); |
|||
_binding = null; |
|||
} |
|||
|
|||
void Register() |
|||
{ |
|||
if (_root != null && _hotkey != null) |
|||
{ |
|||
_binding = new KeyBinding() {Gesture = _hotkey, Command = _wrapper}; |
|||
_root.KeyBindings.Add(_binding); |
|||
} |
|||
} |
|||
|
|||
void Stop() |
|||
{ |
|||
Unregister(); |
|||
_parentSub.Dispose(); |
|||
_hotkeySub.Dispose(); |
|||
} |
|||
} |
|||
|
|||
static HotKeyManager() |
|||
{ |
|||
HotKeyProperty.Changed.Subscribe(args => |
|||
{ |
|||
var control = args.Sender as IControl; |
|||
if (args.OldValue != null|| control == null) |
|||
return; |
|||
new Manager(control).Init(); |
|||
}); |
|||
} |
|||
public static void SetHotKey(PerspexObject target, KeyGesture value) => target.SetValue(HotKeyProperty, value); |
|||
public static KeyGesture GetHotKey(PerspexObject target) => target.GetValue(HotKeyProperty); |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Reactive; |
|||
using System.Reactive.Disposables; |
|||
using System.Reactive.Subjects; |
|||
using System.Reflection; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Perspex.Controls.Utils |
|||
{ |
|||
public static class AncestorFinder |
|||
{ |
|||
class FinderNode : IDisposable |
|||
{ |
|||
private readonly IControl _control; |
|||
private readonly TypeInfo _ancestorType; |
|||
public IObservable<IControl> Observable => _subject; |
|||
private readonly Subject<IControl> _subject = new Subject<IControl>(); |
|||
|
|||
private FinderNode _child; |
|||
private IDisposable _disposable; |
|||
|
|||
public FinderNode(IControl control, TypeInfo ancestorType) |
|||
{ |
|||
_control = control; |
|||
_ancestorType = ancestorType; |
|||
} |
|||
|
|||
public void Init() |
|||
{ |
|||
_disposable = _control.GetObservable(Control.ParentProperty).Subscribe(OnValueChanged); |
|||
} |
|||
|
|||
private void OnValueChanged(IControl next) |
|||
{ |
|||
if (next == null || _ancestorType.IsAssignableFrom(next.GetType().GetTypeInfo())) |
|||
_subject.OnNext(next); |
|||
else |
|||
{ |
|||
_child?.Dispose(); |
|||
_child = new FinderNode(next, _ancestorType); |
|||
_child.Observable.Subscribe(OnChildValueChanged); |
|||
_child.Init(); |
|||
} |
|||
} |
|||
|
|||
private void OnChildValueChanged(IControl control) => _subject.OnNext(control); |
|||
|
|||
|
|||
public void Dispose() |
|||
{ |
|||
_disposable.Dispose(); |
|||
} |
|||
} |
|||
|
|||
|
|||
public static IObservable<IControl> Create(IControl control, Type ancestorType) |
|||
{ |
|||
return new AnonymousObservable<IControl>(observer => |
|||
{ |
|||
var finder = new FinderNode(control, ancestorType.GetTypeInfo()); |
|||
var subscription = finder.Observable.Subscribe(observer); |
|||
finder.Init(); |
|||
|
|||
return Disposable.Create(() => |
|||
{ |
|||
subscription.Dispose(); |
|||
finder.Dispose(); |
|||
}); |
|||
}); |
|||
|
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -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<ICommand> CommandProperty = |
|||
PerspexProperty.Register<KeyBinding, ICommand>("Command"); |
|||
|
|||
public ICommand Command |
|||
{ |
|||
get { return GetValue(CommandProperty); } |
|||
set { SetValue(CommandProperty, value); } |
|||
} |
|||
|
|||
public static PerspexProperty<KeyGesture> GestureProperty = |
|||
PerspexProperty.Register<KeyBinding, KeyGesture>("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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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<KeyGesture> |
|||
{ |
|||
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<string, Key> KeySynonims = new Dictionary<string, Key> |
|||
{ |
|||
{"+", 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<string>(); |
|||
|
|||
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<string>(); |
|||
foreach (var flag in Enum.GetValues(typeof (InputModifiers)).Cast<InputModifiers>()) |
|||
{ |
|||
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; |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Perspex.Controls.Utils; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Controls.UnitTests.Utils |
|||
{ |
|||
public class AncestorFinderTests |
|||
{ |
|||
[Fact] |
|||
public void SanityCheck() |
|||
{ |
|||
var child = new Control(); |
|||
var parent = new Decorator(); |
|||
var grandParent = new Border(); |
|||
var grandParent2 = new Border(); |
|||
|
|||
IVisual currentParent = null; |
|||
var subscription = AncestorFinder.Create(child, typeof (Border)).Subscribe(s => currentParent = s); |
|||
|
|||
Assert.Null(currentParent); |
|||
parent.Child = child; |
|||
Assert.Null(currentParent); |
|||
grandParent.Child = parent; |
|||
Assert.Equal(grandParent, currentParent); |
|||
grandParent.Child = null; |
|||
grandParent2.Child = parent; |
|||
Assert.Equal(grandParent2, currentParent); |
|||
|
|||
subscription.Dispose(); |
|||
parent.Child = null; |
|||
Assert.Equal(grandParent2, currentParent); |
|||
} |
|||
|
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Moq; |
|||
using Perspex.Controls.Presenters; |
|||
using Perspex.Controls.Templates; |
|||
using Perspex.Input; |
|||
using Perspex.Platform; |
|||
using Perspex.Styling; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Controls.UnitTests.Utils |
|||
{ |
|||
public class HotKeyManagerTests |
|||
{ |
|||
[Fact] |
|||
public void HotKeyManager_Should_Register_And_Unregister_Key_Binding() |
|||
{ |
|||
using (PerspexLocator.EnterScope()) |
|||
{ |
|||
var windowImpl = new Mock<IWindowImpl>(); |
|||
var styler = new Mock<Styler>(); |
|||
|
|||
PerspexLocator.CurrentMutable |
|||
.Bind<IWindowImpl>().ToConstant(windowImpl.Object) |
|||
.Bind<IStyler>().ToConstant(styler.Object); |
|||
|
|||
var gesture1 = new KeyGesture {Key = Key.A, Modifiers = InputModifiers.Control}; |
|||
var gesture2 = new KeyGesture {Key = Key.B, Modifiers = InputModifiers.Control}; |
|||
|
|||
var tl = new Window(); |
|||
var button = new Button(); |
|||
tl.Content = button; |
|||
tl.Template = CreateWindowTemplate(); |
|||
tl.ApplyTemplate(); |
|||
|
|||
HotKeyManager.SetHotKey(button, gesture1); |
|||
|
|||
Assert.Equal(gesture1, tl.KeyBindings[0].Gesture); |
|||
|
|||
HotKeyManager.SetHotKey(button, gesture2); |
|||
Assert.Equal(gesture2, tl.KeyBindings[0].Gesture); |
|||
|
|||
button.SetValue(Control.ParentProperty, null); |
|||
|
|||
Assert.Empty(tl.KeyBindings); |
|||
|
|||
button.SetValue(Control.ParentProperty, tl); |
|||
|
|||
Assert.Equal(gesture2, tl.KeyBindings[0].Gesture); |
|||
|
|||
HotKeyManager.SetHotKey(button, null); |
|||
Assert.Empty(tl.KeyBindings); |
|||
|
|||
} |
|||
} |
|||
|
|||
private ControlTemplate CreateWindowTemplate() |
|||
{ |
|||
return new ControlTemplate<Window>(parent => |
|||
{ |
|||
return new ContentPresenter |
|||
{ |
|||
Name = "contentPresenter", |
|||
[~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -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<string, KeyGesture> SampleData = new Dictionary<string, KeyGesture> |
|||
{ |
|||
{"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)); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue