From ab8b1d7cfda241bfc5456d93359b90bbc74e71b6 Mon Sep 17 00:00:00 2001 From: emoacht Date: Sun, 13 Jun 2021 07:20:05 +0900 Subject: [PATCH 1/9] Change serialization --- .../Monitorian.Core/Models/Monitor/DeviceContext.cs | 11 +++++++++-- Source/Monitorian.Core/Models/Monitor/MSMonitor.cs | 9 ++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Source/Monitorian.Core/Models/Monitor/DeviceContext.cs b/Source/Monitorian.Core/Models/Monitor/DeviceContext.cs index 9ecb2bb6..3ccb7011 100644 --- a/Source/Monitorian.Core/Models/Monitor/DeviceContext.cs +++ b/Source/Monitorian.Core/Models/Monitor/DeviceContext.cs @@ -220,11 +220,18 @@ public DeviceItem( [DataContract] public class HandleItem { - [DataMember] + [DataMember(Order = 0)] public int DisplayIndex { get; } - [DataMember] public Rect MonitorRect { get; } + [DataMember(Order = 1, Name = nameof(MonitorRect))] + private string _monitorRectString; + + [OnSerializing] + private void OnSerializing(StreamingContext context) + { + _monitorRectString = $"Location:{MonitorRect.Location}, Size:{MonitorRect.Size}"; + } public IntPtr MonitorHandle { get; } diff --git a/Source/Monitorian.Core/Models/Monitor/MSMonitor.cs b/Source/Monitorian.Core/Models/Monitor/MSMonitor.cs index a1191cf2..fca53d36 100644 --- a/Source/Monitorian.Core/Models/Monitor/MSMonitor.cs +++ b/Source/Monitorian.Core/Models/Monitor/MSMonitor.cs @@ -25,8 +25,15 @@ public class DesktopItem [DataMember(Order = 1)] public string Description { get; } - [DataMember(Order = 2)] public byte[] BrightnessLevels { get; } + [DataMember(Order = 2, Name = nameof(BrightnessLevels))] + private string _brightnessLevelsString; + + [OnSerializing] + private void OnSerializing(StreamingContext context) + { + _brightnessLevelsString = string.Join(" ", BrightnessLevels ?? Array.Empty()); + } public DesktopItem( string deviceInstanceId, From 31938c390600724c23bb5d110ac7707ad6e26c37 Mon Sep 17 00:00:00 2001 From: emoacht Date: Tue, 15 Jun 2021 16:10:43 +0900 Subject: [PATCH 2/9] Rename members for deferring update of brightness --- .../Views/Controls/Sliders/CompoundSlider.cs | 6 ++--- .../Views/Controls/Sliders/EnhancedSlider.cs | 24 +++++++++---------- Source/Monitorian.Core/Views/MainWindow.xaml | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) 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..a6a69031 100644 --- a/Source/Monitorian.Core/Views/Controls/Sliders/EnhancedSlider.cs +++ b/Source/Monitorian.Core/Views/Controls/Sliders/EnhancedSlider.cs @@ -112,7 +112,7 @@ protected override void OnPreviewMouseUp(MouseButtonEventArgs e) { base.OnPreviewMouseUp(e); - UpdateSourceDeferred(); + ExecuteUpdateSource(); } // OnPreviewStylusDown covers the case of OnPreviewTouchDown. @@ -289,7 +289,7 @@ protected override void OnManipulationCompleted(ManipulationCompletedEventArgs e { base.OnManipulationCompleted(e); - UpdateSourceDeferred(); + ExecuteUpdateSource(); } #endregion @@ -321,32 +321,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 +373,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..4843b1be 100644 --- a/Source/Monitorian.Core/Views/MainWindow.xaml +++ b/Source/Monitorian.Core/Views/MainWindow.xaml @@ -641,7 +641,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}" From 95c95a83baa3d9aa501d51a2176c5f774e795374 Mon Sep 17 00:00:00 2001 From: emoacht Date: Tue, 15 Jun 2021 13:48:46 +0900 Subject: [PATCH 3/9] Add touchpad functionality --- Source/Monitorian.Core/Monitorian.Core.csproj | 3 + .../Views/Touchpad/TouchpadContact.cs | 58 ++ .../Views/Touchpad/TouchpadHelper.cs | 504 ++++++++++++++++++ .../Views/Touchpad/TouchpadTracker.cs | 111 ++++ 4 files changed, 676 insertions(+) create mode 100644 Source/Monitorian.Core/Views/Touchpad/TouchpadContact.cs create mode 100644 Source/Monitorian.Core/Views/Touchpad/TouchpadHelper.cs create mode 100644 Source/Monitorian.Core/Views/Touchpad/TouchpadTracker.cs 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/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..703b34fd --- /dev/null +++ b/Source/Monitorian.Core/Views/Touchpad/TouchpadTracker.cs @@ -0,0 +1,111 @@ +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.ContactId != 0) || (_contact == contact)) + return; + + if (_contact == default) + { + _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 From 1e9ed267d98fed139fe20be8d3f74c57018eb3ab Mon Sep 17 00:00:00 2001 From: emoacht Date: Tue, 15 Jun 2021 16:23:04 +0900 Subject: [PATCH 4/9] Enable to adjust brightness by touchpad --- .../Views/Controls/Sliders/EnhancedSlider.cs | 7 +++++++ Source/Monitorian.Core/Views/MainWindow.xaml.cs | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/Source/Monitorian.Core/Views/Controls/Sliders/EnhancedSlider.cs b/Source/Monitorian.Core/Views/Controls/Sliders/EnhancedSlider.cs index a6a69031..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. 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) From d924bb2d71cdbd7a834f95b9b94b33b5e91d0092 Mon Sep 17 00:00:00 2001 From: emoacht Date: Tue, 15 Jun 2021 08:28:28 +0900 Subject: [PATCH 5/9] Remove manipulation on brightness button --- Source/Monitorian.Core/Views/MainWindow.xaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Source/Monitorian.Core/Views/MainWindow.xaml b/Source/Monitorian.Core/Views/MainWindow.xaml index 4843b1be..aed3354b 100644 --- a/Source/Monitorian.Core/Views/MainWindow.xaml +++ b/Source/Monitorian.Core/Views/MainWindow.xaml @@ -610,8 +610,7 @@ From 63a00dc16e9d934c8f4a9d68cd0ae23194607d94 Mon Sep 17 00:00:00 2001 From: emoacht Date: Tue, 15 Jun 2021 18:38:50 +0900 Subject: [PATCH 6/9] Add transmission failed case of set brightness method --- Source/Monitorian.Core/Models/Monitor/IMonitor.cs | 1 + Source/Monitorian.Core/Models/Monitor/MonitorConfiguration.cs | 2 ++ Source/Monitorian.Core/ViewModels/MonitorViewModel.cs | 1 + 3 files changed, 4 insertions(+) diff --git a/Source/Monitorian.Core/Models/Monitor/IMonitor.cs b/Source/Monitorian.Core/Models/Monitor/IMonitor.cs index 68ac5a51..160c2132 100644 --- a/Source/Monitorian.Core/Models/Monitor/IMonitor.cs +++ b/Source/Monitorian.Core/Models/Monitor/IMonitor.cs @@ -29,6 +29,7 @@ public enum AccessStatus Succeeded, Failed, DdcFailed, + TransmissionFailed, NoLongerExist } 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/ViewModels/MonitorViewModel.cs b/Source/Monitorian.Core/ViewModels/MonitorViewModel.cs index 3eb4bdee..359c2acf 100644 --- a/Source/Monitorian.Core/ViewModels/MonitorViewModel.cs +++ b/Source/Monitorian.Core/ViewModels/MonitorViewModel.cs @@ -241,6 +241,7 @@ private bool SetBrightness(int brightness) switch (result.Status) { case AccessStatus.DdcFailed: + case AccessStatus.TransmissionFailed: case AccessStatus.NoLongerExist: _controller.OnMonitorsChangeFound(); break; From 684124e9f698ef0752f64b08bcf74ebba3b762f9 Mon Sep 17 00:00:00 2001 From: emoacht Date: Tue, 15 Jun 2021 08:32:11 +0900 Subject: [PATCH 7/9] Increment minor version --- Source/Installer/Product.wxs | 2 +- Source/Monitorian.Core/Properties/AssemblyInfo.cs | 4 ++-- Source/Monitorian/Properties/AssemblyInfo.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) 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 @@  - Date: Wed, 16 Jun 2021 20:21:36 +0900 Subject: [PATCH 8/9] Modify to accept contact ID other than 0 --- Source/Monitorian.Core/Views/Touchpad/TouchpadTracker.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Source/Monitorian.Core/Views/Touchpad/TouchpadTracker.cs b/Source/Monitorian.Core/Views/Touchpad/TouchpadTracker.cs index 703b34fd..f38b9f23 100644 --- a/Source/Monitorian.Core/Views/Touchpad/TouchpadTracker.cs +++ b/Source/Monitorian.Core/Views/Touchpad/TouchpadTracker.cs @@ -77,10 +77,8 @@ public int UnitResolution private async void Check(TouchpadContact contact) { - if ((contact.ContactId != 0) || (_contact == contact)) - return; - - if (_contact == default) + if ((_contact == default) || + (_contact.ContactId != contact.ContactId)) { _contact = contact; return; From 63d55600942e397426a24a0d8a032e718b9cdc2d Mon Sep 17 00:00:00 2001 From: emoacht Date: Tue, 15 Jun 2021 18:54:14 +0900 Subject: [PATCH 9/9] Refactor --- .../Monitorian.Core/ViewModels/MonitorViewModel.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Source/Monitorian.Core/ViewModels/MonitorViewModel.cs b/Source/Monitorian.Core/ViewModels/MonitorViewModel.cs index 359c2acf..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); }