File: System\Windows\Input\ManipulationLogic.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 System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Security;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Input.Manipulations;
using System.Windows.Media;
using System.Windows.Threading;
using MS.Win32;
using MS.Internal;
using MS.Internal.PresentationCore;
 
namespace System.Windows.Input
{
    /// <summary>
    ///     Handles detection of manipulations.
    /// </summary>
    internal sealed class ManipulationLogic
    {
        /// <summary>
        ///     Instantiates an instance of this class.
        /// </summary>
        internal ManipulationLogic(ManipulationDevice manipulationDevice)
        {
            _manipulationDevice = manipulationDevice;
        }
 
        /// <summary>
        ///     Hooked up to the manipulation processor and inertia processor's started event.
        /// </summary>
        private void OnManipulationStarted(object sender, Manipulation2DStartedEventArgs e)
        {
            PushEvent(new ManipulationStartedEventArgs(
                _manipulationDevice, 
                LastTimestamp, 
                _currentContainer, 
                new Point(e.OriginX, e.OriginY)));
        }
 
        /// <summary>
        ///     Hooked up to the manipulation processor and inertia processor's delta event.
        /// </summary>
        private void OnManipulationDelta(object sender, Manipulation2DDeltaEventArgs e)
        {
            var deltaArguments = new ManipulationDeltaEventArgs(
                _manipulationDevice,
                LastTimestamp,
                _currentContainer,
                new Point(e.OriginX, e.OriginY),
                ConvertDelta(e.Delta, null),
                ConvertDelta(e.Cumulative, _lastManipulationBeforeInertia),
                ConvertVelocities(e.Velocities),
                IsInertiaActive);
 
            PushEvent(deltaArguments);
        }
 
        /// <summary>
        ///     Hooked up to the manipulation processor's completed event.
        /// </summary>
        private void OnManipulationCompleted(object sender, Manipulation2DCompletedEventArgs e)
        {
            // Manipulation portion completed.
 
            if (_manualComplete && !_manualCompleteWithInertia)
            {
                // This is the last event in the sequence.
 
                ManipulationCompletedEventArgs completedArguments = ConvertCompletedArguments(e);
                RaiseManipulationCompleted(completedArguments);
            }
            else
            {
                // This event will configure inertia, which will start after this event.
 
                _lastManipulationBeforeInertia = ConvertDelta(e.Total, null);
 
                ManipulationInertiaStartingEventArgs inertiaArguments = new ManipulationInertiaStartingEventArgs(
                    _manipulationDevice,
                    LastTimestamp,
                    _currentContainer,
                    new Point(e.OriginX, e.OriginY),
                    ConvertVelocities(e.Velocities),
                    false);
 
                PushEvent(inertiaArguments);
            }
 
            _manipulationProcessor = null;
        }
 
        /// <summary>
        ///     Hooked up to the inertia processor's completed event.
        /// </summary>
        private void OnInertiaCompleted(object sender, Manipulation2DCompletedEventArgs e)
        {
            // Inertia portion completed.
 
            ClearTimer();
 
            if (_manualComplete && _manualCompleteWithInertia)
            {
                // Another inertia portion was requested
 
                _lastManipulationBeforeInertia = ConvertDelta(e.Total, _lastManipulationBeforeInertia);
 
                ManipulationInertiaStartingEventArgs inertiaArguments = new ManipulationInertiaStartingEventArgs(
                    _manipulationDevice,
                    LastTimestamp,
                    _currentContainer,
                    new Point(e.OriginX, e.OriginY),
                    ConvertVelocities(e.Velocities),
                    true);
 
                PushEvent(inertiaArguments);
            }
            else
            {
                // This is the last event in the sequence.
 
                ManipulationCompletedEventArgs completedArguments = ConvertCompletedArguments(e);
 
                RaiseManipulationCompleted(completedArguments);
            }
 
            _inertiaProcessor = null;
        }
 
        private void RaiseManipulationCompleted(ManipulationCompletedEventArgs e)
        {
            PushEvent(e);
        }
 
        /// <summary>
        ///     Called after a Completed event has been processed.
        /// </summary>
        internal void OnCompleted()
        {
            _lastManipulationBeforeInertia = null;
            SetContainer(null);
        }
 
        /// <summary>
        ///     Converts an Affine2DOperationCompletedEventArgs object into a ManipulationCompletedEventArgs object.
        /// </summary>
        private ManipulationCompletedEventArgs ConvertCompletedArguments(Manipulation2DCompletedEventArgs e)
        {
            return new ManipulationCompletedEventArgs(
                _manipulationDevice,
                LastTimestamp,
                _currentContainer,
                new Point(e.OriginX, e.OriginY),
                ConvertDelta(e.Total, _lastManipulationBeforeInertia),
                ConvertVelocities(e.Velocities),
                IsInertiaActive);
        }
 
        private static ManipulationDelta ConvertDelta(ManipulationDelta2D delta, ManipulationDelta add)
        {
            if (add != null)
            {
                return new ManipulationDelta(
                    new Vector(delta.TranslationX + add.Translation.X, delta.TranslationY + add.Translation.Y),
                    AngleUtil.RadiansToDegrees(delta.Rotation) + add.Rotation,
                    new Vector(delta.ScaleX * add.Scale.X, delta.ScaleY * add.Scale.Y),
                    new Vector(delta.ExpansionX + add.Expansion.X, delta.ExpansionY + add.Expansion.Y));
            }
            else
            {
                return new ManipulationDelta(
                    new Vector(delta.TranslationX, delta.TranslationY),
                    AngleUtil.RadiansToDegrees(delta.Rotation),
                    new Vector(delta.ScaleX, delta.ScaleY),
                    new Vector(delta.ExpansionX, delta.ExpansionY));
            }
        }
 
        private static ManipulationVelocities ConvertVelocities(ManipulationVelocities2D velocities)
        {
            return new ManipulationVelocities(
                new Vector(velocities.LinearVelocityX, velocities.LinearVelocityY),
                AngleUtil.RadiansToDegrees(velocities.AngularVelocity),
                new Vector(velocities.ExpansionVelocityX, velocities.ExpansionVelocityY));
        }
 
        /// <summary>
        ///     Completes any pending manipulation or inerita processing.
        /// </summary>
        /// <param name="withInertia">
        ///     If a manipulation is active, specifies whether to continue
        ///     to an inertia phase (true) or simply end the sequence (true).
        /// </param>
        internal void Complete(bool withInertia)
        {
            try
            {
                _manualComplete = true;
                _manualCompleteWithInertia = withInertia;
 
                if (IsManipulationActive)
                {
                    _manipulationProcessor.CompleteManipulation(GetCurrentTimestamp());
                }
                else if (IsInertiaActive)
                {
                    _inertiaProcessor.Complete(GetCurrentTimestamp());
                }
            }
            finally
            {
                _manualComplete = false;
                _manualCompleteWithInertia = false;
            }
        }
 
        /// <summary>
        ///     Gets ManipulationCompletedEventArgs object out of ManipulationInertiaStartingEventArgs
        /// </summary>
        private ManipulationCompletedEventArgs GetManipulationCompletedArguments(ManipulationInertiaStartingEventArgs e)
        {
            Debug.Assert(_lastManipulationBeforeInertia != null);
            return new ManipulationCompletedEventArgs(
                _manipulationDevice,
                LastTimestamp,
                _currentContainer,
                new Point(e.ManipulationOrigin.X, e.ManipulationOrigin.Y),
                _lastManipulationBeforeInertia,
                e.InitialVelocities,
                IsInertiaActive);
        }
 
        /// <summary>
        ///     Starts the inertia phase based on the results of a ManipulationInertiaStarting event.
        /// </summary>
        internal void BeginInertia(ManipulationInertiaStartingEventArgs e)
        {
            if (e.CanBeginInertia())
            {
                _inertiaProcessor = new InertiaProcessor2D();
                _inertiaProcessor.Delta += OnManipulationDelta;
                _inertiaProcessor.Completed += OnInertiaCompleted;
 
                e.ApplyParameters(_inertiaProcessor);
 
                // Setup a timer to tick the inertia to completion
                _inertiaTimer = new DispatcherTimer();
                _inertiaTimer.Interval = TimeSpan.FromMilliseconds(15);
                _inertiaTimer.Tick += new EventHandler(OnInertiaTick);
                _inertiaTimer.Start();
            }
            else
            {
                // This is the last event in the sequence.
                ManipulationCompletedEventArgs completedArguments = GetManipulationCompletedArguments(e);
                RaiseManipulationCompleted(completedArguments);
                PushEventsToDevice();
            }
        }
 
        internal static Int64 GetCurrentTimestamp()
        {
            // Does QueryPerformanceCounter to get the current time in 100ns units
            return MediaContext.CurrentTicks;
        }
 
        private void OnInertiaTick(object sender, EventArgs e)
        {
            // Tick the inertia
            if (IsInertiaActive)
            {
                if (!_inertiaProcessor.Process(GetCurrentTimestamp()))
                {
                    ClearTimer();
                }
 
                PushEventsToDevice();
            }
            else
            {
                ClearTimer();
            }
        }
 
        private void ClearTimer()
        {
            if (_inertiaTimer != null)
            {
                _inertiaTimer.Stop();
                _inertiaTimer = null;
            }
        }
 
        /// <summary>
        ///     Prepares and raises a manipulation event.
        /// </summary>
        private void PushEvent(InputEventArgs e)
        {
            // We only expect to generate one event at a time and should never need a queue.
            Debug.Assert(_generatedEvent == null, "There is already a generated event waiting to be pushed.");
            _generatedEvent = e;
        }
 
        /// <summary>
        ///     Pushes generated events to the inertia input provider.
        /// </summary>
        internal void PushEventsToDevice()
        {
            if (_generatedEvent != null)
            {
                InputEventArgs generatedEvent = _generatedEvent;
                _generatedEvent = null;
                _manipulationDevice.ProcessManipulationInput(generatedEvent);
            }
        }
 
        /// <summary>
        ///     Raises ManipulationBoundaryFeedback to allow handlers to provide feedback that manipulation has hit an edge.
        /// </summary>
        /// <param name="unusedManipulation">The total unused manipulation.</param>
        internal void RaiseBoundaryFeedback(ManipulationDelta unusedManipulation, bool requestedComplete)
        {
            bool hasUnusedManipulation = (unusedManipulation != null);
            if ((!hasUnusedManipulation || requestedComplete) && HasPendingBoundaryFeedback)
            {
                // Create a "zero" message to end currently pending feedback
                unusedManipulation = new ManipulationDelta(new Vector(), 0.0, new Vector(1.0, 1.0), new Vector());
                HasPendingBoundaryFeedback = false;
            }
            else if (hasUnusedManipulation)
            {
                HasPendingBoundaryFeedback = true;
            }
 
            if (unusedManipulation != null)
            {
                PushEvent(new ManipulationBoundaryFeedbackEventArgs(_manipulationDevice, LastTimestamp, _currentContainer, unusedManipulation));
            }
        }
 
        private bool HasPendingBoundaryFeedback
        {
            get;
            set;
        }
 
        private int LastTimestamp
        {
            get;
            set;
        }
 
        internal void ReportFrame(ICollection<IManipulator> manipulators)
        {
            Int64 timestamp = GetCurrentTimestamp();
 
            // InputEventArgs timestamps are Int32 while the processors take Int64
            // GetMessageTime() is used for all other InputEventArgs, such as mouse and keyboard input.
            // And it does not match QueryPerformanceCounter(), my experiments show GetMessageTime() is ~ 120ms ahead.
            LastTimestamp = SafeNativeMethods.GetMessageTime(); 
 
            int numManipulators = manipulators.Count;
            if (IsInertiaActive && (numManipulators > 0))
            {
                // Inertia is active but now there are fingers, stop inertia
                _inertiaProcessor.Complete(timestamp);
                PushEventsToDevice();
            }
 
            if (!IsManipulationActive && (numManipulators > 0))
            {
                // Time to start a new manipulation
 
                ManipulationStartingEventArgs startingArgs = RaiseStarting();
                if (!startingArgs.RequestedCancel && (startingArgs.Mode != ManipulationModes.None))
                {
                    // Determine if we allow single-finger manipulation
                    if (startingArgs.IsSingleTouchEnabled || (numManipulators >= 2))
                    {
                        SetContainer(startingArgs.ManipulationContainer);
                        _mode = startingArgs.Mode;
                        _pivot = startingArgs.Pivot;
                        IList<ManipulationParameters2D> parameters = startingArgs.Parameters;
 
                        _manipulationProcessor = new ManipulationProcessor2D(ConvertMode(_mode), ConvertPivot(_pivot));
 
                        if (parameters != null)
                        {
                            int count = parameters.Count;
                            for (int i = 0; i < parameters.Count; i++)
                            {
                                _manipulationProcessor.SetParameters(parameters[i]);
                            }
                        }
 
                        _manipulationProcessor.Started += OnManipulationStarted;
                        _manipulationProcessor.Delta += OnManipulationDelta;
                        _manipulationProcessor.Completed += OnManipulationCompleted;
 
                        _currentManipulators.Clear();
                    }
                }
            }
 
            if (IsManipulationActive)
            {
                // A manipulation process is available to process this frame of manipulators
                UpdateManipulators(manipulators);
                _manipulationProcessor.ProcessManipulators(timestamp, CurrentManipulators);
                PushEventsToDevice();
            }
        }
 
        private ManipulationStartingEventArgs RaiseStarting()
        {
            ManipulationStartingEventArgs starting = new ManipulationStartingEventArgs(_manipulationDevice, Environment.TickCount);
            starting.ManipulationContainer = _manipulationDevice.Target;
 
            _manipulationDevice.ProcessManipulationInput(starting);
 
            return starting;
        }
 
        internal IInputElement ManipulationContainer
        {
            get { return _currentContainer; }
            set
            {
                // If a manipulation is in progress, we should consider creating some form 
                // of transition between the old coordinate space and the 
                // new one. At the very least, the old processor needs to 
                // stop and a new one needs to start.
                SetContainer(value);
            }
        }
 
        internal ManipulationModes ManipulationMode
        {
            get { return _mode; }
            set
            {
                _mode = value;
                if (_manipulationProcessor != null)
                {
                    _manipulationProcessor.SupportedManipulations = ConvertMode(_mode);
                }
            }
        }
 
        private static Manipulations2D ConvertMode(ManipulationModes mode)
        {
            Manipulations2D manipulations = Manipulations2D.None;
 
            if ((mode & ManipulationModes.TranslateX) != 0)
            {
                manipulations |= Manipulations2D.TranslateX;
            }
 
            if ((mode & ManipulationModes.TranslateY) != 0)
            {
                manipulations |= Manipulations2D.TranslateY;
            }
 
            if ((mode & ManipulationModes.Scale) != 0)
            {
                manipulations |= Manipulations2D.Scale;
            }
 
            if ((mode & ManipulationModes.Rotate) != 0)
            {
                manipulations |= Manipulations2D.Rotate;
            }
 
            return manipulations;
        }
 
        internal ManipulationPivot ManipulationPivot
        {
            get { return _pivot; }
            set
            {
                _pivot = value;
                if (_manipulationProcessor != null)
                {
                    _manipulationProcessor.Pivot = ConvertPivot(value);
                }
            }
        }
 
        private static ManipulationPivot2D ConvertPivot(ManipulationPivot pivot)
        {
            if (pivot != null)
            {
                Point center = pivot.Center;
                return new ManipulationPivot2D()
                {
                    X = (float)center.X,
                    Y = (float)center.Y,
                    Radius = (float)Math.Max(1.0, pivot.Radius)
                };
            }
 
            return null;
        }
 
        internal void SetManipulationParameters(ManipulationParameters2D parameter)
        {
            if (_manipulationProcessor != null)
            {
                _manipulationProcessor.SetParameters(parameter);
            }
        }
 
        private void UpdateManipulators(ICollection<IManipulator> updatedManipulators)
        {
            // Clear out the old removed collection and use it to store
            // the new current collection. The old current collection
            // will be used to generate the new removed collection.
            _removedManipulators.Clear();
            var temp = _removedManipulators;
            _removedManipulators = _currentManipulators;
            _currentManipulators = temp;
 
            // End the manipulation if the element is not
            // visible anymore
            UIElement uie = _currentContainer as UIElement;
            if (uie != null)
            {
                if (!uie.IsVisible)
                {
                    return;
                }
            }
            else
            {
                UIElement3D uie3D = _currentContainer as UIElement3D;
                if (uie3D != null &&
                    !uie3D.IsVisible)
                {
                    return;
                }
            }
 
            // For each updated manipulator, convert it to the correct format in the
            // current collection and remove it from the removed collection. What is left
            // in the removed collection will be the manipulators that were removed.
            foreach (IManipulator updatedManipulator in updatedManipulators)
            {
                // consider making these Ids unique across devices
                int id = updatedManipulator.Id;
                _removedManipulators.Remove(id); // This manipulator was not removed
                Point position = updatedManipulator.GetPosition(_currentContainer);
                position = _manipulationDevice.GetTransformedManipulatorPosition(position);
                _currentManipulators[id] = new Manipulator2D(id, (float)position.X, (float)position.Y);
            }
        }
 
        private void SetContainer(IInputElement newContainer)
        {
            // unsubscribe from LayoutUpdated
            UnsubscribeFromLayoutUpdated();
 
            // clear cached values
            _containerPivotPoint = new Point();
            _containerSize = new Size();
            _root = null;
 
            // remember the new container
            _currentContainer = newContainer;
 
            if (newContainer != null)
            {
                // get the new root
                PresentationSource presentationSource = PresentationSource.CriticalFromVisual((Visual)newContainer);
                if (presentationSource != null)
                {
                    _root = presentationSource.RootVisual as UIElement;
                }
 
                // subscribe to LayoutUpdated
                if (_containerLayoutUpdated != null)
                {
                    SubscribeToLayoutUpdated();
                }
            }
        }
 
        internal event EventHandler<EventArgs> ContainerLayoutUpdated
        {
            add
            {
                bool wasNull = _containerLayoutUpdated == null;
                _containerLayoutUpdated += value;
 
                // if this is the first handler, try to subscribe to LayoutUpdated event
                if (wasNull && _containerLayoutUpdated != null)
                {
                    SubscribeToLayoutUpdated();
                }
            }
            remove
            {
                bool wasNull = _containerLayoutUpdated == null;
                _containerLayoutUpdated -= value;
 
                // if this is the last handler, unsubscribe from LayoutUpdated event
                if (!wasNull && _containerLayoutUpdated == null)
                {
                    UnsubscribeFromLayoutUpdated();
                }
            }
        }
 
        private void SubscribeToLayoutUpdated()
        {
            UIElement container = _currentContainer as UIElement;
            if (container != null)
            {
                container.LayoutUpdated += OnLayoutUpdated;
            }
        }
 
        private void UnsubscribeFromLayoutUpdated()
        {
            UIElement container = _currentContainer as UIElement;
            if (container != null)
            {
                container.LayoutUpdated -= OnLayoutUpdated;
            }
        }
 
        /// <summary>
        /// OnLayoutUpdated handler, raises ContainerLayoutUpdated event if container's position or size have been changed 
        /// since the last LayoutUpdate.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnLayoutUpdated(object sender, EventArgs e)
        {
            Debug.Assert(_containerLayoutUpdated != null);
 
            //check position and size and update the cached values
            if (UpdateCachedPositionAndSize())
            {
                _containerLayoutUpdated(this, EventArgs.Empty);
            }
        }
 
        private bool UpdateCachedPositionAndSize()
        {
            // Determine if the manipulation needs to be updated because of position or size change.
            // * Size change is detected by comparing RenderSize
            // * Position change is detected by translating PivotPoint to the element coordinate, in general
            // this is not accurate because rotation over PivotPoint won't be detected but the PivotPoint is selected far outside
            // of the Window bounds, so practically that should be a very rare case.
            // The more accurate solution would require 2 or 3 points which is more expensive.
            if (_root == null)
            {
                return false;
            }
 
            UIElement container = _currentContainer as UIElement;
            if (container == null)
            {
                return false;
            }
 
            Size renderSize = container.RenderSize;
            Point translatedPivotPoint = _root.TranslatePoint(LayoutUpdateDetectionPivotPoint, container);
 
            bool changed = (!DoubleUtil.AreClose(renderSize, _containerSize) ||
                            !DoubleUtil.AreClose(translatedPivotPoint, _containerPivotPoint));
            if (changed)
            {
                // update cached values
                _containerSize = renderSize;
                _containerPivotPoint = translatedPivotPoint;
            }
 
            return changed;
        }
 
        private IEnumerable<Manipulator2D> CurrentManipulators
        {
            get { return (_currentManipulators.Count > 0) ? _currentManipulators.Values : null; }
        }
 
        internal bool IsManipulationActive
        {
            get { return _manipulationProcessor != null; }
        }
 
        private bool IsInertiaActive
        {
            get { return _inertiaProcessor != null; }
        }
 
        private ManipulationDevice _manipulationDevice;
 
        private IInputElement _currentContainer;
        private ManipulationPivot _pivot;
        private ManipulationModes _mode;
 
        private ManipulationProcessor2D _manipulationProcessor;
        private InertiaProcessor2D _inertiaProcessor;
 
        // A list of manipulators that are currently active (i.e. fingers touching the screen)
        private Dictionary<int, Manipulator2D> _currentManipulators = new Dictionary<int, Manipulator2D>(2);
 
        // A list of manipulators that have been removed (stored to avoid allocating each frame)
        private Dictionary<int, Manipulator2D> _removedManipulators = new Dictionary<int, Manipulator2D>(2);
 
        // When inertia starts, its values are relative to the end point specified in
        // this event. WPF's API wants to expose inertia deltas relative to the first
        // Started event. This Completed event provides enough information to convert
        // the delta values so that they are relative to the Started event.
        private ManipulationDelta _lastManipulationBeforeInertia;
 
        private InputEventArgs _generatedEvent;
 
        private DispatcherTimer _inertiaTimer;
 
        private bool _manualComplete;
        private bool _manualCompleteWithInertia;
 
        private EventHandler<EventArgs> _containerLayoutUpdated;
 
        // pivot point to detect position and size change, see UpdateCachedPositionAndSize for more details
        // The odd magic number is to make it more rare.
        private static readonly Point LayoutUpdateDetectionPivotPoint = new Point(-10234.1234, -10234.1234);
 
        // cached values to detect position and size change
        private Point _containerPivotPoint;
        private Size _containerSize;
        private UIElement _root;
    }
}