Browse Source

Merge branch 'interaction-changes'

pull/422/head
Steven Kirk 10 years ago
parent
commit
023c831a63
  1. 59
      src/Perspex.Input/Gestures.cs
  2. 28
      src/Perspex.Input/InputElement.cs
  3. 1
      src/Perspex.Input/Perspex.Input.csproj
  4. 31
      src/Perspex.Input/PointerEventArgs.cs
  5. 5
      src/Perspex.Interactivity/Interactive.cs
  6. 77
      src/Perspex.Interactivity/RoutedEvent.cs
  7. 82
      tests/Perspex.Interactivity.UnitTests/GestureTests.cs
  8. 13
      tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj

59
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<RoutedEventArgs> TappedEvent = RoutedEvent.Register<RoutedEventArgs>(
"Tapped",
RoutingStrategies.Bubble,
typeof(Gestures));
public static readonly RoutedEvent<RoutedEventArgs> DoubleTappedEvent = RoutedEvent.Register<RoutedEventArgs>(
"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));
}
}
}
}
}

28
src/Perspex.Input/InputElement.cs

@ -137,6 +137,16 @@ namespace Perspex.Input
"PointerWheelChanged", "PointerWheelChanged",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble); RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="Tapped"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> TappedEvent = Gestures.TappedEvent;
/// <summary>
/// Defines the <see cref="DoubleTapped"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> DoubleTappedEvent = Gestures.DoubleTappedEvent;
private bool _isFocused; private bool _isFocused;
private bool _isPointerOver; private bool _isPointerOver;
@ -259,6 +269,24 @@ namespace Perspex.Input
remove { RemoveHandler(PointerWheelChangedEvent, value); } remove { RemoveHandler(PointerWheelChangedEvent, value); }
} }
/// <summary>
/// Occurs when a tap gesture occurs on the control.
/// </summary>
public event EventHandler<RoutedEventArgs> Tapped
{
add { AddHandler(TappedEvent, value); }
remove { RemoveHandler(TappedEvent, value); }
}
/// <summary>
/// Occurs when a double-tap gesture occurs on the control.
/// </summary>
public event EventHandler<RoutedEventArgs> DoubleTapped
{
add { AddHandler(DoubleTappedEvent, value); }
remove { RemoveHandler(DoubleTappedEvent, value); }
}
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the control can receive focus. /// Gets or sets a value indicating whether the control can receive focus.
/// </summary> /// </summary>

1
src/Perspex.Input/Perspex.Input.csproj

@ -68,6 +68,7 @@
</Compile> </Compile>
<Compile Include="Cursors.cs" /> <Compile Include="Cursors.cs" />
<Compile Include="AccessKeyHandler.cs" /> <Compile Include="AccessKeyHandler.cs" />
<Compile Include="Gestures.cs" />
<Compile Include="KeyBinding.cs" /> <Compile Include="KeyBinding.cs" />
<Compile Include="KeyGesture.cs" /> <Compile Include="KeyGesture.cs" />
<Compile Include="Platform\IClipboard.cs" /> <Compile Include="Platform\IClipboard.cs" />

31
src/Perspex.Input/PointerEventArgs.cs

@ -8,6 +8,17 @@ namespace Perspex.Input
{ {
public class PointerEventArgs : RoutedEventArgs public class PointerEventArgs : RoutedEventArgs
{ {
public PointerEventArgs()
{
}
public PointerEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
{
}
public IPointerDevice Device { get; set; } public IPointerDevice Device { get; set; }
public InputModifiers InputModifiers { get; set; } public InputModifiers InputModifiers { get; set; }
@ -28,12 +39,32 @@ namespace Perspex.Input
public class PointerPressedEventArgs : PointerEventArgs public class PointerPressedEventArgs : PointerEventArgs
{ {
public PointerPressedEventArgs()
: base(InputElement.PointerPressedEvent)
{
}
public PointerPressedEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
{
}
public int ClickCount { get; set; } public int ClickCount { get; set; }
public MouseButton MouseButton { get; set; } public MouseButton MouseButton { get; set; }
} }
public class PointerReleasedEventArgs : PointerEventArgs public class PointerReleasedEventArgs : PointerEventArgs
{ {
public PointerReleasedEventArgs()
: base(InputElement.PointerReleasedEvent)
{
}
public PointerReleasedEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
{
}
public MouseButton MouseButton { get; set; } public MouseButton MouseButton { get; set; }
} }
} }

5
src/Perspex.Interactivity/Interactive.cs

@ -122,16 +122,19 @@ namespace Perspex.Interactivity
{ {
e.Route = RoutingStrategies.Direct; e.Route = RoutingStrategies.Direct;
RaiseEventImpl(e); RaiseEventImpl(e);
e.RoutedEvent.InvokeRouteFinished(e);
} }
if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0) if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0)
{ {
TunnelEvent(e); TunnelEvent(e);
e.RoutedEvent.InvokeRouteFinished(e);
} }
if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0) if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0)
{ {
BubbleEvent(e); BubbleEvent(e);
e.RoutedEvent.InvokeRouteFinished(e);
} }
} }
@ -175,7 +178,7 @@ namespace Perspex.Interactivity
{ {
Contract.Requires<ArgumentNullException>(e != null); Contract.Requires<ArgumentNullException>(e != null);
e.RoutedEvent.InvokeClassHandlers(this, e); e.RoutedEvent.InvokeRaised(this, e);
List<EventSubscription> subscriptions; List<EventSubscription> subscriptions;

77
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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Collections.Generic; using System.Reactive.Subjects;
using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
@ -19,7 +18,8 @@ namespace Perspex.Interactivity
public class RoutedEvent public class RoutedEvent
{ {
private readonly List<ClassEventSubscription> _subscriptions = new List<ClassEventSubscription>(); private Subject<Tuple<object, RoutedEventArgs>> _raised = new Subject<Tuple<object, RoutedEventArgs>>();
private Subject<RoutedEventArgs> _routeFinished = new Subject<RoutedEventArgs>();
public RoutedEvent( public RoutedEvent(
string name, string name,
@ -62,6 +62,9 @@ namespace Perspex.Interactivity
private set; private set;
} }
public IObservable<Tuple<object, RoutedEventArgs>> Raised => _raised;
public IObservable<RoutedEventArgs> RouteFinished => _routeFinished;
public static RoutedEvent<TEventArgs> Register<TOwner, TEventArgs>( public static RoutedEvent<TEventArgs> Register<TOwner, TEventArgs>(
string name, string name,
RoutingStrategies routingStrategy) RoutingStrategies routingStrategy)
@ -83,27 +86,24 @@ namespace Perspex.Interactivity
return new RoutedEvent<TEventArgs>(name, routingStrategy, ownerType); return new RoutedEvent<TEventArgs>(name, routingStrategy, ownerType);
} }
public void AddClassHandler(Type type, EventHandler<RoutedEventArgs> handler, RoutingStrategies routes) public IDisposable AddClassHandler(
Type targetType,
EventHandler<RoutedEventArgs> handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false)
{ {
_subscriptions.Add(new ClassEventSubscription return Raised.Subscribe(args =>
{ {
TargetType = type, var sender = args.Item1;
Handler = handler, var e = args.Item2;
Routes = routes,
});
}
internal void InvokeClassHandlers(object sender, RoutedEventArgs e) if (targetType.GetTypeInfo().IsAssignableFrom(sender.GetType().GetTypeInfo()) &&
{ ((e.Route == RoutingStrategies.Direct) || (e.Route & routes) != 0) &&
foreach (var sub in _subscriptions) (!e.Handled || handledEventsToo))
{
if (sub.TargetType.GetTypeInfo().IsAssignableFrom(sender.GetType().GetTypeInfo()) &&
((e.Route == RoutingStrategies.Direct) || (e.Route & sub.Routes) != 0) &&
(!e.Handled || sub.AlsoIfHandled))
{ {
try try
{ {
sub.Handler.DynamicInvoke(sender, e); handler.DynamicInvoke(sender, e);
} }
catch (TargetInvocationException ex) catch (TargetInvocationException ex)
{ {
@ -111,12 +111,17 @@ namespace Perspex.Interactivity
ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); 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<ArgumentNullException>(ownerType != null); Contract.Requires<ArgumentNullException>(ownerType != null);
} }
public void AddClassHandler<TTarget>( public IDisposable AddClassHandler<TTarget>(
Func<TTarget, Action<TEventArgs>> handler, Func<TTarget, Action<TEventArgs>> handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble) RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
where TTarget : class bool handledEventsToo = false)
where TTarget : class, IInteractive
{ {
AddClassHandler(typeof(TTarget), (s, e) => ClassHandlerAdapter<TTarget>(s, e, handler), routes); EventHandler<RoutedEventArgs> adapter = (sender, e) =>
} {
var target = sender as TTarget;
var args = e as TEventArgs;
private static void ClassHandlerAdapter<TTarget>( if (target != null && args != null)
object sender, {
RoutedEventArgs e, handler(target)(args);
Func<TTarget, Action<TEventArgs>> handler) where TTarget : class }
{ };
var target = sender as TTarget;
var args = e as TEventArgs;
if (target != null && args != null) return AddClassHandler(typeof(TTarget), adapter, routes, handledEventsToo);
{
handler(target)(args);
}
} }
} }
} }

82
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<string>();
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<string>();
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<string>();
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);
}
}
}

13
tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj

@ -59,6 +59,7 @@
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="GestureTests.cs" />
<Compile Include="InteractiveTests.cs" /> <Compile Include="InteractiveTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
@ -74,6 +75,14 @@
<Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project> <Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project>
<Name>Perspex.Base</Name> <Name>Perspex.Base</Name>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\..\src\Perspex.Controls\Perspex.Controls.csproj">
<Project>{d2221c82-4a25-4583-9b43-d791e3f6820c}</Project>
<Name>Perspex.Controls</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Perspex.Input\Perspex.Input.csproj">
<Project>{62024b2d-53eb-4638-b26b-85eeaa54866e}</Project>
<Name>Perspex.Input</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Perspex.Interactivity\Perspex.Interactivity.csproj"> <ProjectReference Include="..\..\src\Perspex.Interactivity\Perspex.Interactivity.csproj">
<Project>{6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}</Project> <Project>{6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}</Project>
<Name>Perspex.Interactivity</Name> <Name>Perspex.Interactivity</Name>
@ -86,6 +95,10 @@
<Project>{EB582467-6ABB-43A1-B052-E981BA910E3A}</Project> <Project>{EB582467-6ABB-43A1-B052-E981BA910E3A}</Project>
<Name>Perspex.SceneGraph</Name> <Name>Perspex.SceneGraph</Name>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\..\src\Perspex.Styling\Perspex.Styling.csproj">
<Project>{f1baa01a-f176-4c6a-b39d-5b40bb1b148f}</Project>
<Name>Perspex.Styling</Name>
</ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" /> <Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />

Loading…
Cancel
Save