|
// 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.Internal.Interop;
using MS.Win32.Pointer;
using System.Runtime.InteropServices;
using System.Windows.Input;
using System.Windows.Input.StylusPointer;
using System.Windows.Media;
using System.Windows.Threading;
namespace System.Windows.Interop
{
/// <summary>
/// Implements an input provider per hwnd for WM_POINTER messages
/// </summary>
internal sealed class HwndPointerInputProvider : DispatcherObject, IStylusInputProvider
{
#region Member Variables
private bool _disposed = false;
/// <summary>
/// The HwndSource for WM_POINTER messages
/// </summary>
private readonly HwndSource _source;
/// <summary>
/// The Input site to inject messages
/// </summary>
private InputProviderSite _site;
/// <summary>
/// The current pointer logic for this thread
/// </summary>
private readonly PointerLogic _pointerLogic;
/// <summary>
/// The current stylus device we are using
/// </summary>
private PointerStylusDevice _currentStylusDevice;
/// <summary>
/// The current tablet device we are using
/// </summary>
private PointerTabletDevice _currentTabletDevice;
#endregion
#region Properties
/// <summary>
/// If the window we are associated with is currently enabled.
/// </summary>
internal bool IsWindowEnabled { get; private set; } = false;
#endregion
#region Constructor/IDisposable
/// <summary>
/// Creates a new input provider for a particular source that handles WM_POINTER messages
/// </summary>
/// <param name="source">The source to handle messages for</param>
internal HwndPointerInputProvider(HwndSource source)
{
_site = InputManager.Current.RegisterInputProvider(this);
_source = source;
_pointerLogic = StylusLogic.GetCurrentStylusLogicAs<PointerLogic>();
// Register the stylus plugin manager
_pointerLogic.PlugInManagers[_source] = new PointerStylusPlugInManager(_source);
// Store if this window is enabled or disabled
int style = MS.Win32.UnsafeNativeMethods.GetWindowLong(new HandleRef(this, source.Handle), MS.Win32.NativeMethods.GWL_STYLE);
IsWindowEnabled = (style & MS.Win32.NativeMethods.WS_DISABLED) == 0;
}
~HwndPointerInputProvider()
{
Dispose(false);
}
/// <summary>
/// Clean up any held resources
/// </summary>
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_site?.Dispose();
_site = null;
_pointerLogic.PlugInManagers.Remove(_source);
}
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#region Pointer Data Extraction
/// <summary>
/// Extracts the pointer id
/// </summary>
/// <param name="wParam">The parameter containing the id</param>
/// <returns>The pointer id</returns>
private uint GetPointerId(IntPtr wParam)
{
return (uint)MS.Win32.NativeMethods.SignedLOWORD(wParam);
}
/// <summary>
/// Creates raw stylus data from the raw WM_POINTER properties
/// </summary>
/// <param name="pointerData">The current pointer info</param>
/// <param name="tabletDevice">The current TabletDevice</param>
/// <returns>An array of raw pointer data</returns>
private int[] GenerateRawStylusData(PointerData pointerData, PointerTabletDevice tabletDevice)
{
// Since we are copying raw pointer data, we want to use every property supported by this pointer.
// We may never access some of the unknown (unsupported by WPF) properties, but they should be there
// for consumption by the developer.
int pointerPropertyCount = tabletDevice.DeviceInfo.SupportedPointerProperties.Length;
// The data is as wide as the pointer properties and is per history point
int[] rawPointerData = new int[pointerPropertyCount * pointerData.Info.historyCount];
int[] data = Array.Empty<int>();
// Get the raw data formatted to our supported properties
if (UnsafeNativeMethods.GetRawPointerDeviceData(
pointerData.Info.pointerId,
pointerData.Info.historyCount,
(uint)pointerPropertyCount,
tabletDevice.DeviceInfo.SupportedPointerProperties,
rawPointerData))
{
// Get the X and Y offsets to translate device coords to the origin of the hwnd
int originOffsetX, originOffsetY;
GetOriginOffsetsLogical(out originOffsetX, out originOffsetY);
int numButtons = tabletDevice.DeviceInfo.SupportedPointerProperties.Length - tabletDevice.DeviceInfo.SupportedButtonPropertyIndex;
int rawDataPointSize = (numButtons > 0) ? pointerPropertyCount - numButtons + 1 : pointerPropertyCount;
// Instead of a single entry for each button we use one entry for all buttons so reflect that in the raw data size
data = new int[rawDataPointSize * pointerData.Info.historyCount];
// Skip to the beginning of each stylus point in both the target WPF array and the pointer data array.
// The pointer data is arranged from last point to first point in the history while WPF data is arranged
// the reverse of this (in whole stylus points). Therefore we need to fill backward from pointer data
// via stylus point strides.
for (int i = 0, j = rawPointerData.Length - pointerPropertyCount; i < data.Length; i += rawDataPointSize, j -= pointerPropertyCount)
{
Array.Copy(rawPointerData, j, data, i, rawDataPointSize);
// Apply offsets from the origin to raw pointer data here
data[i + StylusPointDescription.RequiredXIndex] -= originOffsetX;
data[i + StylusPointDescription.RequiredYIndex] -= originOffsetY;
if (numButtons > 0)
{
int buttonIndex = i + rawDataPointSize - 1;
// The last data point probably has garbage in it, so clear it to store button info
data[buttonIndex] = 0;
// Condense any leftover button properties into a single entry
for (int k = tabletDevice.DeviceInfo.SupportedButtonPropertyIndex; k < pointerPropertyCount; k++)
{
int mask = rawPointerData[j + k] << (k - tabletDevice.DeviceInfo.SupportedButtonPropertyIndex);
data[buttonIndex] |= mask;
}
}
}
}
return data;
}
#endregion
#region Stylus Event Firing
/// <summary>
/// Processes the latest WM_POINTER message and forwards it to the WPF input stack.
/// </summary>
/// <param name="pointerId">The id of the pointer message</param>
/// <param name="action">The stylus action being done</param>
/// <param name="timestamp">The time (in ticks) the message arrived</param>
/// <returns>True if successfully processed (handled), false otherwise</returns>
private bool ProcessMessage(uint pointerId, RawStylusActions action, int timestamp)
{
bool handled = false;
// Acquire all pointer data needed
PointerData data = new PointerData(pointerId);
// Only process touch or pen messages, do not process mouse or touchpad
if (data.IsValid
&& (data.Info.pointerType == UnsafeNativeMethods.POINTER_INPUT_TYPE.PT_TOUCH
|| data.Info.pointerType == UnsafeNativeMethods.POINTER_INPUT_TYPE.PT_PEN))
{
uint cursorId = 0;
if (UnsafeNativeMethods.GetPointerCursorId(pointerId, ref cursorId))
{
IntPtr deviceId = data.Info.sourceDevice;
// If we cannot acquire the latest tablet and stylus then wait for the
// next message.
if (!UpdateCurrentTabletAndStylus(deviceId, cursorId))
{
return false;
}
// Convert move to InAirMove if applicable
if (action == RawStylusActions.Move
&& (!data.Info.pointerFlags.HasFlag(UnsafeNativeMethods.POINTER_FLAGS.POINTER_FLAG_INCONTACT)
&& data.Info.pointerFlags.HasFlag(UnsafeNativeMethods.POINTER_FLAGS.POINTER_FLAG_INRANGE)))
{
action = RawStylusActions.InAirMove;
}
// Generate a raw input to send to the input manager to start the event chain in PointerLogic
RawStylusInputReport rsir =
new RawStylusInputReport(
InputMode.Foreground,
timestamp,
_source,
action,
() => { return _currentTabletDevice.StylusPointDescription; },
_currentTabletDevice.Id,
_currentStylusDevice.Id,
GenerateRawStylusData(data, _currentTabletDevice))
{
StylusDevice = _currentStylusDevice.StylusDevice,
};
// Send the input report to the stylus plugins if we're not doing a drag and the window
// is currently enabled.
if (!_pointerLogic.InDragDrop && IsWindowEnabled)
{
PointerStylusPlugInManager manager;
if (_pointerLogic.PlugInManagers.TryGetValue(_source, out manager))
{
manager.InvokeStylusPluginCollection(rsir);
}
}
// Update the data in the stylus device with the latest pointer data
_currentStylusDevice.Update(this, _source, data, rsir);
// Call the StylusDevice to process and fire any interactions that
// might have resulted from the input. If the originating inputs
// have been handled, we don't want to generate any gestures.
_currentStylusDevice.UpdateInteractions(rsir);
InputReportEventArgs irea = new InputReportEventArgs(_currentStylusDevice.StylusDevice, rsir)
{
RoutedEvent = InputManager.PreviewInputReportEvent,
};
// Now send the input report
InputManager.UnsecureCurrent.ProcessInput(irea);
// If this is not a primary pointer input, we don't want to
// allow it to go to DefWindowProc, so we should handle it.
// This ensures that primary pointer to mouse promotions
// will occur in multi-touch scenarios.
// We don't use the results of the input processing here as doing
// so could possibly cause some messages of a pointer chain as
// being handled, and some as being unhandled. This results in
// undefined behavior in the WM_POINTER stack.
// <see cref="https://msdn.microsoft.com/en-us/library/windows/desktop/hh454923(v=vs.85).aspx"/>
handled = !_currentStylusDevice.IsPrimary;
}
}
return handled;
}
#endregion
#region Utility
/// <summary>
/// This function uses the logical origin of the current hwnd as the offsets for
/// logical pointer coordinates.
///
/// This is needed as WISP's concept of tablet coordinates is not the entire tablet.
/// Instead, WISP transforms tablet X and Y into the tablet context. This does not
/// change the max, min, or resolution, merely translates the origin point to the hwnd
/// origin. Since the inking system in WPF was based on this raw data, we need to
/// recreate the same thing here.
///
/// See Stylus\Biblio.txt - 7
///
/// </summary>
/// <param name="originOffsetX">The X offset in logical coordinates</param>
/// <param name="originOffsetY">The Y offset in logical coordiantes</param>
private void GetOriginOffsetsLogical(out int originOffsetX, out int originOffsetY)
{
Point originScreenCoord = _source.RootVisual.PointToScreen(new Point(0, 0));
// Use the inverse of our logical tablet to screen matrix to generate tablet coords
MatrixTransform screenToTablet = new MatrixTransform(_currentTabletDevice.TabletToScreen);
screenToTablet = (MatrixTransform)screenToTablet.Inverse;
Point originTabletCoord = originScreenCoord * screenToTablet.Matrix;
originOffsetX = (int)Math.Round(originTabletCoord.X);
originOffsetY = (int)Math.Round(originTabletCoord.Y);
}
/// <summary>
/// Attempts to update the current stylus and tablet devices for the latest WM_POINTER message.
/// Will attempt retries if the tablet collection is invalid or does not contain the proper ids.
/// </summary>
/// <param name="deviceId">The id of the TabletDevice</param>
/// <param name="cursorId">The id of the StylusDevice</param>
/// <returns>True if successfully updated, false otherwise.</returns>
private bool UpdateCurrentTabletAndStylus(IntPtr deviceId, uint cursorId)
{
PointerTabletDeviceCollection tablets = Tablet.TabletDevices?.As<PointerTabletDeviceCollection>();
// We have an invalid tablet collection, we should refresh to make sure
// we have the latest.
if (!tablets.IsValid)
{
tablets.Refresh();
// If the refresh fails, we need to skip input here, nothing we can do.
// We'll try to pick up the proper state on the next WM_POINTER message.
if (!tablets.IsValid)
{
return false;
}
}
_currentTabletDevice = tablets?.GetByDeviceId(deviceId);
_currentStylusDevice = _currentTabletDevice?.GetStylusByCursorId(cursorId);
// Something went wrong when querying the tablet or stylus, attempt a refresh
if (_currentTabletDevice == null || _currentStylusDevice == null)
{
tablets.Refresh();
_currentTabletDevice = tablets?.GetByDeviceId(deviceId);
_currentStylusDevice = _currentTabletDevice?.GetStylusByCursorId(cursorId);
// Still can't get the proper devices, just wait for the next message
if (_currentTabletDevice == null || _currentStylusDevice == null)
{
return false;
}
}
return true;
}
#endregion
#region Message Filtering
/// <summary>
/// Processes the message loop for the HwndSource, filtering WM_POINTER messages where needed
/// </summary>
/// <param name="hwnd">The hwnd the message is for</param>
/// <param name="msg">The message</param>
/// <param name="wParam"></param>
/// <param name="lParam"></param>
/// <param name="handled">If this has been successfully processed</param>
/// <returns></returns>
IntPtr IStylusInputProvider.FilterMessage(IntPtr hwnd, WindowMessage msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
handled = false;
// Do not process any messages if the stack was disabled via reflection hack
if (PointerLogic.IsEnabled)
{
switch (msg)
{
case WindowMessage.WM_ENABLE:
{
IsWindowEnabled = MS.Win32.NativeMethods.IntPtrToInt32(wParam) == 1;
}
break;
case WindowMessage.WM_POINTERENTER:
{
// Enter can be processed as an InRange.
// The MSDN documentation is not correct for InRange (according to feisu)
// As such, using enter is the correct way to generate this. This is also what DirectInk uses.
handled = ProcessMessage(GetPointerId(wParam), RawStylusActions.InRange, Environment.TickCount);
}
break;
case WindowMessage.WM_POINTERUPDATE:
{
handled = ProcessMessage(GetPointerId(wParam), RawStylusActions.Move, Environment.TickCount);
}
break;
case WindowMessage.WM_POINTERDOWN:
{
handled = ProcessMessage(GetPointerId(wParam), RawStylusActions.Down, Environment.TickCount);
}
break;
case WindowMessage.WM_POINTERUP:
{
handled = ProcessMessage(GetPointerId(wParam), RawStylusActions.Up, Environment.TickCount);
}
break;
case WindowMessage.WM_POINTERLEAVE:
{
// Leave can be processed as an OutOfRange.
// The MSDN documentation is not correct for OutOfRange (according to feisu)
// As such, using leave is the correct way to generate this. This is also what DirectInk uses.
handled = ProcessMessage(GetPointerId(wParam), RawStylusActions.OutOfRange, Environment.TickCount);
}
break;
}
}
return IntPtr.Zero;
}
#endregion
#region IInputProvider
/// <summary>
/// Indicates if the provider is responsible for providing
/// input for the specified visual.
/// </summary>
public bool ProvidesInputForRootVisual(Visual v)
{
return false;
}
/// <summary>
/// Notifies the input provider that it is no longer
/// the active input provider. If the input provider
/// needs to report more input, it will need to reactivate.
/// </summary>
public void NotifyDeactivate()
{
}
#endregion
}
}
|