/* * @author Valentin Simonov / http://va.lent.in/ * @author Valentin Frolov * @author Andrew David Griffiths * @author Cryville:Lime */ using AOT; using System; using System.Runtime.InteropServices; using System.Text; using UnityEngine; namespace Cryville.Common.Unity.Input { public class WindowsPointerHandler : InputHandler { static WindowsPointerHandler Instance; const string PRESS_AND_HOLD_ATOM = "MicrosoftTabletPenServiceProperty"; delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); public const int MONITOR_DEFAULTTONEAREST = 2; readonly IntPtr hMainWindow; IntPtr oldWndProcPtr; IntPtr newWndProcPtr; WndProcDelegate newWndProc; ushort pressAndHoldAtomID; readonly int touchInputSize; readonly long freq; static bool usePointerMessage; public WindowsPointerHandler() { if (Instance != null) throw new InvalidOperationException("WindowsPointerHandler already created"); if (Environment.OSVersion.Platform != PlatformID.Win32NT) throw new NotSupportedException("Windows pointer is not supported on this device"); Instance = this; usePointerMessage = true; hMainWindow = GetUnityWindowHandle(); if (hMainWindow == IntPtr.Zero) { throw new InvalidOperationException("Cannot find the game window"); } NativeMethods.QueryPerformanceFrequency(out freq); retry: if (usePointerMessage) { try { NativeMethods.EnableMouseInPointer(true); Logger.Log("main", 0, "Input", "Touch input frequency: {0}", freq); } catch (TypeLoadException) { usePointerMessage = false; Logger.Log("main", 2, "Input", "Advanced touch handling not supported"); goto retry; } } else { touchInputSize = Marshal.SizeOf(typeof(NativeMethods.TOUCHINPUT)); } } public const int TABLET_DISABLE_PRESSANDHOLD = 0x00000001; public const int TABLET_DISABLE_PENTAPFEEDBACK = 0x00000008; public const int TABLET_DISABLE_PENBARRELFEEDBACK = 0x00000010; public const int TABLET_DISABLE_FLICKS = 0x00010000; protected override void Activate() { newWndProc = WndProc; newWndProcPtr = Marshal.GetFunctionPointerForDelegate(newWndProc); oldWndProcPtr = SetWindowLongPtr(hMainWindow, -4, newWndProcPtr); if (!NativeMethods.RegisterTouchWindow(hMainWindow, NativeMethods.TOUCH_WINDOW_FLAGS.TWF_WANTPALM)) { throw new InvalidOperationException("Failed to register touch window"); } pressAndHoldAtomID = NativeMethods.GlobalAddAtom(PRESS_AND_HOLD_ATOM); NativeMethods.SetProp(hMainWindow, PRESS_AND_HOLD_ATOM, TABLET_DISABLE_PRESSANDHOLD | // disables press and hold (right-click) gesture TABLET_DISABLE_PENTAPFEEDBACK | // disables UI feedback on pen up (waves) TABLET_DISABLE_PENBARRELFEEDBACK | // disables UI feedback on pen button down (circle) TABLET_DISABLE_FLICKS // disables pen flicks (back, forward, drag down, drag up); ); } protected override void Deactivate() { if (pressAndHoldAtomID != 0) { NativeMethods.RemoveProp(hMainWindow, PRESS_AND_HOLD_ATOM); NativeMethods.GlobalDeleteAtom(pressAndHoldAtomID); } if (!NativeMethods.UnregisterTouchWindow(hMainWindow)) { throw new InvalidOperationException("Failed to unregister touch window"); } SetWindowLongPtr(hMainWindow, -4, oldWndProcPtr); newWndProcPtr = IntPtr.Zero; newWndProc = null; } const string UnityWindowClassName = "UnityWndClass"; static IntPtr _windowHandle; public static IntPtr GetUnityWindowHandle() { uint threadId = NativeMethods.GetCurrentThreadId(); NativeMethods.EnumThreadWindows( threadId, UnityWindowFilter, IntPtr.Zero ); return _windowHandle; } [MonoPInvokeCallback(typeof(NativeMethods.EnumWindowsProc))] public static bool UnityWindowFilter(IntPtr hWnd, IntPtr lParam) { var classText = new StringBuilder(UnityWindowClassName.Length + 1); NativeMethods.GetClassName(hWnd, classText, classText.Capacity); if (classText.ToString() == UnityWindowClassName) { _windowHandle = hWnd; return false; } return true; } public override bool IsNullable(int type) { return true; } public override byte GetDimension(int type) { return 2; } public override string GetTypeName(int type) { switch (type) { case 0: return "Windows Pointer Unknown"; case 1: return "Windows Pointer Pointer"; case 2: return "Windows Pointer Touch"; case 3: return "Windows Pointer Pen"; case 4: return "Windows Pointer Mouse"; case 5: return "Windows Pointer Touch Pad"; case 255: return "Windows Touch"; default: throw new ArgumentOutOfRangeException(nameof(type)); } } public override double GetCurrentTimestamp() { long tick; NativeMethods.QueryPerformanceCounter(out tick); return (double)tick / freq; } public override void Dispose(bool disposing) { if (disposing) { Deactivate(); if (usePointerMessage) NativeMethods.EnableMouseInPointer(false); } Instance = null; } [MonoPInvokeCallback(typeof(WndProcDelegate))] static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { try { var m = (WindowMessages)msg; /*if (m >= WindowMessages.WM_MOUSEFIRST && m <= WindowMessages.WM_MOUSELAST) { if (!usePointerMessage) Instance.handleMouseMsg(msg, wParam, lParam); } else*/ switch (m) { case WindowMessages.WM_TOUCH: if (usePointerMessage) NativeMethods.CloseTouchInputHandle(lParam); else Instance.HandleTouchMsg(wParam, lParam); break; case WindowMessages.WM_POINTERDOWN: case WindowMessages.WM_POINTERUP: case WindowMessages.WM_POINTERUPDATE: if (usePointerMessage) Instance.HandlePointerMsg(msg, wParam, lParam); break; case WindowMessages.WM_CLOSE: Instance.Dispose(); // Calling oldWndProcPtr here would crash the application Application.Quit(); return IntPtr.Zero; } return NativeMethods.CallWindowProc(Instance.oldWndProcPtr, hWnd, msg, wParam, lParam); } catch (Exception ex) { Logger.Log("main", 4, "Input", "An error occured while handling pointer:\n{0}", ex); return IntPtr.Zero; } } void HandlePointerMsg(uint msg, IntPtr wParam, IntPtr lParam) { int id = LOWORD(wParam.ToInt32()); var rawpinfo = new NativeMethods.POINTER_INFO(); if (!NativeMethods.GetPointerInfo(id, ref rawpinfo)) throw new InvalidOperationException("Failed to get pointer info"); /* Vector2? _cs = null; uint? orientation = null; uint? pressure = null; if (pointerInfo.pointerType == NativeMethods.POINTER_INPUT_TYPE.PT_TOUCH) { var pointerTouchInfo = new NativeMethods.POINTER_TOUCH_INFO(); NativeMethods.GetPointerTouchInfo(pointerId, ref pointerTouchInfo); if (pointerTouchInfo.touchMask.HasFlag(NativeMethods.TOUCH_MASK.TOUCH_MASK_CONTACTAREA)) { Vector2 cs; cs.x = pointerTouchInfo.rcContact.Width; cs.y = pointerTouchInfo.rcContact.Height; _cs = cs; } if (pointerTouchInfo.touchMask.HasFlag(NativeMethods.TOUCH_MASK.TOUCH_MASK_ORIENTATION)) { orientation = pointerTouchInfo.orientation; } if (pointerTouchInfo.touchMask.HasFlag(NativeMethods.TOUCH_MASK.TOUCH_MASK_PRESSURE)) { pressure = pointerTouchInfo.pressure; } }*/ NativeMethods.POINT p = rawpinfo.ptPixelLocation; NativeMethods.ScreenToClient(hMainWindow, ref p); Vector2 _p = UnityCameraUtils.ScreenToWorldPoint(new Vector2(p.X, Screen.height - p.Y)); double time = (double)rawpinfo.PerformanceCount / freq; int type; switch (rawpinfo.pointerType) { case NativeMethods.POINTER_INPUT_TYPE.PT_POINTER : type = 1; break; case NativeMethods.POINTER_INPUT_TYPE.PT_TOUCH : type = 2; break; case NativeMethods.POINTER_INPUT_TYPE.PT_PEN : type = 3; break; case NativeMethods.POINTER_INPUT_TYPE.PT_MOUSE : type = 4; break; case NativeMethods.POINTER_INPUT_TYPE.PT_TOUCHPAD: type = 5; break; default: type = 0; break; } if (rawpinfo.pointerFlags.HasFlag(NativeMethods.POINTER_FLAGS.POINTER_FLAG_CANCELED)) { Feed(type, id, new InputVector(time)); return; } InputVector vec = new InputVector(time, _p); switch ((WindowMessages)msg) { case WindowMessages.WM_POINTERDOWN: case WindowMessages.WM_POINTERUPDATE: Feed(type, id, vec); break; case WindowMessages.WM_POINTERUP: Feed(type, id, vec); Feed(type, id, new InputVector(time)); break; } } /*void handleMouseMsg(uint msg, IntPtr wParam, IntPtr lParam) { int id = mouseId; double procTime = diag::Stopwatch.GetTimestamp() / (double)diag::Stopwatch.Frequency; Vector2 _p; _p.x = ((int)(short)LOWORD(lParam.ToInt32())); _p.y = ((int)(short)HIWORD(lParam.ToInt32())); PointerPhase phase; switch ((WindowMessages)msg) { case WindowMessages.WM_LBUTTONDOWN: case WindowMessages.WM_LBUTTONDBLCLK: phase = PointerPhase.Begin; id = mouseId = newIdCallback(); break; case WindowMessages.WM_MOUSEMOVE: if (mouseId == 0) return; phase = PointerPhase.Update; break; case WindowMessages.WM_LBUTTONUP: phase = PointerPhase.End; mouseId = 0; break; case WindowMessages.WM_MOUSELEAVE: phase = PointerPhase.Cancel; mouseId = 0; break; default: return; } callback(id, new PointerInfo() { Id = id, ProcessTime = procTime, Phase = phase, Type = PointerType.Mouse, Position = _p, }); }*/ void HandleTouchMsg(IntPtr wParam, IntPtr lParam) { int inputCount = LOWORD(wParam.ToInt32()); NativeMethods.TOUCHINPUT[] inputs = new NativeMethods.TOUCHINPUT[inputCount]; if (!NativeMethods.GetTouchInputInfo(lParam, inputCount, inputs, touchInputSize)) throw new InvalidOperationException("Failed to get touch input info"); for (int i = 0; i < inputCount; i++) { NativeMethods.TOUCHINPUT touch = inputs[i]; NativeMethods.POINT p = new NativeMethods.POINT { X = touch.x / 100, Y = touch.y / 100 }; NativeMethods.ScreenToClient(hMainWindow, ref p); Vector2 _p = UnityCameraUtils.ScreenToWorldPoint(new Vector2(p.X, Screen.height - p.Y)); /*Vector2? _cs = null; if (touch.dwMask.HasFlag(NativeMethods.TOUCHINPUT_Mask.TOUCHINPUTMASKF_CONTACTAREA)) { Vector2 cs; cs.x = touch.cxContact / 100; cs.y = touch.cyContact / 100; _cs = cs; }*/ int id = touch.dwID; double time = touch.dwTime / 1000.0; InputVector vec = new InputVector(time, _p); if (touch.dwFlags.HasFlag(NativeMethods.TOUCHINPUT_Flags.TOUCHEVENTF_MOVE) || touch.dwFlags.HasFlag(NativeMethods.TOUCHINPUT_Flags.TOUCHEVENTF_DOWN)) { Feed(255, id, vec); } else if (touch.dwFlags.HasFlag(NativeMethods.TOUCHINPUT_Flags.TOUCHEVENTF_UP)) { Feed(255, id, vec); Feed(255, id, new InputVector(time)); } } NativeMethods.CloseTouchInputHandle(lParam); } public static int LOWORD(int value) { return value & 0xffff; } public static int HIWORD(int value) { return (value & (0xffff << 16)) >> 16; } public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong) { if (IntPtr.Size == 8) return NativeMethods.SetWindowLongPtr64(hWnd, nIndex, dwNewLong); return new IntPtr(NativeMethods.SetWindowLong32(hWnd, nIndex, dwNewLong.ToInt32())); } } }