diff --git a/src/Perspex.Input/Gestures.cs b/src/Perspex.Input/Gestures.cs new file mode 100644 index 0000000000..48e9d87f86 --- /dev/null +++ b/src/Perspex.Input/Gestures.cs @@ -0,0 +1,59 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Perspex.Interactivity; + +namespace Perspex.Input +{ + public static class Gestures + { + public static readonly RoutedEvent TappedEvent = RoutedEvent.Register( + "Tapped", + RoutingStrategies.Bubble, + typeof(Gestures)); + + public static readonly RoutedEvent DoubleTappedEvent = RoutedEvent.Register( + "DoubleTapped", + RoutingStrategies.Bubble, + typeof(Gestures)); + + private static IInteractive s_lastPress; + + static Gestures() + { + InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); + InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased); + } + + private static void PointerPressed(RoutedEventArgs ev) + { + if (ev.Route == RoutingStrategies.Bubble) + { + var e = (PointerPressedEventArgs)ev; + + if (e.ClickCount <= 1) + { + s_lastPress = e.Source; + } + else if (e.ClickCount == 2 && s_lastPress == e.Source) + { + e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent)); + } + } + } + + private static void PointerReleased(RoutedEventArgs ev) + { + if (ev.Route == RoutingStrategies.Bubble) + { + var e = (PointerReleasedEventArgs)ev; + + if (s_lastPress == e.Source) + { + s_lastPress.RaiseEvent(new RoutedEventArgs(TappedEvent)); + } + } + } + } +} diff --git a/src/Perspex.Input/InputElement.cs b/src/Perspex.Input/InputElement.cs index dab429cbdf..00e53df646 100644 --- a/src/Perspex.Input/InputElement.cs +++ b/src/Perspex.Input/InputElement.cs @@ -137,6 +137,16 @@ namespace Perspex.Input "PointerWheelChanged", RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + /// + /// Defines the event. + /// + public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent; + + /// + /// Defines the event. + /// + public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; + private bool _isFocused; private bool _isPointerOver; @@ -259,6 +269,24 @@ namespace Perspex.Input remove { RemoveHandler(PointerWheelChangedEvent, value); } } + /// + /// Occurs when a tap gesture occurs on the control. + /// + public event EventHandler Tapped + { + add { AddHandler(TappedEvent, value); } + remove { RemoveHandler(TappedEvent, value); } + } + + /// + /// Occurs when a double-tap gesture occurs on the control. + /// + public event EventHandler DoubleTapped + { + add { AddHandler(DoubleTappedEvent, value); } + remove { RemoveHandler(DoubleTappedEvent, value); } + } + /// /// Gets or sets a value indicating whether the control can receive focus. /// diff --git a/src/Perspex.Input/Perspex.Input.csproj b/src/Perspex.Input/Perspex.Input.csproj index 264eda7473..80e1566f1c 100644 --- a/src/Perspex.Input/Perspex.Input.csproj +++ b/src/Perspex.Input/Perspex.Input.csproj @@ -68,6 +68,7 @@ + diff --git a/src/Perspex.Input/PointerEventArgs.cs b/src/Perspex.Input/PointerEventArgs.cs index fab4c2a9c6..25fbf7f26d 100644 --- a/src/Perspex.Input/PointerEventArgs.cs +++ b/src/Perspex.Input/PointerEventArgs.cs @@ -8,6 +8,17 @@ namespace Perspex.Input { public class PointerEventArgs : RoutedEventArgs { + public PointerEventArgs() + { + + } + + public PointerEventArgs(RoutedEvent routedEvent) + : base(routedEvent) + { + + } + public IPointerDevice Device { get; set; } public InputModifiers InputModifiers { get; set; } @@ -28,12 +39,32 @@ namespace Perspex.Input public class PointerPressedEventArgs : PointerEventArgs { + public PointerPressedEventArgs() + : base(InputElement.PointerPressedEvent) + { + } + + public PointerPressedEventArgs(RoutedEvent routedEvent) + : base(routedEvent) + { + } + public int ClickCount { get; set; } public MouseButton MouseButton { get; set; } } public class PointerReleasedEventArgs : PointerEventArgs { + public PointerReleasedEventArgs() + : base(InputElement.PointerReleasedEvent) + { + } + + public PointerReleasedEventArgs(RoutedEvent routedEvent) + : base(routedEvent) + { + } + public MouseButton MouseButton { get; set; } } } diff --git a/src/Perspex.Interactivity/Interactive.cs b/src/Perspex.Interactivity/Interactive.cs index a6bfd0d94c..c348a3ed83 100644 --- a/src/Perspex.Interactivity/Interactive.cs +++ b/src/Perspex.Interactivity/Interactive.cs @@ -122,16 +122,19 @@ namespace Perspex.Interactivity { e.Route = RoutingStrategies.Direct; RaiseEventImpl(e); + e.RoutedEvent.InvokeRouteFinished(e); } if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0) { TunnelEvent(e); + e.RoutedEvent.InvokeRouteFinished(e); } if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0) { BubbleEvent(e); + e.RoutedEvent.InvokeRouteFinished(e); } } @@ -175,7 +178,7 @@ namespace Perspex.Interactivity { Contract.Requires(e != null); - e.RoutedEvent.InvokeClassHandlers(this, e); + e.RoutedEvent.InvokeRaised(this, e); List subscriptions; diff --git a/src/Perspex.Interactivity/RoutedEvent.cs b/src/Perspex.Interactivity/RoutedEvent.cs index 3fa8f9aaf5..9b5dee61f5 100644 --- a/src/Perspex.Interactivity/RoutedEvent.cs +++ b/src/Perspex.Interactivity/RoutedEvent.cs @@ -2,8 +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.Expressions; +using System.Reactive.Subjects; using System.Reflection; using System.Runtime.ExceptionServices; @@ -19,7 +18,8 @@ namespace Perspex.Interactivity public class RoutedEvent { - private readonly List _subscriptions = new List(); + private Subject> _raised = new Subject>(); + private Subject _routeFinished = new Subject(); public RoutedEvent( string name, @@ -62,6 +62,9 @@ namespace Perspex.Interactivity private set; } + public IObservable> Raised => _raised; + public IObservable RouteFinished => _routeFinished; + public static RoutedEvent Register( string name, RoutingStrategies routingStrategy) @@ -83,27 +86,24 @@ namespace Perspex.Interactivity return new RoutedEvent(name, routingStrategy, ownerType); } - public void AddClassHandler(Type type, EventHandler handler, RoutingStrategies routes) + public IDisposable AddClassHandler( + Type targetType, + EventHandler handler, + RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, + bool handledEventsToo = false) { - _subscriptions.Add(new ClassEventSubscription + return Raised.Subscribe(args => { - TargetType = type, - Handler = handler, - Routes = routes, - }); - } + var sender = args.Item1; + var e = args.Item2; - internal void InvokeClassHandlers(object sender, RoutedEventArgs e) - { - foreach (var sub in _subscriptions) - { - if (sub.TargetType.GetTypeInfo().IsAssignableFrom(sender.GetType().GetTypeInfo()) && - ((e.Route == RoutingStrategies.Direct) || (e.Route & sub.Routes) != 0) && - (!e.Handled || sub.AlsoIfHandled)) + if (targetType.GetTypeInfo().IsAssignableFrom(sender.GetType().GetTypeInfo()) && + ((e.Route == RoutingStrategies.Direct) || (e.Route & routes) != 0) && + (!e.Handled || handledEventsToo)) { try { - sub.Handler.DynamicInvoke(sender, e); + handler.DynamicInvoke(sender, e); } catch (TargetInvocationException ex) { @@ -111,12 +111,17 @@ namespace Perspex.Interactivity ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); } } - } + }); } - private class ClassEventSubscription : EventSubscription + internal void InvokeRaised(object sender, RoutedEventArgs e) { - public Type TargetType { get; set; } + _raised.OnNext(Tuple.Create(sender, e)); + } + + internal void InvokeRouteFinished(RoutedEventArgs e) + { + _routeFinished.OnNext(e); } } @@ -130,26 +135,24 @@ namespace Perspex.Interactivity Contract.Requires(ownerType != null); } - public void AddClassHandler( + public IDisposable AddClassHandler( Func> handler, - RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble) - where TTarget : class + RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, + bool handledEventsToo = false) + where TTarget : class, IInteractive { - AddClassHandler(typeof(TTarget), (s, e) => ClassHandlerAdapter(s, e, handler), routes); - } + EventHandler adapter = (sender, e) => + { + var target = sender as TTarget; + var args = e as TEventArgs; - private static void ClassHandlerAdapter( - object sender, - RoutedEventArgs e, - Func> handler) where TTarget : class - { - var target = sender as TTarget; - var args = e as TEventArgs; + if (target != null && args != null) + { + handler(target)(args); + } + }; - if (target != null && args != null) - { - handler(target)(args); - } + return AddClassHandler(typeof(TTarget), adapter, routes, handledEventsToo); } } } diff --git a/tests/Perspex.Interactivity.UnitTests/GestureTests.cs b/tests/Perspex.Interactivity.UnitTests/GestureTests.cs new file mode 100644 index 0000000000..9712ea480e --- /dev/null +++ b/tests/Perspex.Interactivity.UnitTests/GestureTests.cs @@ -0,0 +1,82 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; +using Perspex.Controls; +using Perspex.Input; +using Xunit; + +namespace Perspex.Interactivity.UnitTests +{ + public class GestureTests + { + [Fact] + public void Tapped_Should_Follow_Pointer_Pressed_Released() + { + Border border; + var decorator = new Decorator + { + Child = border = new Border() + }; + var result = new List(); + + decorator.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("dp")); + decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr")); + decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); + border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); + border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); + border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); + + border.RaiseEvent(new PointerPressedEventArgs()); + border.RaiseEvent(new PointerReleasedEventArgs()); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt" }, result); + } + + [Fact] + public void Tapped_Should_Be_Raised_Even_When_PointerPressed_Handled() + { + Border border; + var decorator = new Decorator + { + Child = border = new Border() + }; + var result = new List(); + + border.AddHandler(Border.PointerPressedEvent, (s, e) => e.Handled = true); + decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); + border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); + + border.RaiseEvent(new PointerPressedEventArgs()); + border.RaiseEvent(new PointerReleasedEventArgs()); + + Assert.Equal(new[] { "bt", "dt" }, result); + } + + [Fact] + public void DoubleTapped_Should_Follow_Pointer_Pressed_Released_Pressed() + { + Border border; + var decorator = new Decorator + { + Child = border = new Border() + }; + var result = new List(); + + decorator.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("dp")); + decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr")); + decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); + decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt")); + border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); + border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); + border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); + border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); + + border.RaiseEvent(new PointerPressedEventArgs()); + border.RaiseEvent(new PointerReleasedEventArgs()); + border.RaiseEvent(new PointerPressedEventArgs { ClickCount = 2 }); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result); + } + } +} diff --git a/tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj b/tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj index 69137a9d35..9532487e13 100644 --- a/tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj +++ b/tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj @@ -59,6 +59,7 @@ + @@ -74,6 +75,14 @@ {B09B78D8-9B26-48B0-9149-D64A2F120F3F} Perspex.Base + + {d2221c82-4a25-4583-9b43-d791e3f6820c} + Perspex.Controls + + + {62024b2d-53eb-4638-b26b-85eeaa54866e} + Perspex.Input + {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B} Perspex.Interactivity @@ -86,6 +95,10 @@ {EB582467-6ABB-43A1-B052-E981BA910E3A} Perspex.SceneGraph + + {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} + Perspex.Styling +