diff --git a/OTDPlugins.sln b/OTDPlugins.sln index 0a181a3..c5e1a84 100644 --- a/OTDPlugins.sln +++ b/OTDPlugins.sln @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MLFilter", "MLFilter\MLFilt EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OemKill", "OemKill\OemKill.csproj", "{4EC8FA2D-0CCC-4FC3-A32A-BC063924A803}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsInk", "WindowsInk\WindowsInk.csproj", "{FC135341-4870-4555-8917-7248CFE07FCC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {4EC8FA2D-0CCC-4FC3-A32A-BC063924A803}.Debug|Any CPU.Build.0 = Debug|Any CPU {4EC8FA2D-0CCC-4FC3-A32A-BC063924A803}.Release|Any CPU.ActiveCfg = Release|Any CPU {4EC8FA2D-0CCC-4FC3-A32A-BC063924A803}.Release|Any CPU.Build.0 = Release|Any CPU + {FC135341-4870-4555-8917-7248CFE07FCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC135341-4870-4555-8917-7248CFE07FCC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC135341-4870-4555-8917-7248CFE07FCC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC135341-4870-4555-8917-7248CFE07FCC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/WindowsInk/NativeMethods.cs b/WindowsInk/NativeMethods.cs new file mode 100644 index 0000000..bf4382b --- /dev/null +++ b/WindowsInk/NativeMethods.cs @@ -0,0 +1,143 @@ +using System; +using System.Runtime.InteropServices; + +namespace WindowsInk +{ + using HSYNTHETICPOINTERDEVICE = IntPtr; + using HANDLE = IntPtr; + using HWND = IntPtr; + using DWORD = UInt32; + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int X; + public int Y; + + public POINT(int x, int y) + { + X = x; + Y = y; + } + } + public enum POINTER_INPUT_TYPE + { + PT_POINTER, + PT_TOUCH, + PT_PEN, + PT_MOUSE, + PT_TOUCHPAD + } + + public enum POINTER_FEEDBACK_MODE + { + DEFAULT, + INDIRECT, + NONE + } + + public enum POINTER_BUTTON_CHANGE_TYPE + { + NONE, + FIRSTBUTTON_DOWN, + FIRSTBUTTON_UP, + SECONDBUTTON_DOWN, + SECONDBUTTON_UP, + THIRDBUTTON_DOWN, + THIRDBUTTON_UP, + FOURTHBUTTON_DOWN, + FOURTHBUTTON_UP, + FIFTHBUTTON_DOWN, + FIFTHBUTTON_UP + } + + public enum POINTER_FLAGS + { + POINTER_FLAG_NONE = 0x00000000, + POINTER_FLAG_NEW = 0x00000001, + POINTER_FLAG_INRANGE = 0x00000002, + POINTER_FLAG_INCONTACT = 0x00000004, + POINTER_FLAG_FIRSTBUTTON = 0x00000010, + POINTER_FLAG_SECONDBUTTON = 0x00000020, + POINTER_FLAG_THIRDBUTTON = 0x00000040, + POINTER_FLAG_FOURTHBUTTON = 0x00000080, + POINTER_FLAG_FIFTHBUTTON = 0x00000100, + POINTER_FLAG_PRIMARY = 0x00002000, + POINTER_FLAG_CONFIDENCE = 0x000004000, + POINTER_FLAG_CANCELED = 0x000008000, + POINTER_FLAG_DOWN = 0x00010000, + POINTER_FLAG_UPDATE = 0x00020000, + POINTER_FLAG_UP = 0x00040000, + POINTER_FLAG_WHEEL = 0x00080000, + POINTER_FLAG_HWHEEL = 0x00100000, + POINTER_FLAG_CAPTURECHANGED = 0x00200000, + POINTER_FLAG_HASTRANSFORM = 0x00400000, + } + + [StructLayout(LayoutKind.Sequential)] + public struct POINTER_INFO + { + public POINTER_INPUT_TYPE pointerType; + public uint pointerId; + public uint frameId; + public POINTER_FLAGS pointerFlags; + public HANDLE sourceDevice; + public HWND hwndTarget; + public POINT ptPixelLocation; + public POINT ptHimetricLocation; + public POINT ptPixelLocationRaw; + public POINT ptHimetricLocationRaw; + public DWORD dwTime; + public uint historyCount; + public int InputData; + public DWORD dwKeyStates; + public ulong PerformanceCount; + public POINTER_BUTTON_CHANGE_TYPE ButtonChangeType; + } + + public enum PEN_FLAGS + { + NONE = 0x00000000, + BARREL = 0x00000001, + INVERTED = 0x00000002, + ERASER = 0x00000004 + } + + public enum PEN_MASK + { + NONE = 0x00000000, + PRESSURE = 0x00000001, + ROTATION = 0x00000002, + TILT_X = 0x00000004, + TILT_Y = 0x00000008 + } + + public struct POINTER_PEN_INFO + { + public POINTER_INFO pointerInfo; + public PEN_FLAGS pointerFlags; + public PEN_MASK penMask; + public uint pressure; + public uint rotation; + public int tiltX; + public int tiltY; + } + + [StructLayout(LayoutKind.Sequential)] + public struct POINTER_TYPE_INFO + { + public POINTER_INPUT_TYPE type; + public POINTER_PEN_INFO penInfo; + } + + public static class NativeMethods + { + [DllImport("user32.dll")] + public static extern HSYNTHETICPOINTERDEVICE CreateSyntheticPointerDevice(POINTER_INPUT_TYPE pointerType, + UInt32 maxCount, POINTER_FEEDBACK_MODE mode); + + [DllImport("user32.dll")] + public static extern bool InjectSyntheticPointerInput(HSYNTHETICPOINTERDEVICE device, + [In, MarshalAs(UnmanagedType.LPArray)] POINTER_TYPE_INFO[] pointerInfo, UInt32 count); + } +} \ No newline at end of file diff --git a/WindowsInk/Pen.cs b/WindowsInk/Pen.cs new file mode 100644 index 0000000..0f01764 --- /dev/null +++ b/WindowsInk/Pen.cs @@ -0,0 +1,102 @@ +using System; +using TabletDriverPlugin; + +namespace WindowsInk +{ + using HSYNTHETICPOINTERDEVICE = IntPtr; + + public class Pen + { + private POINTER_PEN_INFO _penInfo; + private POINTER_INFO _pointerInfo; + private HSYNTHETICPOINTERDEVICE _penHandle; + private bool inContact = false; + private POINTER_FLAGS pointerFlags; + + public Pen() + { + _pointerInfo = new POINTER_INFO + { + pointerType = POINTER_INPUT_TYPE.PT_PEN, + pointerId = 1, + frameId = 0, + pointerFlags = POINTER_FLAGS.POINTER_FLAG_NONE, + sourceDevice = IntPtr.Zero, + hwndTarget = IntPtr.Zero, + ptPixelLocation = new POINT(), + ptHimetricLocation = new POINT(), + ptPixelLocationRaw = new POINT(), + ptHimetricLocationRaw = new POINT(), + dwTime = 0, + historyCount = 1, + dwKeyStates = 0, + PerformanceCount = 0, + ButtonChangeType = POINTER_BUTTON_CHANGE_TYPE.NONE + }; + + _penInfo = new POINTER_PEN_INFO + { + pointerInfo = _pointerInfo, + pointerFlags = PEN_FLAGS.NONE, + penMask = PEN_MASK.NONE, + pressure = 0, + rotation = 0, + tiltX = 0, + tiltY = 0 + }; + + // Retrieve handle to custom pen + _penHandle = NativeMethods.CreateSyntheticPointerDevice(POINTER_INPUT_TYPE.PT_PEN, 1, POINTER_FEEDBACK_MODE.INDIRECT); + + // Notify WindowsInk + _pointerInfo.pointerFlags = POINTER_FLAGS.POINTER_FLAG_NEW; + Inject(); + + // Back to normal state + _pointerInfo.pointerFlags = POINTER_FLAGS.POINTER_FLAG_INRANGE | POINTER_FLAGS.POINTER_FLAG_PRIMARY; + } + + public void Inject() + { + var pointer = new POINTER_TYPE_INFO + { + type = POINTER_INPUT_TYPE.PT_PEN, + penInfo = _penInfo + }; + if (!NativeMethods.InjectSyntheticPointerInput(_penHandle, new POINTER_TYPE_INFO[] { pointer }, 1)) + { + Log.Write("WindowsInk", "Injection Failed"); + } + } + + public void SetPosition(POINT point) + { + _pointerInfo.ptPixelLocation = point; + _pointerInfo.ptPixelLocationRaw = point; + _pointerInfo.ptHimetricLocation = point; + _pointerInfo.ptHimetricLocationRaw = point; + } + + public void SetPressure(uint pressure) + { + if (pressure > 0) + inContact = true; + _penInfo.pressure = pressure; + } + + public void SetPointerFlags(POINTER_FLAGS flags) + { + pointerFlags |= flags; + } + + public void UnsetPointerFlags(POINTER_FLAGS flags) + { + pointerFlags &= ~flags; + } + + public void ClearPointerFlags() + { + pointerFlags = 0; + } + } +} \ No newline at end of file diff --git a/WindowsInk/WindowsInk.cs b/WindowsInk/WindowsInk.cs new file mode 100644 index 0000000..2dd7f15 --- /dev/null +++ b/WindowsInk/WindowsInk.cs @@ -0,0 +1,189 @@ +using System.Collections.Generic; +using System.Linq; +using TabletDriverPlugin; +using TabletDriverPlugin.Attributes; +using TabletDriverPlugin.Output; +using TabletDriverPlugin.Platform.Display; +using TabletDriverPlugin.Platform.Pointer; +using TabletDriverPlugin.Tablet; + +namespace WindowsInk +{ + [PluginName("Windows Ink")] + public class InkOutputMode : IOutputMode + { + private float[] _rotationMatrix; + private float _halfDisplayWidth, _halfDisplayHeight, _halfTabletWidth, _halfTabletHeight; + private float _minX, _maxX, _minY, _maxY; + + private List _filters, _preFilters = new List(), _postFilters = new List(); + public IEnumerable Filters + { + set + { + _filters = value.ToList(); + _preFilters.Clear(); + _postFilters.Clear(); + foreach (IFilter filter in _filters) + if (filter.FilterStage == FilterStage.PreTranspose) + _preFilters.Add(filter); + else if (filter.FilterStage == FilterStage.PostTranspose) + _postFilters.Add(filter); + } + get => _filters; + } + + private TabletProperties _tabletProperties; + public TabletProperties TabletProperties + { + set + { + _tabletProperties = value; + UpdateCache(); + } + get => _tabletProperties; + } + + private Area _displayArea, _tabletArea; + + public Area Input + { + set + { + _tabletArea = value; + UpdateCache(); + } + get => _tabletArea; + } + + public Area Output + { + set + { + _displayArea = value; + UpdateCache(); + } + get => _displayArea; + } + + public IVirtualScreen VirtualScreen { set; get; } + public IPointerHandler PointerHandler => new InkPointerHandler(); + public bool AreaClipping { set; get; } + + internal void UpdateCache() + { + _rotationMatrix = Input?.GetRotationMatrix(); + + _halfDisplayWidth = Output?.Width / 2 ?? 0; + _halfDisplayHeight = Output?.Height / 2 ?? 0; + _halfTabletWidth = Input?.Width / 2 ?? 0; + _halfTabletHeight = Input?.Height / 2 ?? 0; + + _minX = Output?.Position.X - _halfDisplayWidth ?? 0; + _maxX = Output?.Position.X + Output?.Width - _halfDisplayWidth ?? 0; + _minY = Output?.Position.Y - _halfDisplayHeight ?? 0; + _maxY = Output?.Position.Y + Output?.Height - _halfDisplayHeight ?? 0; + } + + public void Read(IDeviceReport report) + { + if (report is ITabletReport tabletReport) + { + if (TabletProperties.ActiveReportID.IsInRange(tabletReport.ReportID)) + { + if (PointerHandler is IPressureHandler pressureHandler) + pressureHandler.SetPressure((float)tabletReport.Pressure / (float)TabletProperties.MaxPressure); + + var pos = Transpose(tabletReport); + PointerHandler.SetPosition(pos); + } + } + // HandleBinding(report); + } + + internal Point Transpose(ITabletReport report) + { + var pos = new Point(report.Position.X, report.Position.Y); + + // Pre Filter + foreach (IFilter filter in _preFilters) + pos = filter.Filter(pos); + + // Normalize (ratio of 1) + pos.X /= TabletProperties.MaxX; + pos.Y /= TabletProperties.MaxY; + + // Scale to tablet dimensions (mm) + pos.X *= TabletProperties.Width; + pos.Y *= TabletProperties.Height; + + // Adjust area to set origin to 0,0 + pos -= Input.Position; + + // Rotation + if (Input.Rotation != 0f) + { + var tempCopy = new Point(pos.X, pos.Y); + pos.X = (tempCopy.X * _rotationMatrix[0]) + (tempCopy.Y * _rotationMatrix[1]); + pos.Y = (tempCopy.X * _rotationMatrix[2]) + (tempCopy.Y * _rotationMatrix[3]); + } + + // Move area back + pos.X += _halfTabletWidth; + pos.Y += _halfTabletHeight; + + // Scale to tablet area (ratio of 1) + pos.X /= Input.Width; + pos.Y /= Input.Height; + + // Scale to display area + pos.X *= Output.Width; + pos.Y *= Output.Height; + + // Adjust display offset by center + pos.X += Output.Position.X - _halfDisplayWidth; + pos.Y += Output.Position.Y - _halfDisplayHeight; + + // Clipping to display bounds + if (AreaClipping) + { + if (pos.X < _minX) + pos.X = _minX; + if (pos.X > _maxX) + pos.X = _maxX; + if (pos.Y < _minY) + pos.Y = _minY; + if (pos.Y > _maxY) + pos.Y = _maxY; + } + + // Post Filter + foreach (IFilter filter in _postFilters) + pos = filter.Filter(pos); + + return pos; + } + } + + public class InkPointerHandler : IPointerHandler, IPressureHandler + { + private Pen pen = new Pen(); + private Point _lastPos; + public Point GetPosition() + { + return _lastPos; + } + + public void SetPosition(Point pos) + { + pen.SetPosition(new POINT((int)pos.X, (int)pos.Y)); + pen.Inject(); + _lastPos = pos; + } + + public void SetPressure(float percentage) + { + pen.SetPressure((uint)(percentage * 1024)); + } + } +} \ No newline at end of file diff --git a/WindowsInk/WindowsInk.csproj b/WindowsInk/WindowsInk.csproj new file mode 100644 index 0000000..759741c --- /dev/null +++ b/WindowsInk/WindowsInk.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1; net5.0 + + + + + + +