Browse Source

Implemented TouchDevice and touch input support for X11 backend

pull/2581/head
Nikita Tsukanov 7 years ago
parent
commit
6827179476
  1. 7
      src/Avalonia.Input/IPointer.cs
  2. 1
      src/Avalonia.Input/MouseDevice.cs
  3. 60
      src/Avalonia.Input/Pointer.cs
  4. 70
      src/Avalonia.Input/TouchDevice.cs
  5. 60
      src/Avalonia.Input/TouchGestureRecognizers/TapGestureRecognizer.cs
  6. 4
      src/Avalonia.X11/X11Platform.cs
  7. 55
      src/Avalonia.X11/XI2Manager.cs
  8. 9
      src/Avalonia.X11/XIStructs.cs
  9. 2
      tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs
  10. 2
      tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs

7
src/Avalonia.Input/IPointer.cs

@ -2,6 +2,7 @@ namespace Avalonia.Input
{ {
public interface IPointer public interface IPointer
{ {
int Id { get; }
void Capture(IInputElement control); void Capture(IInputElement control);
IInputElement Captured { get; } IInputElement Captured { get; }
PointerType Type { get; } PointerType Type { get; }
@ -14,4 +15,10 @@ namespace Avalonia.Input
Mouse, Mouse,
Touch Touch
} }
public class PointerIds
{
private static int s_nextPointerId = 1000;
public static int Next() => s_nextPointerId++;
}
} }

1
src/Avalonia.Input/MouseDevice.cs

@ -24,6 +24,7 @@ namespace Avalonia.Input
PointerType IPointer.Type => PointerType.Mouse; PointerType IPointer.Type => PointerType.Mouse;
bool IPointer.IsPrimary => true; bool IPointer.IsPrimary => true;
int IPointer.Id { get; } = PointerIds.Next();
/// <summary> /// <summary>
/// Gets the control that is currently capturing by the mouse, if any. /// Gets the control that is currently capturing by the mouse, if any.

60
src/Avalonia.Input/Pointer.cs

@ -0,0 +1,60 @@
using System;
using System.Linq;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
public class Pointer : IPointer, IDisposable
{
public Pointer(int id, PointerType type, bool isPrimary, IInputElement implicitlyCaptured)
{
Id = id;
Type = type;
IsPrimary = isPrimary;
ImplicitlyCaptured = implicitlyCaptured;
if (ImplicitlyCaptured != null)
ImplicitlyCaptured.DetachedFromVisualTree += OnImplicitCaptureDetached;
}
public int Id { get; }
public void Capture(IInputElement control)
{
if (Captured != null)
Captured.DetachedFromVisualTree -= OnCaptureDetached;
Captured = control;
if (Captured != null)
Captured.DetachedFromVisualTree += OnCaptureDetached;
}
IInputElement GetNextCapture(IVisual parent) =>
parent as IInputElement ?? parent.GetVisualAncestors().OfType<IInputElement>().FirstOrDefault();
private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e)
{
Capture(GetNextCapture(e.Parent));
}
private void OnImplicitCaptureDetached(object sender, VisualTreeAttachmentEventArgs e)
{
ImplicitlyCaptured.DetachedFromVisualTree -= OnImplicitCaptureDetached;
ImplicitlyCaptured = GetNextCapture(e.Parent);
if (ImplicitlyCaptured != null)
ImplicitlyCaptured.DetachedFromVisualTree += OnImplicitCaptureDetached;
}
public IInputElement Captured { get; private set; }
public IInputElement ImplicitlyCaptured { get; private set; }
public IInputElement GetEffectiveCapture() => Captured ?? ImplicitlyCaptured;
public PointerType Type { get; }
public bool IsPrimary { get; }
public void Dispose()
{
if (ImplicitlyCaptured != null)
ImplicitlyCaptured.DetachedFromVisualTree -= OnImplicitCaptureDetached;
if (Captured != null)
Captured.DetachedFromVisualTree -= OnCaptureDetached;
}
}
}

70
src/Avalonia.Input/TouchDevice.cs

@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Input.Raw;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
/// <summary>
/// Handles raw touch events
/// This class is supposed to be used on per-toplevel basis, don't event try to have a global instance
/// </summary>
public class TouchDevice : IInputDevice
{
Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
static InputModifiers GetModifiers(InputModifiers modifiers, bool left)
{
var mask = (InputModifiers)0x7fffffff ^ InputModifiers.LeftMouseButton ^ InputModifiers.MiddleMouseButton ^
InputModifiers.RightMouseButton;
modifiers &= mask;
if (left)
modifiers |= InputModifiers.LeftMouseButton;
return modifiers;
}
public void ProcessRawEvent(RawInputEventArgs ev)
{
var args = (RawTouchEventArgs)ev;
if (!_pointers.TryGetValue(args.TouchPointId, out var pointer))
{
if (args.Type == RawMouseEventType.TouchEnd)
return;
var hit = args.Root.InputHitTest(args.Position);
_pointers[args.TouchPointId] = pointer = new Pointer(PointerIds.Next(),
PointerType.Touch, _pointers.Count == 0, hit);
}
var target = pointer.GetEffectiveCapture() ?? args.Root;
if (args.Type == RawMouseEventType.TouchBegin)
{
var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
args.Root, args.Position, new PointerPointProperties(modifiers),
modifiers));
}
if (args.Type == RawMouseEventType.TouchEnd)
{
_pointers.Remove(args.TouchPointId);
var modifiers = GetModifiers(args.InputModifiers, false);
using (pointer)
{
target.RaiseEvent(new PointerReleasedEventArgs(target, pointer,
args.Root, args.Position, new PointerPointProperties(modifiers),
modifiers, pointer.IsPrimary ? MouseButton.Left : MouseButton.None));
}
}
if (args.Type == RawMouseEventType.TouchUpdate)
{
var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
args.Position, new PointerPointProperties(modifiers), modifiers));
}
}
}
}

60
src/Avalonia.Input/TouchGestureRecognizers/TapGestureRecognizer.cs

@ -0,0 +1,60 @@
using System;
using Avalonia.Interactivity;
namespace Avalonia.Input.TouchGestureRecognizers
{
/*
public class TapGestureRecognizer : ITouchGestureRecognizer
{
long _started;
Point _startPoint;
const double Distance = 20;
const long MaxTapDuration = 500;
TouchGestureRecognizerResult ITouchGestureRecognizer.RecognizeGesture(IInputElement owner, TouchEventArgs args)
{
if (args.Route == RoutingStrategies.Tunnel)
return TouchGestureRecognizerResult.Continue;
// Multi-touch sequence
if(args.Touches.Count > 1)
return TouchGestureRecognizerResult.Reject;
// Sequence started, save the start time
if(args.Type == TouchEventType.TouchBegin)
{
_started = args.Timestamp;
var pos = args.Touches[0].GetPosition(owner);
if (pos == null)
return TouchGestureRecognizerResult.Reject;
_startPoint = pos.Value;
return TouchGestureRecognizerResult.Continue;
}
if(args.Type == TouchEventType.TouchEnd)
{
var pos = args.RemovedTouches[0].GetPosition(owner);
if (pos == null)
return TouchGestureRecognizerResult.Reject;
var endPoint = pos.Value;
if(Math.Abs(endPoint.X - _startPoint.X) < Distance
&& Math.Abs(endPoint.Y - _startPoint.Y) < Distance
&& (args.Timestamp - _started) < MaxTapDuration)
{
((Interactive)args.RemovedTouches[0].InitialTarget).RaiseEvent(
new RoutedEventArgs(Gestures.TappedEvent));
return TouchGestureRecognizerResult.Accept;
}
else
return TouchGestureRecognizerResult.Reject;
}
return TouchGestureRecognizerResult.Continue;
}
public void Cancel()
{
_started = 0;
_startPoint = default;
}
}
*/
}

4
src/Avalonia.X11/X11Platform.cs

@ -28,6 +28,7 @@ namespace Avalonia.X11
public X11PlatformOptions Options { get; private set; } public X11PlatformOptions Options { get; private set; }
public void Initialize(X11PlatformOptions options) public void Initialize(X11PlatformOptions options)
{ {
Options = options;
XInitThreads(); XInitThreads();
Display = XOpenDisplay(IntPtr.Zero); Display = XOpenDisplay(IntPtr.Zero);
DeferredDisplay = XOpenDisplay(IntPtr.Zero); DeferredDisplay = XOpenDisplay(IntPtr.Zero);
@ -66,7 +67,7 @@ namespace Avalonia.X11
GlxGlPlatformFeature.TryInitialize(Info); GlxGlPlatformFeature.TryInitialize(Info);
} }
Options = options;
} }
public IntPtr DeferredDisplay { get; set; } public IntPtr DeferredDisplay { get; set; }
@ -96,6 +97,7 @@ namespace Avalonia
public bool UseEGL { get; set; } public bool UseEGL { get; set; }
public bool UseGpu { get; set; } = true; public bool UseGpu { get; set; } = true;
public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication"; public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication";
public bool? EnableMultiTouch { get; set; }
} }
public static class AvaloniaX11PlatformExtensions public static class AvaloniaX11PlatformExtensions
{ {

55
src/Avalonia.X11/XI2Manager.cs

@ -11,6 +11,7 @@ namespace Avalonia.X11
unsafe class XI2Manager unsafe class XI2Manager
{ {
private X11Info _x11; private X11Info _x11;
private bool _multitouch;
private Dictionary<IntPtr, IXI2Client> _clients = new Dictionary<IntPtr, IXI2Client>(); private Dictionary<IntPtr, IXI2Client> _clients = new Dictionary<IntPtr, IXI2Client>();
class DeviceInfo class DeviceInfo
{ {
@ -77,11 +78,14 @@ namespace Avalonia.X11
private PointerDeviceInfo _pointerDevice; private PointerDeviceInfo _pointerDevice;
private AvaloniaX11Platform _platform; private AvaloniaX11Platform _platform;
private readonly TouchDevice _touchDevice = new TouchDevice();
public bool Init(AvaloniaX11Platform platform) public bool Init(AvaloniaX11Platform platform)
{ {
_platform = platform; _platform = platform;
_x11 = platform.Info; _x11 = platform.Info;
_multitouch = platform.Options?.EnableMultiTouch ?? false;
var devices =(XIDeviceInfo*) XIQueryDevice(_x11.Display, var devices =(XIDeviceInfo*) XIQueryDevice(_x11.Display,
(int)XiPredefinedDeviceId.XIAllMasterDevices, out int num); (int)XiPredefinedDeviceId.XIAllMasterDevices, out int num);
for (var c = 0; c < num; c++) for (var c = 0; c < num; c++)
@ -121,16 +125,23 @@ namespace Avalonia.X11
public XEventMask AddWindow(IntPtr xid, IXI2Client window) public XEventMask AddWindow(IntPtr xid, IXI2Client window)
{ {
_clients[xid] = window; _clients[xid] = window;
var events = new List<XiEventType>
XiSelectEvents(_x11.Display, xid, new Dictionary<int, List<XiEventType>>
{ {
[_pointerDevice.Id] = new List<XiEventType>() XiEventType.XI_Motion,
XiEventType.XI_ButtonPress,
XiEventType.XI_ButtonRelease
};
if (_multitouch)
events.AddRange(new[]
{ {
XiEventType.XI_Motion, XiEventType.XI_TouchBegin,
XiEventType.XI_ButtonPress, XiEventType.XI_TouchUpdate,
XiEventType.XI_ButtonRelease, XiEventType.XI_TouchEnd
} });
});
XiSelectEvents(_x11.Display, xid,
new Dictionary<int, List<XiEventType>> {[_pointerDevice.Id] = events});
// We are taking over mouse input handling from here // We are taking over mouse input handling from here
return XEventMask.PointerMotionMask return XEventMask.PointerMotionMask
@ -154,8 +165,9 @@ namespace Avalonia.X11
_pointerDevice.Update(changed->Classes, changed->NumClasses); _pointerDevice.Update(changed->Classes, changed->NumClasses);
} }
//TODO: this should only be used for non-touch devices
if (xev->evtype >= XiEventType.XI_ButtonPress && xev->evtype <= XiEventType.XI_Motion) if ((xev->evtype >= XiEventType.XI_ButtonPress && xev->evtype <= XiEventType.XI_Motion)
|| (xev->evtype>=XiEventType.XI_TouchBegin&&xev->evtype<=XiEventType.XI_TouchEnd))
{ {
var dev = (XIDeviceEvent*)xev; var dev = (XIDeviceEvent*)xev;
if (_clients.TryGetValue(dev->EventWindow, out var client)) if (_clients.TryGetValue(dev->EventWindow, out var client))
@ -165,6 +177,23 @@ namespace Avalonia.X11
void OnDeviceEvent(IXI2Client client, ParsedDeviceEvent ev) void OnDeviceEvent(IXI2Client client, ParsedDeviceEvent ev)
{ {
if (ev.Type == XiEventType.XI_TouchBegin
|| ev.Type == XiEventType.XI_TouchUpdate
|| ev.Type == XiEventType.XI_TouchEnd)
{
var type = ev.Type == XiEventType.XI_TouchBegin ?
RawMouseEventType.TouchBegin :
(ev.Type == XiEventType.XI_TouchUpdate ?
RawMouseEventType.TouchUpdate :
RawMouseEventType.TouchEnd);
client.ScheduleInput(new RawTouchEventArgs(_touchDevice,
ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail));
return;
}
if (_multitouch && ev.Emulated)
return;
if (ev.Type == XiEventType.XI_Motion) if (ev.Type == XiEventType.XI_Motion)
{ {
Vector scrollDelta = default; Vector scrollDelta = default;
@ -210,7 +239,7 @@ namespace Avalonia.X11
client.ScheduleInput(new RawMouseEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, client.ScheduleInput(new RawMouseEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
type.Value, ev.Position, ev.Modifiers)); type.Value, ev.Position, ev.Modifiers));
} }
_pointerDevice.UpdateValuators(ev.Valuators); _pointerDevice.UpdateValuators(ev.Valuators);
} }
} }
@ -222,6 +251,8 @@ namespace Avalonia.X11
public ulong Timestamp { get; } public ulong Timestamp { get; }
public Point Position { get; } public Point Position { get; }
public int Button { get; set; } public int Button { get; set; }
public int Detail { get; set; }
public bool Emulated { get; set; }
public Dictionary<int, double> Valuators { get; } public Dictionary<int, double> Valuators { get; }
public ParsedDeviceEvent(XIDeviceEvent* ev) public ParsedDeviceEvent(XIDeviceEvent* ev)
{ {
@ -258,6 +289,8 @@ namespace Avalonia.X11
Valuators[c] = *values++; Valuators[c] = *values++;
if (Type == XiEventType.XI_ButtonPress || Type == XiEventType.XI_ButtonRelease) if (Type == XiEventType.XI_ButtonPress || Type == XiEventType.XI_ButtonRelease)
Button = ev->detail; Button = ev->detail;
Detail = ev->detail;
Emulated = ev->flags.HasFlag(XiDeviceEventFlags.XIPointerEmulated);
} }
} }

9
src/Avalonia.X11/XIStructs.cs

@ -230,13 +230,20 @@ namespace Avalonia.X11
public double root_y; public double root_y;
public double event_x; public double event_x;
public double event_y; public double event_y;
public int flags; public XiDeviceEventFlags flags;
public XIButtonState buttons; public XIButtonState buttons;
public XIValuatorState valuators; public XIValuatorState valuators;
public XIModifierState mods; public XIModifierState mods;
public XIModifierState group; public XIModifierState group;
} }
[Flags]
public enum XiDeviceEventFlags : int
{
None = 0,
XIPointerEmulated = (1 << 16)
}
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
unsafe struct XIEvent unsafe struct XIEvent
{ {

2
tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs

@ -9,6 +9,8 @@ namespace Avalonia.Controls.UnitTests
class TestPointer : IPointer class TestPointer : IPointer
{ {
public int Id { get; } = PointerIds.Next();
public void Capture(IInputElement control) public void Capture(IInputElement control)
{ {
Captured = control; Captured = control;

2
tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs

@ -12,6 +12,8 @@ namespace Avalonia.Controls.UnitTests.Platform
{ {
class FakePointer : IPointer class FakePointer : IPointer
{ {
public int Id { get; } = PointerIds.Next();
public void Capture(IInputElement control) public void Capture(IInputElement control)
{ {
Captured = control; Captured = control;

Loading…
Cancel
Save