File: System\Windows\Input\Stylus\Pointer\PointerInteractionEngine.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationCore\PresentationCore.csproj (PresentationCore)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
 
using MS.Win32.Pointer;
using MS.Internal;
using MS.Internal.Interop;
using MS.Internal.PresentationCore;                        // SecurityHelper
using MS.Utility;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security;
using System.Windows;
using System.Windows.Input;
using System.Windows.Input.StylusPlugIns;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Threading;
using SR = MS.Internal.PresentationCore.SR;
 
namespace System.Windows.Input.StylusPointer
{
    /// <summary>
    /// Provides access and data from the Windows Interaction Context engine.
    /// 
    /// This gives WPF access to gestures and other features based off WM_POINTER data.
    /// </summary>
    internal class PointerInteractionEngine : IDisposable
    {
        #region Constants
 
        /// <summary>
        /// The delay before firing a hover in processor ticks.
        /// This threshold is taken from WISP activation delay code.
        /// </summary>
        private const int HoverActivationThresholdTicks = 275;
 
        /// <summary>
        /// The drag threshold in inches.
        /// This threshold is taken from WISP drag detection code.
        /// </summary>
        private const double DragThresholdInches = 0.106299;
 
        /// <summary>
        /// Configuration parameters for the interaction context.  We use it for tap, hold, right (secondary) tap,
        /// and drag detection via manipulation.  Manipulation can also feed flick detection if used.
        /// </summary>
        private static List<UnsafeNativeMethods.INTERACTION_CONTEXT_CONFIGURATION> DefaultConfiguration =
            new List<UnsafeNativeMethods.INTERACTION_CONTEXT_CONFIGURATION>()
            {
                new UnsafeNativeMethods.INTERACTION_CONTEXT_CONFIGURATION()
                {
                    enable = UnsafeNativeMethods.INTERACTION_CONFIGURATION_FLAGS.INTERACTION_CONFIGURATION_FLAG_TAP,
                    interactionId = UnsafeNativeMethods.INTERACTION_ID.INTERACTION_ID_TAP
                },
                new UnsafeNativeMethods.INTERACTION_CONTEXT_CONFIGURATION()
                {
                    enable = UnsafeNativeMethods.INTERACTION_CONFIGURATION_FLAGS.INTERACTION_CONFIGURATION_FLAG_HOLD,
                    interactionId = UnsafeNativeMethods.INTERACTION_ID.INTERACTION_ID_HOLD
                },
                new UnsafeNativeMethods.INTERACTION_CONTEXT_CONFIGURATION()
                {
                    enable = UnsafeNativeMethods.INTERACTION_CONFIGURATION_FLAGS.INTERACTION_CONFIGURATION_FLAG_SECONDARY_TAP,
                    interactionId = UnsafeNativeMethods.INTERACTION_ID.INTERACTION_ID_SECONDARY_TAP
                },
                new UnsafeNativeMethods.INTERACTION_CONTEXT_CONFIGURATION()
                {
                    enable = UnsafeNativeMethods.INTERACTION_CONFIGURATION_FLAGS.INTERACTION_CONFIGURATION_FLAG_MANIPULATION
                    | UnsafeNativeMethods.INTERACTION_CONFIGURATION_FLAGS.INTERACTION_CONFIGURATION_FLAG_MANIPULATION_TRANSLATION_INERTIA
                    | UnsafeNativeMethods.INTERACTION_CONFIGURATION_FLAGS.INTERACTION_CONFIGURATION_FLAG_MANIPULATION_TRANSLATION_X
                    | UnsafeNativeMethods.INTERACTION_CONFIGURATION_FLAGS.INTERACTION_CONFIGURATION_FLAG_MANIPULATION_TRANSLATION_Y,
                    interactionId = UnsafeNativeMethods.INTERACTION_ID.INTERACTION_ID_MANIPULATION
                },
            };
 
        #endregion
 
        #region Enumerations
 
        /// <summary>
        /// Determines the current hover tracking state
        /// </summary>
        private enum HoverState
        {
            AwaitingHover,
            TimingHover,
            HoverCancelled,
            InHover,
        }
 
        #endregion
 
        #region Private Members
 
        /// <summary>
        /// Holds the reference to the interaction context
        /// </summary>
        private IntPtr _interactionContext = IntPtr.Zero;
 
        /// <summary>
        /// The stylus device that owns this interaction engine.
        /// </summary>
        private PointerStylusDevice _stylusDevice = null;
 
        /// <summary>
        /// A callback for interaction events
        /// </summary>
        private UnsafeNativeMethods.INTERACTION_CONTEXT_OUTPUT_CALLBACK _callbackDelegate;
 
        #region Drag/Flick/Hold/Hover Tracking
 
        /// <summary>
        /// Has a drag been fired in the latest pointer message series
        /// </summary>
        private bool _firedDrag = false;
 
        /// <summary>
        /// Has a hold been fired in the latest pointer message series
        /// </summary>
        private bool _firedHold = false;
 
        /// <summary>
        /// Has a flick been fired in the latest pointer message series
        /// </summary>
        private bool _firedFlick = false;
 
        /// <summary>
        /// The current state of hover tracking
        /// </summary>
        private HoverState _hoverState;
 
        /// <summary>
        /// When hover started, in processor ticks
        /// </summary>
        private uint _hoverStartTicks = 0;
 
        /// <summary>
        /// The engine used to detect and fire flicks
        /// </summary>
        private PointerFlickEngine _flickEngine = null;
 
        #endregion
 
        #endregion
 
        #region Events
 
        /// <summary>
        /// An event to forward interactions back to the stack as touch gestures
        /// </summary>
        internal event EventHandler<RawStylusSystemGestureInputReport> InteractionDetected;
 
        #endregion
 
        #region Constructor
 
        /// <summary>
        /// Initializes the interaction engine
        /// </summary>
        /// <param name="stylusDevice"></param>
        /// <param name="configuration"></param>
        internal PointerInteractionEngine(PointerStylusDevice stylusDevice, List<UnsafeNativeMethods.INTERACTION_CONTEXT_CONFIGURATION> configuration = null)
        {
            _stylusDevice = stylusDevice;
 
            // Only create a flick engine for Pen devices
            if (_stylusDevice.TabletDevice.Type == TabletDeviceType.Stylus)
            {
                // Currently disabled pending decision about flick support in Windows 10 RS3
                //_flickEngine = new PointerFlickEngine(_stylusDevice);
            }
 
            // Create our interaction context for gesture recognition
            IntPtr interactionContext = IntPtr.Zero;
            UnsafeNativeMethods.CreateInteractionContext(out interactionContext);
            _interactionContext = interactionContext;
 
            if (configuration == null)
            {
                configuration = DefaultConfiguration;
            }
 
            if (_interactionContext != IntPtr.Zero)
            {
                // We do not want to filter specific pointers
                UnsafeNativeMethods.SetPropertyInteractionContext(_interactionContext,
                    UnsafeNativeMethods.INTERACTION_CONTEXT_PROPERTY.INTERACTION_CONTEXT_PROPERTY_FILTER_POINTERS,
                    Convert.ToUInt32(false));
 
                // Use screen measurements here as this makes certain math easier for us
                UnsafeNativeMethods.SetPropertyInteractionContext(_interactionContext,
                   UnsafeNativeMethods.INTERACTION_CONTEXT_PROPERTY.INTERACTION_CONTEXT_PROPERTY_MEASUREMENT_UNITS,
                   (UInt32)UnsafeNativeMethods.InteractionMeasurementUnits.Screen);
 
                // Configure the context
                UnsafeNativeMethods.SetInteractionConfigurationInteractionContext(_interactionContext, (uint)configuration.Count, configuration.ToArray());
 
                // Store the delegate so it can be accessed over time
                _callbackDelegate = Callback;
 
                // Register for interaction notifications
                UnsafeNativeMethods.RegisterOutputCallbackInteractionContext(_interactionContext, _callbackDelegate);
            }
        }
 
        #endregion
 
        #region IDisposable Support
 
        private bool _disposed = false;
 
        /// <summary>
        /// Destroy native resources
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                // We must destroy the interaction context when done
                if (_interactionContext != IntPtr.Zero)
                {
                    UnsafeNativeMethods.DestroyInteractionContext(_interactionContext);
                    _interactionContext = IntPtr.Zero;
                }
 
                _disposed = true;
            }
        }
 
        ~PointerInteractionEngine()
        {
            Dispose(false);
        }
 
        public void Dispose()
        {
            Dispose(true);
 
            GC.SuppressFinalize(this);
        }
 
        #endregion
 
        #region Message Processing
 
        /// <summary>
        /// Update the interaction context with the latest pointer input
        /// </summary>
        /// <param name="rsir">The raw stylus input</param>
        internal void Update(RawStylusInputReport rsir)
        {
            try
            {
                // Queue up the latest message for processing
                UnsafeNativeMethods.BufferPointerPacketsInteractionContext(_interactionContext, 1, new UnsafeNativeMethods.POINTER_INFO[] { _stylusDevice.CurrentPointerInfo });
 
                // Hover processing should occur directly from message receipt.
                // Do this prior to the IC engine processing so HoverEnter/Leave has priority.
                DetectHover();
 
                // This should be removed if flicks become unsupported
                DetectFlick(rsir);
 
                // Fire processing of the queued messages
                UnsafeNativeMethods.ProcessBufferedPacketsInteractionContext(_interactionContext);
            }
            catch
            {
            }
        }
 
        #endregion
 
        #region Callback Function
 
        /// <summary>
        /// Processes raw interaction output into gesture messages for the stack callback
        /// </summary>
        /// <param name="clientData">Unused, the interaction context pointer</param>
        /// <param name="output">The interaction output</param>
        private void Callback(IntPtr clientData, ref UnsafeNativeMethods.INTERACTION_CONTEXT_OUTPUT output)
        {
            SystemGesture gesture = SystemGesture.None;
 
            // Create the appropriate gesture based on interaction output
            switch (output.interactionId)
            {
                case UnsafeNativeMethods.INTERACTION_ID.INTERACTION_ID_TAP:
                    {
                        gesture = SystemGesture.Tap;
                    }
                    break;
                case UnsafeNativeMethods.INTERACTION_ID.INTERACTION_ID_SECONDARY_TAP:
                    {
                        gesture = SystemGesture.RightTap;
                    }
                    break;
                case UnsafeNativeMethods.INTERACTION_ID.INTERACTION_ID_HOLD:
                    {
                        _firedHold = true;
 
                        if (output.interactionFlags.HasFlag(UnsafeNativeMethods.INTERACTION_FLAGS.INTERACTION_FLAG_BEGIN))
                        {
                            gesture = SystemGesture.HoldEnter;
                        }
                        else
                        {
                            gesture = SystemGesture.HoldLeave;
                        }
                    }
                    break;
                case UnsafeNativeMethods.INTERACTION_ID.INTERACTION_ID_MANIPULATION:
                    {
                        gesture = DetectDragOrFlick(output);
                    }
                    break;
            }
 
            if (gesture != SystemGesture.None)
            {
                InteractionDetected?.Invoke(this,
                    new RawStylusSystemGestureInputReport(
                        InputMode.Foreground,
                        Environment.TickCount,
                        _stylusDevice.CriticalActiveSource,
                        (Func<StylusPointDescription>)null,
                        -1,
                        -1,
                        gesture,
                        Convert.ToInt32(output.x),
                        Convert.ToInt32(output.y),
                        0));
            }
        }
 
        #endregion
 
        #region Interaction Detection Functions       
 
        /// <summary>
        /// Updates and forwards flick engine results as a gesture
        /// </summary>
        /// <remarks>
        /// Remove this if flicks no longer supported
        /// </remarks>
        private void DetectFlick(RawStylusInputReport rsir)
        {
            // Make sure the flick engine has the latest data if applicable.
            _flickEngine?.Update(rsir);
 
            // If we have received an up then we should check if
            // we can still be a flick.  If this is true, then fire a flick.
            if (rsir.Actions == RawStylusActions.Up
                && (_flickEngine?.Result?.CanBeFlick ?? false))
            {
                // 
 
                InteractionDetected?.Invoke(this,
                    new RawStylusSystemGestureInputReport(
                        InputMode.Foreground,
                        Environment.TickCount,
                        _stylusDevice.CriticalActiveSource,
                        (Func<StylusPointDescription>)null,
                        -1,
                        -1,
                        SystemGesture.Flick,
                        Convert.ToInt32(_flickEngine.Result.TabletStart.X),
                        Convert.ToInt32(_flickEngine.Result.TabletStart.Y),
                        0));
 
                _firedFlick = true;
            }
        }
 
        /// <summary>
        /// Detects a hover and forwards it as a system gesture.
        /// </summary>
        private void DetectHover()
        {
            // Hover only applies to Pen
            if (_stylusDevice.TabletDevice.Type == TabletDeviceType.Stylus)
            {
                SystemGesture gesture = SystemGesture.None;
 
                if (_stylusDevice.IsNew)
                {
                    // Any new stylus should automatically await hover
                    _hoverState = HoverState.AwaitingHover;
                }
 
                switch (_hoverState)
                {
                    case HoverState.AwaitingHover:
                        {
                            if (_stylusDevice.InAir)
                            {
                                // If we see an InAir while awaiting, start timing
                                _hoverStartTicks = _stylusDevice.TimeStamp;
                                _hoverState = HoverState.TimingHover;
                            }
                        }
                        break;
                    case HoverState.TimingHover:
                        {
                            if (_stylusDevice.InAir)
                            {
                                if (_stylusDevice.TimeStamp < _hoverStartTicks)
                                {
                                    // We looped over in ticks, just retry using the new ticks as the start.
                                    // At worst this doubles the hover time, but it is rare and simplifies logic.
                                    _hoverStartTicks = _stylusDevice.TimeStamp;
                                }
                                else if (_stylusDevice.TimeStamp - _hoverStartTicks > HoverActivationThresholdTicks)
                                {
                                    // We should now activate a hover, so send HoverEnter and switch state
                                    gesture = SystemGesture.HoverEnter;
                                    _hoverState = HoverState.InHover;
                                }
                            }
                            else if (_stylusDevice.IsDown)
                            {
                                // The device is no longer in air so cancel
                                _hoverState = HoverState.HoverCancelled;
                            }
                        }
                        break;
                    case HoverState.HoverCancelled:
                        {
                            if (_stylusDevice.InAir)
                            {
                                // If we are back in air post cancellation, we might need to trigger another hover
                                // so restart the state machine
                                _hoverState = HoverState.AwaitingHover;
                            }
                        }
                        break;
                    case HoverState.InHover:
                        {
                            if (_stylusDevice.IsDown || !_stylusDevice.InRange)
                            {
                                // We are cancelling a hover so send HoverLeave and switch state.
                                gesture = SystemGesture.HoverLeave;
                                _hoverState = HoverState.HoverCancelled;
                            }
                        }
                        break;
                }
 
                if (gesture != SystemGesture.None)
                {
                    InteractionDetected?.Invoke(this,
                        new RawStylusSystemGestureInputReport(
                            InputMode.Foreground,
                            Environment.TickCount,
                            _stylusDevice.CriticalActiveSource,
                            (Func<StylusPointDescription>)null,
                            -1,
                            -1,
                            gesture,
                            Convert.ToInt32(_stylusDevice.RawStylusPoint.X),
                            Convert.ToInt32(_stylusDevice.RawStylusPoint.Y),
                            0));
                }
            }
        }
 
        /// <summary>
        /// If flicks are removed, clean up this code
        /// Detect a flick or a drag and send the appropriate gesture.  Flicks always
        /// take precedence over drags as a flick is basically a very fast drag/release.
        /// </summary>
        private SystemGesture DetectDragOrFlick(UnsafeNativeMethods.INTERACTION_CONTEXT_OUTPUT output)
        {
            SystemGesture gesture = SystemGesture.None;
 
            if (output.interactionFlags.HasFlag(UnsafeNativeMethods.INTERACTION_FLAGS.INTERACTION_FLAG_END))
            {
                // At the end of an interaction, any drag/flick/hold state is no longer needed
                _firedDrag = false;
                _firedHold = false;
                _firedFlick = false;
            }
            else
            {
                // If we have not already fired a drag/flick and we cannot be a flick
                if (!_firedDrag && !_firedFlick
                    && (!_flickEngine?.Result?.CanBeFlick ?? true))
                {
                    // Convert screen pixels to inches using current DPI
                    DpiScale dpi = VisualTreeHelper.GetDpi(_stylusDevice.CriticalActiveSource.RootVisual);
 
                    double xChangeInches = output.arguments.manipulation.cumulative.translationX / dpi.PixelsPerInchX;
                    double yChangeInches = output.arguments.manipulation.cumulative.translationY / dpi.PixelsPerInchY;
 
                    // If the cumulative change is greater than our threshold 
                    // (taken from WISP, converted to inches) then fire a drag.
                    if (xChangeInches > DragThresholdInches || yChangeInches > DragThresholdInches)
                    {
                        // If we have a current hold being tracked, convert to right drag
                        gesture = (_firedHold) ? SystemGesture.RightDrag : SystemGesture.Drag;
 
                        // This pointer has seen a drag
                        _firedDrag = true;
                    }
                }
            }
 
            return gesture;
        }
 
        #endregion
    }
}