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",
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 _isPointerOver;
@ -259,6 +269,24 @@ namespace Perspex.Input
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>
/// Gets or sets a value indicating whether the control can receive focus.
/// </summary>

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

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

31
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; }
}
}

5
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<ArgumentNullException>(e != null);
e.RoutedEvent.InvokeClassHandlers(this, e);
e.RoutedEvent.InvokeRaised(this, e);
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.
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<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(
string name,
@ -62,6 +62,9 @@ namespace Perspex.Interactivity
private set;
}
public IObservable<Tuple<object, RoutedEventArgs>> Raised => _raised;
public IObservable<RoutedEventArgs> RouteFinished => _routeFinished;
public static RoutedEvent<TEventArgs> Register<TOwner, TEventArgs>(
string name,
RoutingStrategies routingStrategy)
@ -83,27 +86,24 @@ namespace Perspex.Interactivity
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,
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<ArgumentNullException>(ownerType != null);
}
public void AddClassHandler<TTarget>(
public IDisposable AddClassHandler<TTarget>(
Func<TTarget, Action<TEventArgs>> 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<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>(
object sender,
RoutedEventArgs e,
Func<TTarget, Action<TEventArgs>> 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);
}
}
}

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>
</ItemGroup>
<ItemGroup>
<Compile Include="GestureTests.cs" />
<Compile Include="InteractiveTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
@ -74,6 +75,14 @@
<Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project>
<Name>Perspex.Base</Name>
</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">
<Project>{6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}</Project>
<Name>Perspex.Interactivity</Name>
@ -86,6 +95,10 @@
<Project>{EB582467-6ABB-43A1-B052-E981BA910E3A}</Project>
<Name>Perspex.SceneGraph</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Perspex.Styling\Perspex.Styling.csproj">
<Project>{f1baa01a-f176-4c6a-b39d-5b40bb1b148f}</Project>
<Name>Perspex.Styling</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />

Loading…
Cancel
Save