diff --git a/Source/Installer/Product.wxs b/Source/Installer/Product.wxs index 22ae7e5f..f68508a9 100644 --- a/Source/Installer/Product.wxs +++ b/Source/Installer/Product.wxs @@ -1,6 +1,6 @@  - ()); + } public DesktopItem( string deviceInstanceId, diff --git a/Source/Monitorian.Core/Models/Monitor/MonitorConfiguration.cs b/Source/Monitorian.Core/Models/Monitor/MonitorConfiguration.cs index 9c38d8e2..58738e15 100644 --- a/Source/Monitorian.Core/Models/Monitor/MonitorConfiguration.cs +++ b/Source/Monitorian.Core/Models/Monitor/MonitorConfiguration.cs @@ -463,6 +463,7 @@ public static AccessResult SetBrightness(SafePhysicalMonitorHandle physicalMonit private const uint ERROR_GRAPHICS_DDCCI_INVALID_MESSAGE_COMMAND = 0xC0262589; private const uint ERROR_GRAPHICS_DDCCI_INVALID_MESSAGE_LENGTH = 0xC026258A; private const uint ERROR_GRAPHICS_DDCCI_INVALID_MESSAGE_CHECKSUM = 0xC026258B; + private const uint ERROR_GRAPHICS_I2C_ERROR_TRANSMITTING_DATA = 0xC0262582; private const uint ERROR_GRAPHICS_MONITOR_NO_LONGER_EXISTS = 0xC026258D; private static AccessStatus GetStatus(int errorCode) @@ -474,6 +475,7 @@ ERROR_GRAPHICS_DDCCI_INVALID_DATA or ERROR_GRAPHICS_DDCCI_INVALID_MESSAGE_COMMAND or ERROR_GRAPHICS_DDCCI_INVALID_MESSAGE_LENGTH or ERROR_GRAPHICS_DDCCI_INVALID_MESSAGE_CHECKSUM => AccessStatus.DdcFailed, + ERROR_GRAPHICS_I2C_ERROR_TRANSMITTING_DATA => AccessStatus.TransmissionFailed, ERROR_GRAPHICS_MONITOR_NO_LONGER_EXISTS => AccessStatus.NoLongerExist, _ => AccessStatus.Failed }; diff --git a/Source/Monitorian.Core/Monitorian.Core.csproj b/Source/Monitorian.Core/Monitorian.Core.csproj index a8c55499..8e93e098 100644 --- a/Source/Monitorian.Core/Monitorian.Core.csproj +++ b/Source/Monitorian.Core/Monitorian.Core.csproj @@ -129,6 +129,9 @@ + + + MainWindow.xaml diff --git a/Source/Monitorian.Core/Properties/AssemblyInfo.cs b/Source/Monitorian.Core/Properties/AssemblyInfo.cs index e43ae91e..0b34f460 100644 --- a/Source/Monitorian.Core/Properties/AssemblyInfo.cs +++ b/Source/Monitorian.Core/Properties/AssemblyInfo.cs @@ -33,8 +33,8 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.18.1.0")] -[assembly: AssemblyFileVersion("2.18.1.0")] +[assembly: AssemblyVersion("2.19.0.0")] +[assembly: AssemblyFileVersion("2.19.0.0")] [assembly: NeutralResourcesLanguage("en-US")] // For unit test diff --git a/Source/Monitorian.Core/ViewModels/MonitorViewModel.cs b/Source/Monitorian.Core/ViewModels/MonitorViewModel.cs index 3eb4bdee..c83f4824 100644 --- a/Source/Monitorian.Core/ViewModels/MonitorViewModel.cs +++ b/Source/Monitorian.Core/ViewModels/MonitorViewModel.cs @@ -187,12 +187,7 @@ public void IncrementBrightness(int tickSize, bool isCycle = true) var count = Math.Floor((Brightness - RangeLowest) / size); int brightness = RangeLowest + (int)Math.Ceiling((count + 1) * size); - if (brightness < RangeLowest) - brightness = RangeLowest; - else if (RangeHighest < brightness) - brightness = isCycle ? RangeLowest : RangeHighest; - - SetBrightness(brightness); + SetBrightness(brightness, isCycle); } public void DecrementBrightness() @@ -212,10 +207,15 @@ public void DecrementBrightness(int tickSize, bool isCycle = true) var count = Math.Ceiling((Brightness - RangeLowest) / size); int brightness = RangeLowest + (int)Math.Floor((count - 1) * size); + SetBrightness(brightness, isCycle); + } + + private void SetBrightness(int brightness, bool isCycle) + { if (brightness < RangeLowest) brightness = isCycle ? RangeHighest : RangeLowest; else if (RangeHighest < brightness) - brightness = RangeHighest; + brightness = isCycle ? RangeLowest : RangeHighest; SetBrightness(brightness); } @@ -241,6 +241,7 @@ private bool SetBrightness(int brightness) switch (result.Status) { case AccessStatus.DdcFailed: + case AccessStatus.TransmissionFailed: case AccessStatus.NoLongerExist: _controller.OnMonitorsChangeFound(); break; diff --git a/Source/Monitorian.Core/Views/Controls/Sliders/CompoundSlider.cs b/Source/Monitorian.Core/Views/Controls/Sliders/CompoundSlider.cs index 109b45f6..fd685b0a 100644 --- a/Source/Monitorian.Core/Views/Controls/Sliders/CompoundSlider.cs +++ b/Source/Monitorian.Core/Views/Controls/Sliders/CompoundSlider.cs @@ -95,13 +95,13 @@ private void OnMoved(object sender, double delta) } else { - base.UpdateSourceDeferred(); + base.ExecuteUpdateSource(); } } - protected override void UpdateSourceDeferred() + protected override void ExecuteUpdateSource() { - base.UpdateSourceDeferred(); + base.ExecuteUpdateSource(); Moved?.Invoke(this, 0D); } diff --git a/Source/Monitorian.Core/Views/Controls/Sliders/EnhancedSlider.cs b/Source/Monitorian.Core/Views/Controls/Sliders/EnhancedSlider.cs index 764d053c..d703e692 100644 --- a/Source/Monitorian.Core/Views/Controls/Sliders/EnhancedSlider.cs +++ b/Source/Monitorian.Core/Views/Controls/Sliders/EnhancedSlider.cs @@ -34,6 +34,13 @@ public override void OnApplyTemplate() CheckCanDrag(); } + public bool ChangeValue(double changeSize) + { + return UpdateValue(this.Value + changeSize); + } + + public void EnsureUpdateSource() => ExecuteUpdateSource(); + protected virtual bool UpdateValue(double value) { // Slider.SnapToTick property will not be reflected like Slider.UpdateValue method. @@ -112,7 +119,7 @@ protected override void OnPreviewMouseUp(MouseButtonEventArgs e) { base.OnPreviewMouseUp(e); - UpdateSourceDeferred(); + ExecuteUpdateSource(); } // OnPreviewStylusDown covers the case of OnPreviewTouchDown. @@ -289,7 +296,7 @@ protected override void OnManipulationCompleted(ManipulationCompletedEventArgs e { base.OnManipulationCompleted(e); - UpdateSourceDeferred(); + ExecuteUpdateSource(); } #endregion @@ -321,32 +328,32 @@ protected override void OnMouseWheel(MouseWheelEventArgs e) // Mouse.MouseWheelDeltaForOneLine should be casted to double in case the delta is smaller than 120. var newValue = this.Value + (e.Delta / (double)Mouse.MouseWheelDeltaForOneLine * WheelFactor); UpdateValue(newValue); - UpdateSourceDeferred(); + ExecuteUpdateSource(); } #endregion #region Deferral - public bool IsUpdateSourceDeferred + public bool DefersUpdateSource { - get { return (bool)GetValue(IsUpdateSourceDeferredProperty); } - set { SetValue(IsUpdateSourceDeferredProperty, value); } + get { return (bool)GetValue(DefersUpdateSourceProperty); } + set { SetValue(DefersUpdateSourceProperty, value); } } - public static readonly DependencyProperty IsUpdateSourceDeferredProperty = + public static readonly DependencyProperty DefersUpdateSourceProperty = DependencyProperty.Register( - "IsUpdateSourceDeferred", + "DefersUpdateSource", typeof(bool), typeof(EnhancedSlider), new PropertyMetadata( false, - (d, e) => ((EnhancedSlider)d).PrepareSourceDeferred((bool)e.NewValue))); + (d, e) => ((EnhancedSlider)d).PrepareUpdateSource((bool)e.NewValue))); private BindingExpression _valuePropertyExpression; - protected virtual void PrepareSourceDeferred(bool isDeferred) + protected virtual void PrepareUpdateSource(bool defer) { - if (isDeferred) + if (defer) { _valuePropertyExpression = ReplaceBinding(this, ValueProperty, BindingMode.TwoWay, UpdateSourceTrigger.Explicit); } @@ -373,7 +380,7 @@ static BindingExpression ReplaceBinding(DependencyObject target, DependencyPrope } } - protected virtual void UpdateSourceDeferred() + protected virtual void ExecuteUpdateSource() { _valuePropertyExpression?.UpdateSource(); } diff --git a/Source/Monitorian.Core/Views/MainWindow.xaml b/Source/Monitorian.Core/Views/MainWindow.xaml index 4b2ce2c2..aed3354b 100644 --- a/Source/Monitorian.Core/Views/MainWindow.xaml +++ b/Source/Monitorian.Core/Views/MainWindow.xaml @@ -610,8 +610,7 @@ @@ -641,7 +635,7 @@ Style="{StaticResource SliderHorizontal}" Minimum="0" Maximum="100" Value="{Binding Brightness, Mode=TwoWay, Delay=50}" - IsUpdateSourceDeferred="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Settings.DefersUpdate, Mode=OneWay}" + DefersUpdateSource="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Settings.DefersUpdate, Mode=OneWay}" IsShadowVisible="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Settings.ShowsAdjusted}" ValueShadow="{Binding BrightnessSystemAdjusted, Mode=OneWay}" IsUnison="{Binding IsUnison, Mode=TwoWay}" diff --git a/Source/Monitorian.Core/Views/MainWindow.xaml.cs b/Source/Monitorian.Core/Views/MainWindow.xaml.cs index a869663e..6dbd94a0 100644 --- a/Source/Monitorian.Core/Views/MainWindow.xaml.cs +++ b/Source/Monitorian.Core/Views/MainWindow.xaml.cs @@ -15,6 +15,8 @@ using Monitorian.Core.Helper; using Monitorian.Core.Models; using Monitorian.Core.ViewModels; +using Monitorian.Core.Views.Controls; +using Monitorian.Core.Views.Touchpad; using ScreenFrame.Movers; namespace Monitorian.Core.Views @@ -22,6 +24,7 @@ namespace Monitorian.Core.Views public partial class MainWindow : Window { private readonly StickWindowMover _mover; + private readonly TouchpadTracker _tracker; public MainWindowViewModel ViewModel => (MainWindowViewModel)this.DataContext; public MainWindow(AppControllerCore controller) @@ -33,6 +36,18 @@ public MainWindow(AppControllerCore controller) this.DataContext = new MainWindowViewModel(controller); _mover = new StickWindowMover(this, controller.NotifyIconContainer.NotifyIcon); + + _tracker = new TouchpadTracker(this); + _tracker.ManipulationDelta += (_, delta) => + { + var slider = FocusManager.GetFocusedElement(this) as EnhancedSlider; + slider?.ChangeValue(delta); + }; + _tracker.ManipulationCompleted += (_, _) => + { + var slider = FocusManager.GetFocusedElement(this) as EnhancedSlider; + slider?.EnsureUpdateSource(); + }; } protected override void OnSourceInitialized(EventArgs e) diff --git a/Source/Monitorian.Core/Views/Touchpad/TouchpadContact.cs b/Source/Monitorian.Core/Views/Touchpad/TouchpadContact.cs new file mode 100644 index 00000000..be9a2c5f --- /dev/null +++ b/Source/Monitorian.Core/Views/Touchpad/TouchpadContact.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace Monitorian.Core.Views.Touchpad +{ + internal struct TouchpadContact : IEquatable + { + public int ContactId { get; } + public int X { get; } + public int Y { get; } + + public Point Point => new(X, Y); + + public TouchpadContact(int contactId, int x, int y) => + (this.ContactId, this.X, this.Y) = (contactId, x, y); + + public override bool Equals(object obj) => (obj is TouchpadContact other) && Equals(other); + + public bool Equals(TouchpadContact other) => + (this.ContactId == other.ContactId) && (this.X == other.X) && (this.Y == other.Y); + + public static bool operator ==(TouchpadContact a, TouchpadContact b) => a.Equals(b); + public static bool operator !=(TouchpadContact a, TouchpadContact b) => !a.Equals(b); + + public override int GetHashCode() => (this.ContactId, this.X, this.Y).GetHashCode(); + + public override string ToString() => $"Contact ID:{ContactId} Point:{X},{Y}"; + } + + internal class TouchpadContactCreator + { + public int? ContactId { get; set; } + public int? X { get; set; } + public int? Y { get; set; } + + public bool TryCreate(out TouchpadContact contact) + { + if (ContactId.HasValue && X.HasValue && Y.HasValue) + { + contact = new TouchpadContact(ContactId.Value, X.Value, Y.Value); + return true; + } + contact = default; + return false; + } + + public void Clear() + { + ContactId = null; + X = null; + Y = null; + } + } +} \ No newline at end of file diff --git a/Source/Monitorian.Core/Views/Touchpad/TouchpadHelper.cs b/Source/Monitorian.Core/Views/Touchpad/TouchpadHelper.cs new file mode 100644 index 00000000..e4da6772 --- /dev/null +++ b/Source/Monitorian.Core/Views/Touchpad/TouchpadHelper.cs @@ -0,0 +1,504 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Monitorian.Core.Views.Touchpad +{ + internal static class TouchpadHelper + { + #region Win32 + + [DllImport("User32", SetLastError = true)] + private static extern uint GetRawInputDeviceList( + [Out] RAWINPUTDEVICELIST[] pRawInputDeviceList, + ref uint puiNumDevices, + uint cbSize); + + [StructLayout(LayoutKind.Sequential)] + private struct RAWINPUTDEVICELIST + { + public IntPtr hDevice; + public uint dwType; // RIM_TYPEMOUSE or RIM_TYPEKEYBOARD or RIM_TYPEHID + } + + private const uint RIM_TYPEMOUSE = 0; + private const uint RIM_TYPEKEYBOARD = 1; + private const uint RIM_TYPEHID = 2; + + [DllImport("User32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool RegisterRawInputDevices( + RAWINPUTDEVICE[] pRawInputDevices, + uint uiNumDevices, + uint cbSize); + + [StructLayout(LayoutKind.Sequential)] + private struct RAWINPUTDEVICE + { + public ushort usUsagePage; + public ushort usUsage; + public uint dwFlags; // RIDEV_REMOVE or RIDEV_INPUTSINK + public IntPtr hwndTarget; + } + + private const uint RIDEV_REMOVE = 0x00000001; + private const uint RIDEV_INPUTSINK = 0x00000100; + + [DllImport("User32.dll", SetLastError = true)] + private static extern uint GetRawInputData( + IntPtr hRawInput, // lParam in WM_INPUT + uint uiCommand, // RID_HEADER + IntPtr pData, + ref uint pcbSize, + uint cbSizeHeader); + + private const uint RID_INPUT = 0x10000003; + + [StructLayout(LayoutKind.Sequential)] + private struct RAWINPUT + { + public RAWINPUTHEADER Header; + public RAWHID Hid; + } + + [StructLayout(LayoutKind.Sequential)] + private struct RAWINPUTHEADER + { + public uint dwType; // RIM_TYPEMOUSE or RIM_TYPEKEYBOARD or RIM_TYPEHID + public uint dwSize; + public IntPtr hDevice; + public IntPtr wParam; // wParam in WM_INPUT + } + + [StructLayout(LayoutKind.Sequential)] + private struct RAWHID + { + public uint dwSizeHid; + public uint dwCount; + + public IntPtr bRawData; // This is not for use. + } + + [DllImport("User32.dll", SetLastError = true)] + private static extern uint GetRawInputDeviceInfo( + IntPtr hDevice, // hDevice by RAWINPUTHEADER + uint uiCommand, // RIDI_PREPARSEDDATA + IntPtr pData, + ref uint pcbSize); + + [DllImport("User32.dll", SetLastError = true)] + private static extern uint GetRawInputDeviceInfo( + IntPtr hDevice, // hDevice by RAWINPUTDEVICELIST + uint uiCommand, // RIDI_DEVICEINFO + ref RID_DEVICE_INFO pData, + ref uint pcbSize); + + private const uint RIDI_PREPARSEDDATA = 0x20000005; + private const uint RIDI_DEVICEINFO = 0x2000000b; + + [StructLayout(LayoutKind.Sequential)] + private struct RID_DEVICE_INFO + { + public uint cbSize; // This is determined to accommodate RID_DEVICE_INFO_KEYBOARD. + public uint dwType; + public RID_DEVICE_INFO_HID hid; + } + + [StructLayout(LayoutKind.Sequential)] + private struct RID_DEVICE_INFO_HID + { + public uint dwVendorId; + public uint dwProductId; + public uint dwVersionNumber; + public ushort usUsagePage; + public ushort usUsage; + } + + [DllImport("Hid.dll", SetLastError = true)] + private static extern uint HidP_GetCaps( + IntPtr PreparsedData, + out HIDP_CAPS Capabilities); + + private const uint HIDP_STATUS_SUCCESS = 0x00110000; + + [StructLayout(LayoutKind.Sequential)] + private struct HIDP_CAPS + { + public ushort Usage; + public ushort UsagePage; + public ushort InputReportByteLength; + public ushort OutputReportByteLength; + public ushort FeatureReportByteLength; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)] + public ushort[] Reserved; + + public ushort NumberLinkCollectionNodes; + public ushort NumberInputButtonCaps; + public ushort NumberInputValueCaps; + public ushort NumberInputDataIndices; + public ushort NumberOutputButtonCaps; + public ushort NumberOutputValueCaps; + public ushort NumberOutputDataIndices; + public ushort NumberFeatureButtonCaps; + public ushort NumberFeatureValueCaps; + public ushort NumberFeatureDataIndices; + } + + [DllImport("Hid.dll", CharSet = CharSet.Auto)] + private static extern uint HidP_GetValueCaps( + HIDP_REPORT_TYPE ReportType, + [Out] HIDP_VALUE_CAPS[] ValueCaps, + ref ushort ValueCapsLength, + IntPtr PreparsedData); + + private enum HIDP_REPORT_TYPE + { + HidP_Input, + HidP_Output, + HidP_Feature + } + + [StructLayout(LayoutKind.Sequential)] + private struct HIDP_VALUE_CAPS + { + public ushort UsagePage; + public byte ReportID; + + [MarshalAs(UnmanagedType.U1)] + public bool IsAlias; + + public ushort BitField; + public ushort LinkCollection; + public ushort LinkUsage; + public ushort LinkUsagePage; + + [MarshalAs(UnmanagedType.U1)] + public bool IsRange; + [MarshalAs(UnmanagedType.U1)] + public bool IsStringRange; + [MarshalAs(UnmanagedType.U1)] + public bool IsDesignatorRange; + [MarshalAs(UnmanagedType.U1)] + public bool IsAbsolute; + [MarshalAs(UnmanagedType.U1)] + public bool HasNull; + + public byte Reserved; + public ushort BitSize; + public ushort ReportCount; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] + public ushort[] Reserved2; + + public uint UnitsExp; + public uint Units; + public int LogicalMin; + public int LogicalMax; + public int PhysicalMin; + public int PhysicalMax; + + // Range + public ushort UsageMin; + public ushort UsageMax; + public ushort StringMin; + public ushort StringMax; + public ushort DesignatorMin; + public ushort DesignatorMax; + public ushort DataIndexMin; + public ushort DataIndexMax; + + // NotRange + public ushort Usage => UsageMin; + // ushort Reserved1; + public ushort StringIndex => StringMin; + // ushort Reserved2; + public ushort DesignatorIndex => DesignatorMin; + // ushort Reserved3; + public ushort DataIndex => DataIndexMin; + // ushort Reserved4; + } + + [DllImport("Hid.dll", CharSet = CharSet.Auto)] + private static extern uint HidP_GetUsageValue( + HIDP_REPORT_TYPE ReportType, + ushort UsagePage, + ushort LinkCollection, + ushort Usage, + out uint UsageValue, + IntPtr PreparsedData, + IntPtr Report, + uint ReportLength); + + #endregion + + // Precision Touchpad (PTP) in HID Clients Supported in Windows + // https://docs.microsoft.com/en-us/windows-hardware/drivers/hid/hid-architecture#hid-clients-supported-in-windows + private const ushort TouchpadUsagePage = 0x000D; + private const ushort TouchpadUsage = 0x0005; + + public static bool Exists() + { + uint deviceListCount = 0; + uint rawInputDeviceListSize = (uint)Marshal.SizeOf(); + + if (GetRawInputDeviceList( + null, + ref deviceListCount, + rawInputDeviceListSize) != 0) + { + return false; + } + + var devices = new RAWINPUTDEVICELIST[deviceListCount]; + + if (GetRawInputDeviceList( + devices, + ref deviceListCount, + rawInputDeviceListSize) != deviceListCount) + { + return false; + } + + foreach (var device in devices.Where(x => x.dwType == RIM_TYPEHID)) + { + uint deviceInfoSize = 0; + + if (GetRawInputDeviceInfo( + device.hDevice, + RIDI_DEVICEINFO, + IntPtr.Zero, + ref deviceInfoSize) != 0) + { + continue; + } + + var deviceInfo = new RID_DEVICE_INFO { cbSize = deviceInfoSize }; + + if (GetRawInputDeviceInfo( + device.hDevice, + RIDI_DEVICEINFO, + ref deviceInfo, + ref deviceInfoSize) == unchecked((uint)-1)) + { + continue; + } + + if ((deviceInfo.hid.usUsagePage == TouchpadUsagePage) && + (deviceInfo.hid.usUsage == TouchpadUsage)) + { + return true; + } + } + return false; + } + + #region Register/Unregister + + public static bool RegisterInput(IntPtr windowHandle) + { + var device = new RAWINPUTDEVICE + { + usUsagePage = TouchpadUsagePage, + usUsage = TouchpadUsage, + dwFlags = 0, // WM_INPUT messages come only when the window is in the foreground. + hwndTarget = windowHandle + }; + + return RegisterRawInputDevices(new[] { device }, 1, (uint)Marshal.SizeOf()); + } + + public static bool UnregisterInput() + { + var device = new RAWINPUTDEVICE + { + usUsagePage = TouchpadUsagePage, + usUsage = TouchpadUsage, + dwFlags = RIDEV_REMOVE, + hwndTarget = IntPtr.Zero + }; + + return RegisterRawInputDevices(new[] { device }, 1, (uint)Marshal.SizeOf()); + } + + #endregion + + public const int WM_INPUT = 0x00FF; + public const int RIM_INPUT = 0; + public const int RIM_INPUTSINK = 1; + + public static TouchpadContact[] ParseInput(IntPtr lParam) + { + // Get RAWINPUT. + uint rawInputSize = 0; + uint rawInputHeaderSize = (uint)Marshal.SizeOf(); + + if (GetRawInputData( + lParam, + RID_INPUT, + IntPtr.Zero, + ref rawInputSize, + rawInputHeaderSize) != 0) + { + return null; + } + + RAWINPUT rawInput; + byte[] rawHidRawData; + + IntPtr rawInputPointer = IntPtr.Zero; + try + { + rawInputPointer = Marshal.AllocHGlobal((int)rawInputSize); + + if (GetRawInputData( + lParam, + RID_INPUT, + rawInputPointer, + ref rawInputSize, + rawInputHeaderSize) != rawInputSize) + { + return null; + } + + rawInput = Marshal.PtrToStructure(rawInputPointer); + + var rawInputData = new byte[rawInputSize]; + Marshal.Copy(rawInputPointer, rawInputData, 0, rawInputData.Length); + + rawHidRawData = new byte[rawInput.Hid.dwSizeHid * rawInput.Hid.dwCount]; + int rawInputOffset = (int)rawInputSize - rawHidRawData.Length; + Buffer.BlockCopy(rawInputData, rawInputOffset, rawHidRawData, 0, rawHidRawData.Length); + } + finally + { + Marshal.FreeHGlobal(rawInputPointer); + } + + // Parse RAWINPUT. + IntPtr preparsedDataPointer = IntPtr.Zero; + IntPtr rawHidRawDataPointer = IntPtr.Zero; + try + { + uint preparsedDataSize = 0; + + if (GetRawInputDeviceInfo( + rawInput.Header.hDevice, + RIDI_PREPARSEDDATA, + IntPtr.Zero, + ref preparsedDataSize) != 0) + { + return null; + } + + preparsedDataPointer = Marshal.AllocHGlobal((int)preparsedDataSize); + + if (GetRawInputDeviceInfo( + rawInput.Header.hDevice, + RIDI_PREPARSEDDATA, + preparsedDataPointer, + ref preparsedDataSize) != preparsedDataSize) + { + return null; + } + + if (HidP_GetCaps( + preparsedDataPointer, + out HIDP_CAPS caps) != HIDP_STATUS_SUCCESS) + { + return null; + } + + ushort valueCapsLength = caps.NumberInputValueCaps; + var valueCaps = new HIDP_VALUE_CAPS[valueCapsLength]; + + if (HidP_GetValueCaps( + HIDP_REPORT_TYPE.HidP_Input, + valueCaps, + ref valueCapsLength, + preparsedDataPointer) != HIDP_STATUS_SUCCESS) + { + return null; + } + + rawHidRawDataPointer = Marshal.AllocHGlobal(rawHidRawData.Length); + Marshal.Copy(rawHidRawData, 0, rawHidRawDataPointer, rawHidRawData.Length); + + uint scanTime = 0; + uint contactCount = 0; + TouchpadContactCreator creator = new(); + List contacts = new(); + + foreach (var valueCap in valueCaps.OrderBy(x => x.LinkCollection)) + { + if (HidP_GetUsageValue( + HIDP_REPORT_TYPE.HidP_Input, + valueCap.UsagePage, + valueCap.LinkCollection, + valueCap.Usage, + out uint value, + preparsedDataPointer, + rawHidRawDataPointer, + (uint)rawHidRawData.Length) != HIDP_STATUS_SUCCESS) + { + continue; + } + + // Usage Page and ID in Windows Precision Touchpad input reports + // https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-precision-touchpad-required-hid-top-level-collections#windows-precision-touchpad-input-reports + switch (valueCap.LinkCollection) + { + case 0: + switch (valueCap.UsagePage, valueCap.Usage) + { + case (0x0D, 0x56): // Scan Time + scanTime = value; + break; + + case (0x0D, 0x54): // Contact Count + contactCount = value; + break; + } + break; + + default: + switch (valueCap.UsagePage, valueCap.Usage) + { + case (0x0D, 0x51): // Contact ID + creator.ContactId = (int)value; + break; + + case (0x01, 0x30): // X + creator.X = (int)value; + break; + + case (0x01, 0x31): // Y + creator.Y = (int)value; + break; + } + break; + } + + if (creator.TryCreate(out TouchpadContact contact)) + { + contacts.Add(contact); + if (contacts.Count >= contactCount) + break; + + creator.Clear(); + } + } + + return contacts.ToArray(); + } + finally + { + Marshal.FreeHGlobal(preparsedDataPointer); + Marshal.FreeHGlobal(rawHidRawDataPointer); + } + } + } +} \ No newline at end of file diff --git a/Source/Monitorian.Core/Views/Touchpad/TouchpadTracker.cs b/Source/Monitorian.Core/Views/Touchpad/TouchpadTracker.cs new file mode 100644 index 00000000..f38b9f23 --- /dev/null +++ b/Source/Monitorian.Core/Views/Touchpad/TouchpadTracker.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Interop; + +using Monitorian.Core.Helper; + +namespace Monitorian.Core.Views.Touchpad +{ + public class TouchpadTracker + { + private readonly Window _window; + + public TouchpadTracker(Window window) + { + if (!TouchpadHelper.Exists()) + return; + + this._window = window ?? throw new ArgumentNullException(nameof(window)); + this._window.SourceInitialized += OnSourceInitialized; + this._window.Closed += OnClosed; + } + + private Throttle _complete; + private HwndSource _source; + + private void OnSourceInitialized(object sender, EventArgs e) + { + _complete = new Throttle(TimeSpan.FromMilliseconds(100), Complete); + + _source = (HwndSource)PresentationSource.FromVisual(_window); + _source.AddHook(WndProc); + + TouchpadHelper.RegisterInput(_source.Handle); + } + + private void OnClosed(object sender, EventArgs e) + { + ManipulationDelta = null; + ManipulationCompleted = null; + + _source?.RemoveHook(WndProc); + + TouchpadHelper.UnregisterInput(); + } + + private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + switch (msg) + { + case TouchpadHelper.WM_INPUT: + var contacts = TouchpadHelper.ParseInput(lParam); + if (contacts?.Length > 1) + { + Check(contacts[0]); + handled = true; + } + break; + } + return IntPtr.Zero; + } + + public event EventHandler ManipulationDelta; + public event EventHandler ManipulationCompleted; + + public int UnitResolution + { + get => _unitResolution; + set => _unitResolution = Math.Max(1, value); + } + private int _unitResolution = 30; // Default + + private TouchpadContact _contact; + + private async void Check(TouchpadContact contact) + { + if ((_contact == default) || + (_contact.ContactId != contact.ContactId)) + { + _contact = contact; + return; + } + + var vector = contact.Point - _contact.Point; + var delta = (vector.X / UnitResolution) switch + { + >= 1 => 1, + <= -1 => -1, + _ => 0 + }; + if (delta == 0) + return; + + _contact = contact; + ManipulationDelta?.Invoke(_window, delta); + + await _complete.PushAsync(); + } + + private void Complete() + { + _contact = default; + ManipulationCompleted?.Invoke(_window, EventArgs.Empty); + } + } +} \ No newline at end of file diff --git a/Source/Monitorian/Properties/AssemblyInfo.cs b/Source/Monitorian/Properties/AssemblyInfo.cs index c2d6e2b1..40f3ade3 100644 --- a/Source/Monitorian/Properties/AssemblyInfo.cs +++ b/Source/Monitorian/Properties/AssemblyInfo.cs @@ -51,7 +51,7 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.18.1.0")] -[assembly: AssemblyFileVersion("2.18.1.0")] +[assembly: AssemblyVersion("2.19.0.0")] +[assembly: AssemblyFileVersion("2.19.0.0")] [assembly: Guid("a4cc5362-9b08-465b-ad64-5cfabc72a4c7")] [assembly: NeutralResourcesLanguage("en-US")]