From ebe06b09b190a17a4cdae8cfc1390344f0164f38 Mon Sep 17 00:00:00 2001 From: Cookiesmuch Date: Wed, 14 Jan 2026 19:47:59 +0800 Subject: [PATCH 1/4] Add Smart Profiles feature with automatic profile switching Implemented profile management system with create, rename, duplicate, delete Added Smart Profile Switching to auto-switch based on active programs Created ProfilesSettings page with modern WinUI 3 card-based UI Detection modes: Windows Hook (instant) and Interval-based (fallback) Profile selector in top bar for quick switching Right-click context menu for profile actions Auto-text selection in dialogs for better UX --- ThreeFingerDragOnWindows/App.xaml.cs | 16 +- .../ThreeFingerDragOnWindows.csproj | 3 + .../settings/ProfilesSettings.xaml | 226 ++++++ .../settings/ProfilesSettings.xaml.cs | 672 ++++++++++++++++++ .../settings/SettingsData.cs | 183 ++++- .../settings/SettingsWindow.xaml | 100 ++- .../settings/SettingsWindow.xaml.cs | 303 +++++++- .../settings/SmartProfileSwitcher.cs | 236 ++++++ .../settings/ThreeFingerDragSettings.xaml.cs | 86 ++- .../profiles/ThreeFingerDragProfile.cs | 113 +++ .../threefingerdrag/DistanceManager.cs | 8 +- .../threefingerdrag/FingerCounter.cs | 8 +- .../threefingerdrag/ThreeFingerDrag.cs | 12 +- .../touchpad/HandlerWindow.xaml.cs | 2 +- .../utils/MouseOperations.cs | 17 +- 15 files changed, 1870 insertions(+), 115 deletions(-) create mode 100644 ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml create mode 100644 ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml.cs create mode 100644 ThreeFingerDragOnWindows/settings/SmartProfileSwitcher.cs create mode 100644 ThreeFingerDragOnWindows/settings/profiles/ThreeFingerDragProfile.cs diff --git a/ThreeFingerDragOnWindows/App.xaml.cs b/ThreeFingerDragOnWindows/App.xaml.cs index 648552b..ad419c5 100644 --- a/ThreeFingerDragOnWindows/App.xaml.cs +++ b/ThreeFingerDragOnWindows/App.xaml.cs @@ -10,13 +10,14 @@ namespace ThreeFingerDragOnWindows; public partial class App { - public readonly DispatcherQueue DispatcherQueue; +public readonly DispatcherQueue DispatcherQueue; - public static App Instance; - public static SettingsData SettingsData; - public static SettingsWindow SettingsWindow; +public static App Instance; +public static SettingsData SettingsData; +public static SettingsWindow SettingsWindow; +public static SmartProfileSwitcher SmartProfileSwitcher; - public HandlerWindow HandlerWindow; +public HandlerWindow HandlerWindow; public App(){ Instance = this; @@ -64,6 +65,11 @@ public App(){ } else{ HandlerWindow = new HandlerWindow(this); } + + // Start Smart Profile Switcher + SmartProfileSwitcher = new SmartProfileSwitcher(); + SmartProfileSwitcher.Start(); + Logger.Log("App initialization complete"); } diff --git a/ThreeFingerDragOnWindows/ThreeFingerDragOnWindows.csproj b/ThreeFingerDragOnWindows/ThreeFingerDragOnWindows.csproj index ef51503..1b6aee1 100644 --- a/ThreeFingerDragOnWindows/ThreeFingerDragOnWindows.csproj +++ b/ThreeFingerDragOnWindows/ThreeFingerDragOnWindows.csproj @@ -139,6 +139,9 @@ True \ + + MSBuild:Compile + diff --git a/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml b/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml new file mode 100644 index 0000000..a7842d0 --- /dev/null +++ b/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + @@ -73,6 +132,11 @@ + + + + + diff --git a/ThreeFingerDragOnWindows/settings/SettingsWindow.xaml.cs b/ThreeFingerDragOnWindows/settings/SettingsWindow.xaml.cs index 2a58ac8..8f7dedc 100644 --- a/ThreeFingerDragOnWindows/settings/SettingsWindow.xaml.cs +++ b/ThreeFingerDragOnWindows/settings/SettingsWindow.xaml.cs @@ -1,16 +1,21 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using Windows.Graphics; +using Microsoft.UI; +using Microsoft.UI.Input; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using ThreeFingerDragOnWindows.settings.profiles; using ThreeFingerDragOnWindows.utils; using Windows.ApplicationModel; using Windows.Foundation.Metadata; using System.Reflection; using ThreeFingerDragEngine.utils; +using WinUICommunity; namespace ThreeFingerDragOnWindows.settings; @@ -33,20 +38,78 @@ public SettingsWindow(App app, bool openOtherSettings){ ExtendsContentIntoTitleBar = true; // enable custom titlebar SetTitleBar(TitleBar); // set TitleBar element as titlebar + + // Set non-client regions after controls are fully loaded + ProfileComboBox.Loaded += (s, e) => SetupNonClientRegions(); - NavigationView.SelectedItem = openOtherSettings ? OtherSettings : Touchpad; + LoadProfiles(); + NavigationView.SelectedItem = openOtherSettings ? OtherSettings : Profiles; + } + + private void SetupNonClientRegions(){ + try{ + // Ensure controls are loaded + if(TitleBar?.XamlRoot == null || ProfileComboBox?.ActualWidth == 0){ + // Retry after a short delay if controls aren't ready + Utils.runOnMainThreadAfter(200, SetupNonClientRegions); + return; + } + + // Get the scale factor for the display + var scaleFactor = TitleBar.XamlRoot.RasterizationScale; + + // Create non-client regions for interactive controls + var nonClientRegions = new List(); + + // Add ComboBox region + var comboTransform = ProfileComboBox.TransformToVisual(null); + var comboPoint = comboTransform.TransformPoint(new Windows.Foundation.Point(0, 0)); + nonClientRegions.Add(new Windows.Graphics.RectInt32( + (int)(comboPoint.X * scaleFactor), + (int)(comboPoint.Y * scaleFactor), + (int)(ProfileComboBox.ActualWidth * scaleFactor), + (int)(ProfileComboBox.ActualHeight * scaleFactor) + )); + + // Add button regions + foreach(var button in new[] { CreateProfileButton, RenameProfileButton, DuplicateProfileButton }){ + if(button?.ActualWidth > 0){ + var transform = button.TransformToVisual(null); + var point = transform.TransformPoint(new Windows.Foundation.Point(0, 0)); + nonClientRegions.Add(new Windows.Graphics.RectInt32( + (int)(point.X * scaleFactor), + (int)(point.Y * scaleFactor), + (int)(button.ActualWidth * scaleFactor), + (int)(button.ActualHeight * scaleFactor) + )); + } + } + + // Apply non-client regions + if(AppWindow != null && AppWindow.TitleBar != null && nonClientRegions.Count > 0){ + var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(AppWindow.Id); + nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, nonClientRegions.ToArray()); + Logger.Log($"Non-client regions set successfully - {nonClientRegions.Count} regions"); + } + } catch(Exception ex){ + Logger.Log($"Error setting non-client regions: {ex.Message}"); + } } private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs e){ - if(e.SelectedItem.Equals(Touchpad)){ - sender.Header = "Touchpad"; - ContentFrame.Navigate(typeof(TouchpadSettings)); + if(e.SelectedItem.Equals(Profiles)){ + sender.Header = "Profiles"; + ContentFrame.Navigate(typeof(ProfilesSettings)); - }if(e.SelectedItem.Equals(ThreeFingerDrag)){ + } else if(e.SelectedItem.Equals(ThreeFingerDrag)){ sender.Header = "Three Finger Drag"; ContentFrame.Navigate(typeof(ThreeFingerDragSettings)); + } else if(e.SelectedItem.Equals(Touchpad)){ + sender.Header = "Touchpad"; + ContentFrame.Navigate(typeof(TouchpadSettings)); + } else if(e.SelectedItem.Equals(OtherSettings)){ sender.Header = "Other Settings"; ContentFrame.Navigate(typeof(OtherSettings)); @@ -54,6 +117,236 @@ private void NavigationView_SelectionChanged(NavigationView sender, NavigationVi } + ////////// Profile Management ////////// + + private bool _isLoadingProfiles = false; + + public void LoadProfiles(){ + _isLoadingProfiles = true; + + ProfileComboBox.Items.Clear(); + + // Load all profiles from the profiles directory + var profilesDir = Path.Combine(Windows.Storage.ApplicationData.Current.LocalFolder.Path, "profiles"); + if(Directory.Exists(profilesDir)){ + var profileFiles = Directory.GetFiles(profilesDir, "*.xml"); + foreach(var file in profileFiles){ + try{ + var profile = ThreeFingerDragProfile.Load(file); + ProfileComboBox.Items.Add(profile.ProfileName); + } catch(Exception e){ + Logger.Log($"Error loading profile {file}: {e.Message}"); + } + } + } + + // Select the active profile + try{ + var activeProfileName = App.SettingsData.ActiveProfile.ProfileName; + ProfileComboBox.SelectedItem = activeProfileName; + } catch(Exception e){ + Logger.Log($"Error selecting active profile: {e.Message}"); + // If there are any profiles loaded, select the first one + if(ProfileComboBox.Items.Count > 0){ + ProfileComboBox.SelectedIndex = 0; + } + } + + _isLoadingProfiles = false; + } + + private void ProfileComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e){ + if(_isLoadingProfiles) return; + + var selectedProfileName = ProfileComboBox.SelectedItem as string; + if(string.IsNullOrEmpty(selectedProfileName)) return; + + SwitchProfile(selectedProfileName); + } + + private void SwitchProfile(string profileName){ + var profilePath = SettingsData.GetProfileFilePath(profileName); + + if(!File.Exists(profilePath)){ + Logger.Log($"Profile file not found: {profilePath}"); + return; + } + + // Switch to the new profile + App.SettingsData.SwitchToProfile(profilePath); + + // Refresh the current page to show new profile settings + RefreshCurrentPage(); + } + + private async void CreateProfileButton_Click(object sender, RoutedEventArgs e){ + var dialog = new ContentDialog{ + XamlRoot = Content.XamlRoot, + Title = "Create New Profile", + PrimaryButtonText = "Create", + CloseButtonText = "Cancel", + DefaultButton = ContentDialogButton.Primary + }; + + var textBox = new TextBox{ + PlaceholderText = "Enter profile name", + Text = "New Profile" + }; + dialog.Content = textBox; + + var result = await dialog.ShowAsyncDraggable(); + + if(result == ContentDialogResult.Primary && !string.IsNullOrWhiteSpace(textBox.Text)){ + var newProfileName = textBox.Text.Trim(); + var newProfilePath = SettingsData.GetProfileFilePath(newProfileName); + + if(File.Exists(newProfilePath)){ + await ShowErrorDialog("A profile with this name already exists."); + return; + } + + // Create new profile with default settings + var newProfile = new ThreeFingerDragProfile{ + ProfileName = newProfileName + }; + newProfile.Save(newProfilePath); + + // Add to profiles list + App.SettingsData.Profiles.Add(new SettingsData.ProfileInfo{ + Name = newProfileName, + FilePath = newProfilePath + }); + App.SettingsData.save(); + + LoadProfiles(); + ProfileComboBox.SelectedItem = newProfileName; + + Logger.Log($"Created new profile: {newProfileName}"); + } + } + + private async void RenameProfileButton_Click(object sender, RoutedEventArgs e){ + var currentProfileName = App.SettingsData.ActiveProfile.ProfileName; + + var dialog = new ContentDialog{ + XamlRoot = Content.XamlRoot, + Title = "Rename Profile", + PrimaryButtonText = "Rename", + CloseButtonText = "Cancel", + DefaultButton = ContentDialogButton.Primary + }; + + var textBox = new TextBox{ + PlaceholderText = "Enter new profile name", + Text = currentProfileName + }; + dialog.Content = textBox; + + var result = await dialog.ShowAsyncDraggable(); + + if(result == ContentDialogResult.Primary && !string.IsNullOrWhiteSpace(textBox.Text)){ + var newProfileName = textBox.Text.Trim(); + + if(newProfileName == currentProfileName){ + return; // No change + } + + var newProfilePath = SettingsData.GetProfileFilePath(newProfileName); + if(File.Exists(newProfilePath)){ + await ShowErrorDialog("A profile with this name already exists."); + return; + } + + App.SettingsData.RenameActiveProfile(newProfileName); + LoadProfiles(); + + Logger.Log($"Renamed profile from {currentProfileName} to {newProfileName}"); + } + } + + private async void DuplicateProfileButton_Click(object sender, RoutedEventArgs e){ + var currentProfile = App.SettingsData.ActiveProfile; + var currentProfileName = currentProfile.ProfileName; + + var dialog = new ContentDialog{ + XamlRoot = Content.XamlRoot, + Title = "Duplicate Profile", + PrimaryButtonText = "Duplicate", + CloseButtonText = "Cancel", + DefaultButton = ContentDialogButton.Primary + }; + + var textBox = new TextBox{ + PlaceholderText = "Enter name for duplicated profile", + Text = currentProfileName + " Copy" + }; + dialog.Content = textBox; + + var result = await dialog.ShowAsyncDraggable(); + + if(result == ContentDialogResult.Primary && !string.IsNullOrWhiteSpace(textBox.Text)){ + var newProfileName = textBox.Text.Trim(); + var newProfilePath = SettingsData.GetProfileFilePath(newProfileName); + + if(File.Exists(newProfilePath)){ + await ShowErrorDialog("A profile with this name already exists."); + return; + } + + // Create a copy of the current profile + var duplicatedProfile = new ThreeFingerDragProfile{ + ProfileName = newProfileName, + ThreeFingerDrag = currentProfile.ThreeFingerDrag, + ThreeFingerDragButton = currentProfile.ThreeFingerDragButton, + ThreeFingerDragAllowReleaseAndRestart = currentProfile.ThreeFingerDragAllowReleaseAndRestart, + ThreeFingerDragReleaseDelay = currentProfile.ThreeFingerDragReleaseDelay, + ThreeFingerDragCursorMove = currentProfile.ThreeFingerDragCursorMove, + ThreeFingerDragCursorSpeed = currentProfile.ThreeFingerDragCursorSpeed, + ThreeFingerDragCursorAcceleration = currentProfile.ThreeFingerDragCursorAcceleration, + ThreeFingerDragCursorAveraging = currentProfile.ThreeFingerDragCursorAveraging, + ThreeFingerDragMaxFingerMoveDistance = currentProfile.ThreeFingerDragMaxFingerMoveDistance, + ThreeFingerDragStartThreshold = currentProfile.ThreeFingerDragStartThreshold, + ThreeFingerDragStopThreshold = currentProfile.ThreeFingerDragStopThreshold + }; + duplicatedProfile.Save(newProfilePath); + + // Add to profiles list + App.SettingsData.Profiles.Add(new SettingsData.ProfileInfo{ + Name = newProfileName, + FilePath = newProfilePath + }); + App.SettingsData.save(); + + LoadProfiles(); + ProfileComboBox.SelectedItem = newProfileName; + + Logger.Log($"Duplicated profile {currentProfileName} to {newProfileName}"); + } + } + + private async System.Threading.Tasks.Task ShowErrorDialog(string message){ + var errorDialog = new ContentDialog{ + XamlRoot = Content.XamlRoot, + Title = "Error", + Content = message, + CloseButtonText = "OK" + }; + await errorDialog.ShowAsyncDraggable(); + } + + private void RefreshCurrentPage(){ + // Re-navigate to current page to refresh bindings + if(NavigationView.SelectedItem.Equals(Profiles)){ + ContentFrame.Navigate(typeof(ProfilesSettings)); + } else if(NavigationView.SelectedItem.Equals(ThreeFingerDrag)){ + ContentFrame.Navigate(typeof(ThreeFingerDragSettings)); + } else if(NavigationView.SelectedItem.Equals(Touchpad)){ + ContentFrame.Navigate(typeof(TouchpadSettings)); + } else if(NavigationView.SelectedItem.Equals(OtherSettings)){ + ContentFrame.Navigate(typeof(OtherSettings)); + } + } + ////////// Close & quit ////////// private void CloseButton_Click(object sender, RoutedEventArgs e){ diff --git a/ThreeFingerDragOnWindows/settings/SmartProfileSwitcher.cs b/ThreeFingerDragOnWindows/settings/SmartProfileSwitcher.cs new file mode 100644 index 0000000..78362ce --- /dev/null +++ b/ThreeFingerDragOnWindows/settings/SmartProfileSwitcher.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Timers; +using ThreeFingerDragOnWindows.settings.profiles; +using ThreeFingerDragOnWindows.utils; + +namespace ThreeFingerDragOnWindows.settings; + +public class SmartProfileSwitcher{ +[DllImport("user32.dll")] +private static extern IntPtr GetForegroundWindow(); + +[DllImport("user32.dll")] +private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); + +[DllImport("user32.dll")] +private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, + WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags); + +[DllImport("user32.dll")] +private static extern bool UnhookWinEvent(IntPtr hWinEventHook); + +private const uint EVENT_SYSTEM_FOREGROUND = 3; +private const uint WINEVENT_OUTOFCONTEXT = 0; + +private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, + int idObject, int idChild, uint dwEventThread, uint dwmsEventTime); + +private readonly Timer _checkTimer; +private WinEventDelegate _hookDelegate; +private IntPtr _hookHandle; +private string _lastProcessPath = ""; +private bool _isEnabled = false; +private int _checkCount = 0; + + public SmartProfileSwitcher(){ + _checkTimer = new Timer(); + _checkTimer.Elapsed += OnTimerElapsed; + _checkTimer.AutoReset = true; + + // Keep delegate alive to prevent garbage collection + _hookDelegate = new WinEventDelegate(WinEventProc); + } + + public void Start(){ + if(!_isEnabled){ + _isEnabled = true; + + var mode = App.SettingsData.DetectionMode; + Logger.Log($"★★★ Smart Profile Switcher STARTED - mode: {mode} ★★★"); + + if(mode == SettingsData.ProfileSwitchingDetectionMode.WindowsHook){ + StartWindowsHook(); + } else { + StartIntervalCheck(); + } + + // Do an immediate check + CheckForegroundWindow(); + } + } + + private void StartWindowsHook(){ + _hookHandle = SetWinEventHook( + EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, + IntPtr.Zero, _hookDelegate, + 0, 0, WINEVENT_OUTOFCONTEXT); + + if(_hookHandle == IntPtr.Zero){ + Logger.Log("[SmartSwitcher] ERROR: Failed to set Windows hook, falling back to interval mode"); + StartIntervalCheck(); + } else { + Logger.Log("[SmartSwitcher] Windows hook installed successfully"); + } + } + + private void StartIntervalCheck(){ + var interval = App.SettingsData.DetectionInterval; + _checkTimer.Interval = interval; + _checkTimer.Start(); + Logger.Log($"[SmartSwitcher] Interval-based checking started ({interval}ms)"); + } + + public void Stop(){ + if(_isEnabled){ + _isEnabled = false; + + if(_hookHandle != IntPtr.Zero){ + UnhookWinEvent(_hookHandle); + _hookHandle = IntPtr.Zero; + Logger.Log("[SmartSwitcher] Windows hook uninstalled"); + } + + _checkTimer.Stop(); + Logger.Log("Smart Profile Switcher stopped"); + } + } + + public void Restart(){ + Logger.Log("[SmartSwitcher] Restarting with new settings..."); + Stop(); + Start(); + } + + private void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, + int idObject, int idChild, uint dwEventThread, uint dwmsEventTime){ + if(eventType == EVENT_SYSTEM_FOREGROUND){ + CheckForegroundWindow(); + } + } + + private void OnTimerElapsed(object sender, ElapsedEventArgs e){ + CheckForegroundWindow(); + } + + private void CheckForegroundWindow(){ + if(!_isEnabled) return; + + try{ + _checkCount++; + + var foregroundWindow = GetForegroundWindow(); + if(foregroundWindow == IntPtr.Zero){ + if(_checkCount % 10 == 1) Logger.Log("[SmartSwitcher] No foreground window detected"); + return; + } + + GetWindowThreadProcessId(foregroundWindow, out uint processId); + if(processId == 0){ + if(_checkCount % 10 == 1) Logger.Log("[SmartSwitcher] No process ID found"); + return; + } + + var process = Process.GetProcessById((int)processId); + var processPath = GetProcessPath(process); + var processName = process.ProcessName; + + if(string.IsNullOrEmpty(processPath)){ + if(_checkCount % 10 == 1) Logger.Log($"[SmartSwitcher] Could not get path for {processName}"); + return; + } + + if(processPath == _lastProcessPath){ + return; // Same as before, no change + } + + _lastProcessPath = processPath; + Logger.Log($"[SmartSwitcher] ►►► Foreground changed to {processName}"); + Logger.Log($"[SmartSwitcher] Path: {processPath}"); + + // Check if any profile is associated with this process + CheckAndSwitchProfile(processPath); + + } catch(Exception ex){ + Logger.Log($"[SmartSwitcher] ERROR: {ex.Message}"); + } + } + + private string GetProcessPath(Process process){ + try{ + return process.MainModule?.FileName; + } catch{ + return null; + } + } + + private void CheckAndSwitchProfile(string processPath){ + var profilesDir = Path.Combine(Windows.Storage.ApplicationData.Current.LocalFolder.Path, "profiles"); + if(!Directory.Exists(profilesDir)){ + Logger.Log("[SmartSwitcher] ERROR: Profiles directory not found!"); + return; + } + + var profileFiles = Directory.GetFiles(profilesDir, "*.xml"); + var currentProfilePath = App.SettingsData.ActiveProfilePath; + + Logger.Log($"[SmartSwitcher] Checking {profileFiles.Length} profiles..."); + Logger.Log($"[SmartSwitcher] Current active: {Path.GetFileName(currentProfilePath)}"); + + int profilesWithSmart = 0; + int totalPrograms = 0; + ThreeFingerDragProfile matchedProfile = null; + string matchedProfileFile = null; + + // First pass: find matching profile + foreach(var profileFile in profileFiles){ + try{ + var profile = ThreeFingerDragProfile.Load(profileFile); + var isActive = profileFile == currentProfilePath; + + Logger.Log($"[SmartSwitcher] Profile: '{profile.ProfileName}' {(isActive ? "(ACTIVE)" : "")}"); + Logger.Log($"[SmartSwitcher] Smart switching: {(profile.SmartSwitchingEnabled ? "YES" : "NO")}"); + + if(profile.SmartSwitchingEnabled){ + profilesWithSmart++; + Logger.Log($"[SmartSwitcher] Associated programs ({profile.AssociatedPrograms.Count}):"); + + foreach(var prog in profile.AssociatedPrograms){ + totalPrograms++; + var matches = string.Equals(prog, processPath, StringComparison.OrdinalIgnoreCase); + Logger.Log($"[SmartSwitcher] {(matches ? "✓ MATCH" : "-")} {prog}"); + + if(matches && !isActive){ + matchedProfile = profile; + matchedProfileFile = profileFile; + Logger.Log($"[SmartSwitcher] >>> Found match in '{profile.ProfileName}'!"); + } + } + } + } catch(Exception ex){ + Logger.Log($"[SmartSwitcher] ERROR loading {Path.GetFileName(profileFile)}: {ex.Message}"); + } + } + + // If we found a match, switch to it + if(matchedProfile != null && matchedProfileFile != null){ + Logger.Log($"[SmartSwitcher] ►►► SWITCHING to '{matchedProfile.ProfileName}'!"); + App.SettingsData.SwitchToProfile(matchedProfileFile); + + // Refresh settings window if open + App.Instance.DispatcherQueue.TryEnqueue(() => { + if(App.SettingsWindow is SettingsWindow settingsWindow){ + settingsWindow.LoadProfiles(); + } + }); + } else { + Logger.Log($"[SmartSwitcher] Summary: {profilesWithSmart} profiles with smart switching, {totalPrograms} total programs"); + Logger.Log($"[SmartSwitcher] No matching profile found for current process"); + } + } +} diff --git a/ThreeFingerDragOnWindows/settings/ThreeFingerDragSettings.xaml.cs b/ThreeFingerDragOnWindows/settings/ThreeFingerDragSettings.xaml.cs index c13cd61..3e8ada4 100644 --- a/ThreeFingerDragOnWindows/settings/ThreeFingerDragSettings.xaml.cs +++ b/ThreeFingerDragOnWindows/settings/ThreeFingerDragSettings.xaml.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using Microsoft.UI.Xaml; +using ThreeFingerDragOnWindows.settings.profiles; namespace ThreeFingerDragOnWindows.settings; @@ -23,92 +24,113 @@ private void OnPropertyChanged(string propertyName){ public bool EnabledProperty { - get{ return App.SettingsData.ThreeFingerDrag; } - set{ App.SettingsData.ThreeFingerDrag = value; } + get{ return App.SettingsData.ActiveProfile.ThreeFingerDrag; } + set{ + App.SettingsData.ActiveProfile.ThreeFingerDrag = value; + App.SettingsData.SaveActiveProfile(); + } } public int ButtonTypeProperty { - get{ return (int) App.SettingsData.ThreeFingerDragButton; } - set{ App.SettingsData.ThreeFingerDragButton = (SettingsData.ThreeFingerDragButtonType) value; } + get{ return (int) App.SettingsData.ActiveProfile.ThreeFingerDragButton; } + set{ + App.SettingsData.ActiveProfile.ThreeFingerDragButton = (ThreeFingerDragProfile.ThreeFingerDragButtonType) value; + App.SettingsData.SaveActiveProfile(); + } } public bool AllowReleaseAndRestartProperty { - get{ return App.SettingsData.ThreeFingerDragAllowReleaseAndRestart; } - set{ App.SettingsData.ThreeFingerDragAllowReleaseAndRestart = value; } + get{ return App.SettingsData.ActiveProfile.ThreeFingerDragAllowReleaseAndRestart; } + set{ + App.SettingsData.ActiveProfile.ThreeFingerDragAllowReleaseAndRestart = value; + App.SettingsData.SaveActiveProfile(); + } } public int ReleaseDelayProperty { - get{ return App.SettingsData.ThreeFingerDragReleaseDelay; } - set{ App.SettingsData.ThreeFingerDragReleaseDelay = value; } + get{ return App.SettingsData.ActiveProfile.ThreeFingerDragReleaseDelay; } + set{ + App.SettingsData.ActiveProfile.ThreeFingerDragReleaseDelay = value; + App.SettingsData.SaveActiveProfile(); + } } public bool CursorMoveProperty { - get{ return App.SettingsData.ThreeFingerDragCursorMove; } - set{ App.SettingsData.ThreeFingerDragCursorMove = value; } + get{ return App.SettingsData.ActiveProfile.ThreeFingerDragCursorMove; } + set{ + App.SettingsData.ActiveProfile.ThreeFingerDragCursorMove = value; + App.SettingsData.SaveActiveProfile(); + } } public float CursorSpeedProperty { - get{ return App.SettingsData.ThreeFingerDragCursorSpeed; } + get{ return App.SettingsData.ActiveProfile.ThreeFingerDragCursorSpeed; } set{ - if(App.SettingsData.ThreeFingerDragCursorSpeed != value){ - App.SettingsData.ThreeFingerDragCursorSpeed = value; + if(App.SettingsData.ActiveProfile.ThreeFingerDragCursorSpeed != value){ + App.SettingsData.ActiveProfile.ThreeFingerDragCursorSpeed = value; + App.SettingsData.SaveActiveProfile(); OnPropertyChanged(nameof(CursorSpeedProperty)); } } } public float CursorAccelerationProperty { - get{ return App.SettingsData.ThreeFingerDragCursorAcceleration; } + get{ return App.SettingsData.ActiveProfile.ThreeFingerDragCursorAcceleration; } set{ - if(App.SettingsData.ThreeFingerDragCursorAcceleration != value){ - App.SettingsData.ThreeFingerDragCursorAcceleration = value; + if(App.SettingsData.ActiveProfile.ThreeFingerDragCursorAcceleration != value){ + App.SettingsData.ActiveProfile.ThreeFingerDragCursorAcceleration = value; + App.SettingsData.SaveActiveProfile(); OnPropertyChanged(nameof(CursorAccelerationProperty)); } } } public int CursorAveragingProperty { - get{ return App.SettingsData.ThreeFingerDragCursorAveraging; } + get{ return App.SettingsData.ActiveProfile.ThreeFingerDragCursorAveraging; } set{ - if(App.SettingsData.ThreeFingerDragCursorAveraging != value){ - App.SettingsData.ThreeFingerDragCursorAveraging = value; + if(App.SettingsData.ActiveProfile.ThreeFingerDragCursorAveraging != value){ + App.SettingsData.ActiveProfile.ThreeFingerDragCursorAveraging = value; + App.SettingsData.SaveActiveProfile(); } } } public int StartDragThresholdProperty { - get{ return App.SettingsData.ThreeFingerDragStartThreshold; } + get{ return App.SettingsData.ActiveProfile.ThreeFingerDragStartThreshold; } set{ - if(App.SettingsData.ThreeFingerDragStartThreshold != value){ - App.SettingsData.ThreeFingerDragStartThreshold = value; + if(App.SettingsData.ActiveProfile.ThreeFingerDragStartThreshold != value){ + App.SettingsData.ActiveProfile.ThreeFingerDragStartThreshold = value; OnPropertyChanged(nameof(StartDragThresholdProperty)); - if(value < App.SettingsData.ThreeFingerDragStopThreshold){ - App.SettingsData.ThreeFingerDragStopThreshold = value; + if(value < App.SettingsData.ActiveProfile.ThreeFingerDragStopThreshold){ + App.SettingsData.ActiveProfile.ThreeFingerDragStopThreshold = value; OnPropertyChanged(nameof(StopDragThresholdProperty)); } + App.SettingsData.SaveActiveProfile(); } } } public int StopDragThresholdProperty { - get{ return App.SettingsData.ThreeFingerDragStopThreshold; } + get{ return App.SettingsData.ActiveProfile.ThreeFingerDragStopThreshold; } set{ - if(App.SettingsData.ThreeFingerDragStopThreshold != value){ - App.SettingsData.ThreeFingerDragStopThreshold = value; + if(App.SettingsData.ActiveProfile.ThreeFingerDragStopThreshold != value){ + App.SettingsData.ActiveProfile.ThreeFingerDragStopThreshold = value; OnPropertyChanged(nameof(StopDragThresholdProperty)); - if(value > App.SettingsData.ThreeFingerDragStartThreshold){ - App.SettingsData.ThreeFingerDragStartThreshold = value; + if(value > App.SettingsData.ActiveProfile.ThreeFingerDragStartThreshold){ + App.SettingsData.ActiveProfile.ThreeFingerDragStartThreshold = value; OnPropertyChanged(nameof(StartDragThresholdProperty)); } + App.SettingsData.SaveActiveProfile(); } } } public int MaxFingerMoveDistanceProperty { - get{ return App.SettingsData.ThreeFingerDragMaxFingerMoveDistance; } + get{ return App.SettingsData.ActiveProfile.ThreeFingerDragMaxFingerMoveDistance; } set{ - if(App.SettingsData.ThreeFingerDragMaxFingerMoveDistance != value){ - App.SettingsData.ThreeFingerDragMaxFingerMoveDistance = value; + if(App.SettingsData.ActiveProfile.ThreeFingerDragMaxFingerMoveDistance != value){ + App.SettingsData.ActiveProfile.ThreeFingerDragMaxFingerMoveDistance = value; + App.SettingsData.SaveActiveProfile(); OnPropertyChanged(nameof(MaxFingerMoveDistanceProperty)); } } diff --git a/ThreeFingerDragOnWindows/settings/profiles/ThreeFingerDragProfile.cs b/ThreeFingerDragOnWindows/settings/profiles/ThreeFingerDragProfile.cs new file mode 100644 index 0000000..30d81eb --- /dev/null +++ b/ThreeFingerDragOnWindows/settings/profiles/ThreeFingerDragProfile.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Xml.Serialization; +using ThreeFingerDragOnWindows.utils; + +namespace ThreeFingerDragOnWindows.settings.profiles; + +public class ThreeFingerDragProfile{ +// Profile metadata +public string ProfileName { get; set; } = "Default"; + +// Smart Profile Switching +public bool SmartSwitchingEnabled { get; set; } = false; +public List AssociatedPrograms { get; set; } = new List(); + +// Three finger drag Settings +public bool ThreeFingerDrag { get; set; } = true; + +public enum ThreeFingerDragButtonType { + NONE, + LEFT, + RIGHT, + MIDDLE, +} +public ThreeFingerDragButtonType ThreeFingerDragButton { get; set; } = ThreeFingerDragButtonType.LEFT; + + public bool ThreeFingerDragAllowReleaseAndRestart { get; set; } = true; + public int ThreeFingerDragReleaseDelay { get; set; } = 500; + + public bool ThreeFingerDragCursorMove { get; set; } = true; + public float ThreeFingerDragCursorSpeed { get; set; } = 30; + public float ThreeFingerDragCursorAcceleration { get; set; } = 10; + public int ThreeFingerDragCursorAveraging { get; set; } = 1; + public int ThreeFingerDragMaxFingerMoveDistance{ get; set; } = 0; + + public int ThreeFingerDragStartThreshold { get; set; } = 100; + public int ThreeFingerDragStopThreshold { get; set; } = 10; + + public static ThreeFingerDragProfile Load(string filePath){ + Logger.Log($"Loading profile from {filePath}..."); + + if(!File.Exists(filePath)){ + Logger.Log("Profile file not found, creating default profile"); + var defaultProfile = new ThreeFingerDragProfile(); + defaultProfile.Save(filePath); + return defaultProfile; + } + + var mySerializer = new XmlSerializer(typeof(ThreeFingerDragProfile)); + ThreeFingerDragProfile profile; + + try{ + using(var myFileStream = new FileStream(filePath, FileMode.Open)){ + profile = (ThreeFingerDragProfile)mySerializer.Deserialize(myFileStream); + } + Logger.Log($"Profile loaded: {profile.ProfileName}"); + } catch(Exception e){ + Logger.Log($"Error loading profile: {e.Message}"); + Console.WriteLine(e); + profile = new ThreeFingerDragProfile(); + profile.Save(filePath); + } + + return profile; + } + + public void Save(string filePath){ + Logger.Log($"Saving profile to {filePath}..."); + + var directory = Path.GetDirectoryName(filePath); + if(!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)){ + Directory.CreateDirectory(directory); + } + + var mySerializer = new XmlSerializer(typeof(ThreeFingerDragProfile)); + using(var myWriter = new StreamWriter(filePath)){ + mySerializer.Serialize(myWriter, this); + } + + + Logger.Log("Profile saved"); + } + + /// + /// Sanitizes a profile name to make it safe for use as a filename + /// + public static string SanitizeProfileName(string profileName){ + if(string.IsNullOrWhiteSpace(profileName)){ + return "Default"; + } + + // Remove invalid filename characters + var invalidChars = Path.GetInvalidFileNameChars(); + var sanitized = string.Join("_", profileName.Split(invalidChars, StringSplitOptions.RemoveEmptyEntries)); + + // Trim whitespace and limit length + sanitized = sanitized.Trim(); + if(sanitized.Length > 100){ + sanitized = sanitized.Substring(0, 100); + } + + // Ensure we have a valid name + if(string.IsNullOrWhiteSpace(sanitized)){ + return "Default"; + } + + return sanitized; + } +} + diff --git a/ThreeFingerDragOnWindows/threefingerdrag/DistanceManager.cs b/ThreeFingerDragOnWindows/threefingerdrag/DistanceManager.cs index c7396c2..d65b8ff 100644 --- a/ThreeFingerDragOnWindows/threefingerdrag/DistanceManager.cs +++ b/ThreeFingerDragOnWindows/threefingerdrag/DistanceManager.cs @@ -84,7 +84,7 @@ public class DistanceManager { public static Point ApplySpeedAndAcc(Point delta, int elapsed){ // Apply Speed - delta.Multiply(App.SettingsData.ThreeFingerDragCursorSpeed / 120); + delta.Multiply(App.SettingsData.ActiveProfile.ThreeFingerDragCursorSpeed / 120); // Calculate the mouse velocity : sort of a relative speed between 0 and 4, 1 being the average speed. var mouseVelocity = Math.Min(delta.Length() / elapsed, 4); @@ -92,10 +92,10 @@ public static Point ApplySpeedAndAcc(Point delta, int elapsed){ float pointerVelocity = 1; - if(App.SettingsData.ThreeFingerDragCursorAcceleration != 0){ + if(App.SettingsData.ActiveProfile.ThreeFingerDragCursorAcceleration != 0){ // Apply acceleration : function that transform the mouseVelocity into a pointerVelocity : 0.7+zs\left(2.6a\left(x-1+\frac{3-\ln\left(\frac{z}{0.3}-1\right)}{2.6a}\right)-3\right) // See https://www.desmos.com/calculator/khtj85jopn - float a = App.SettingsData.ThreeFingerDragCursorAcceleration / 10f; // Acceleration is multiplied by 10 in settings. + float a = App.SettingsData.ActiveProfile.ThreeFingerDragCursorAcceleration / 10f; // Acceleration is multiplied by 10 in settings. pointerVelocity = (float) (0.7 + 0.8 * Sigmoid(2.6 * a * (mouseVelocity - 1 + (3 - Math.Log2(0.8/0.3 - 1)) / (2.6 * a)) - 3)); // No need to clamp, the function gives values between 0.7 and 1.5. Logger.Log(" pointerVelocity: " + pointerVelocity + " (mouseVelocity: " + mouseVelocity + ")"); @@ -108,7 +108,7 @@ public static Point ApplySpeedAndAcc(Point delta, int elapsed){ } public static float ApplySpeed(float distance){ - distance *= App.SettingsData.ThreeFingerDragCursorSpeed / 60; + distance *= App.SettingsData.ActiveProfile.ThreeFingerDragCursorSpeed / 60; return distance; } diff --git a/ThreeFingerDragOnWindows/threefingerdrag/FingerCounter.cs b/ThreeFingerDragOnWindows/threefingerdrag/FingerCounter.cs index 9d1612d..601f4b8 100644 --- a/ThreeFingerDragOnWindows/threefingerdrag/FingerCounter.cs +++ b/ThreeFingerDragOnWindows/threefingerdrag/FingerCounter.cs @@ -23,9 +23,9 @@ public class FingerCounter { /// Whether if fingers has been released and replaced on the touchpad /// /// fingersCount : real number of fingers on the touchpad, or 0 if contacts changed - /// shortDelayMovingFingersCount : number of fingers that are on the touchpad and that have led to a moving distance higher than App.SettingsData.ThreeFingerDragStopThreshold + /// shortDelayMovingFingersCount : number of fingers that are on the touchpad and that have led to a moving distance higher than App.SettingsData.ActiveProfile.ThreeFingerDragStopThreshold /// Used to determine what is the real number of fingers on the touchpad when contacts changed - /// longDelayMovingFingersCount : number of fingers that are on the touchpad and that have led to a moving distance higher than App.SettingsData.ThreeFingerDragStartThreshold + /// longDelayMovingFingersCount : number of fingers that are on the touchpad and that have led to a moving distance higher than App.SettingsData.ActiveProfile.ThreeFingerDragStartThreshold /// Used to determine if the user has really started to drag /// originalFingersCount : number of original fingers on the touchpad after the short delay. /// This is updated only when contacts list length is <= 1 or when contacts have been released for more than RELEASE_FINGERS_THRESHOLD_MS ms. @@ -48,12 +48,12 @@ public class FingerCounter { _longDelayFingersMove += longestDist2D; } - if(_shortDelayFingersMove >= App.SettingsData.ThreeFingerDragStopThreshold){ + if(_shortDelayFingersMove >= App.SettingsData.ActiveProfile.ThreeFingerDragStopThreshold){ _shortDelayFingersCount = newContacts.Length; _shortDelayFingersMove = 0; } - if(_longDelayFingersMove > App.SettingsData.ThreeFingerDragStartThreshold){ + if(_longDelayFingersMove > App.SettingsData.ActiveProfile.ThreeFingerDragStartThreshold){ _longDelayFingersCount = newContacts.Length; _longDelayFingersMove = 0; if(_originalFingersCount <= 1){ diff --git a/ThreeFingerDragOnWindows/threefingerdrag/ThreeFingerDrag.cs b/ThreeFingerDragOnWindows/threefingerdrag/ThreeFingerDrag.cs index 3dd2aa4..3a3e061 100644 --- a/ThreeFingerDragOnWindows/threefingerdrag/ThreeFingerDrag.cs +++ b/ThreeFingerDragOnWindows/threefingerdrag/ThreeFingerDrag.cs @@ -52,17 +52,17 @@ public void OnTouchpadContact(TouchpadContact[] oldContacts, TouchpadContact[] c StopDrag(); } else if(fingersCount >= 2 && originalFingersCount == 3 && areContactsIdsCommons && _isDragging){ // Dragging - if(App.SettingsData.ThreeFingerDragCursorMove){ - if(App.SettingsData.ThreeFingerDragMaxFingerMoveDistance != 0 && longestDist2D > App.SettingsData.ThreeFingerDragMaxFingerMoveDistance){ + if(App.SettingsData.ActiveProfile.ThreeFingerDragCursorMove){ + if(App.SettingsData.ActiveProfile.ThreeFingerDragMaxFingerMoveDistance != 0 && longestDist2D > App.SettingsData.ActiveProfile.ThreeFingerDragMaxFingerMoveDistance){ Logger.Log(" DISCARDING MOVE, (x, y) = (" + longestDistDelta.x + ", " + longestDistDelta.y + ")"); } else if(!longestDistDelta.IsNull()){ Point delta = DistanceManager.ApplySpeedAndAcc(longestDistDelta, (int)elapsed); Logger.Log(" MOVING (avg), (x, y) = (" + longestDistDelta.x + ", " + longestDistDelta.y + ")"); - if(App.SettingsData.ThreeFingerDragCursorAveraging > 1){ + if(App.SettingsData.ActiveProfile.ThreeFingerDragCursorAveraging > 1){ _averagingX += delta.x; _averagingY += delta.y; _averagingCount++; - if(_averagingCount >= App.SettingsData.ThreeFingerDragCursorAveraging){ + if(_averagingCount >= App.SettingsData.ActiveProfile.ThreeFingerDragCursorAveraging){ Logger.Log(" MOVING (avg effectively), (x, y) = (" + longestDistDelta.x + ", " + longestDistDelta.y + ")"); MouseOperations.ShiftCursorPosition(_averagingX, _averagingY); _averagingX = 0; @@ -98,8 +98,8 @@ private void StopDrag(){ private int GetReleaseDelay(){ // Delay after which the click is released if no input is detected - return App.SettingsData.ThreeFingerDragAllowReleaseAndRestart - ? Math.Max(App.SettingsData.ThreeFingerDragReleaseDelay, RELEASE_FINGERS_THRESHOLD_MS) + return App.SettingsData.ActiveProfile.ThreeFingerDragAllowReleaseAndRestart + ? Math.Max(App.SettingsData.ActiveProfile.ThreeFingerDragReleaseDelay, RELEASE_FINGERS_THRESHOLD_MS) : RELEASE_FINGERS_THRESHOLD_MS; } } diff --git a/ThreeFingerDragOnWindows/touchpad/HandlerWindow.xaml.cs b/ThreeFingerDragOnWindows/touchpad/HandlerWindow.xaml.cs index b08b457..6fe16b5 100644 --- a/ThreeFingerDragOnWindows/touchpad/HandlerWindow.xaml.cs +++ b/ThreeFingerDragOnWindows/touchpad/HandlerWindow.xaml.cs @@ -66,7 +66,7 @@ public void OnTouchpadInitialized(bool touchpadExists, bool inputReceiverInstall private long _lastContactCtms = Ctms(); public void OnTouchpadContact(List contacts){ - if(App.SettingsData.ThreeFingerDrag){ + if(App.SettingsData.ActiveProfile.ThreeFingerDrag){ _threeFingersDrag.OnTouchpadContact(_oldContacts, contacts.ToArray(), Ctms() - _lastContactCtms); } diff --git a/ThreeFingerDragOnWindows/utils/MouseOperations.cs b/ThreeFingerDragOnWindows/utils/MouseOperations.cs index 3421214..967dd94 100644 --- a/ThreeFingerDragOnWindows/utils/MouseOperations.cs +++ b/ThreeFingerDragOnWindows/utils/MouseOperations.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using ThreeFingerDragOnWindows.settings; +using ThreeFingerDragOnWindows.settings.profiles; namespace ThreeFingerDragOnWindows.utils; @@ -76,28 +77,28 @@ public static void MouseClick(int mouseEventFlag){ } public static void ThreeFingersDragMouseDown(){ - switch (App.SettingsData.ThreeFingerDragButton){ - case SettingsData.ThreeFingerDragButtonType.LEFT: + switch (App.SettingsData.ActiveProfile.ThreeFingerDragButton){ + case ThreeFingerDragProfile.ThreeFingerDragButtonType.LEFT: MouseClick(MOUSEEVENTF_LEFTDOWN); break; - case SettingsData.ThreeFingerDragButtonType.RIGHT: + case ThreeFingerDragProfile.ThreeFingerDragButtonType.RIGHT: MouseClick(MOUSEEVENTF_RIGHTDOWN); break; - case SettingsData.ThreeFingerDragButtonType.MIDDLE: + case ThreeFingerDragProfile.ThreeFingerDragButtonType.MIDDLE: MouseClick(MOUSEEVENTF_MIDDLEDOWN); break; } } public static void ThreeFingersDragMouseUp(){ - switch (App.SettingsData.ThreeFingerDragButton){ - case SettingsData.ThreeFingerDragButtonType.LEFT: + switch (App.SettingsData.ActiveProfile.ThreeFingerDragButton){ + case ThreeFingerDragProfile.ThreeFingerDragButtonType.LEFT: MouseClick(MOUSEEVENTF_LEFTUP); break; - case SettingsData.ThreeFingerDragButtonType.RIGHT: + case ThreeFingerDragProfile.ThreeFingerDragButtonType.RIGHT: MouseClick(MOUSEEVENTF_RIGHTUP); break; - case SettingsData.ThreeFingerDragButtonType.MIDDLE: + case ThreeFingerDragProfile.ThreeFingerDragButtonType.MIDDLE: MouseClick(MOUSEEVENTF_MIDDLEUP); break; } From 07f353ba1ca468660db3430b4a1249533f55144e Mon Sep 17 00:00:00 2001 From: Cookiesmuch Date: Thu, 15 Jan 2026 14:15:08 +0800 Subject: [PATCH 2/4] Smart Profile Switching --- ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml | 1 + 1 file changed, 1 insertion(+) diff --git a/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml b/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml index a7842d0..a24f580 100644 --- a/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml +++ b/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml @@ -37,6 +37,7 @@ + Date: Thu, 15 Jan 2026 17:51:35 +0800 Subject: [PATCH 3/4] Improve touchpad device management for Bluetooth/wired mode switching - Add grace period for device removal to handle reconnections - Implement RefreshDevices() to re-enumerate devices on change events - Add device activity tracking (hasSentInput) to distinguish active vs stale devices - Extract normalized hardware IDs from device paths for better deduplication - Implement three-tier priority system for device deduplication: 1. Devices actively sending input 2. Newly discovered devices 3. Most recently seen devices - Add deviceName field to TouchpadDeviceInfo for connection type detection - Update ContactsManager to call RefreshDevices on WM_INPUT_DEVICE_CHANGE - Improve logging for device addition, removal, and deduplication These changes significantly improve handling of touchpads that switch between Bluetooth and wired connections, though edge cases with stale Windows drivers may still occur. --- ...5cd4b7bce24=$\357\200\277\357\200\242 } }" | 11 + .../settings/ProfilesSettings.xaml | 12 + .../settings/ProfilesSettings.xaml.cs | 99 ++++++-- .../settings/SettingsData.cs | 39 ---- .../settings/SettingsWindow.xaml.cs | 14 ++ .../settings/SmartProfileSwitcher.cs | 61 ++++- .../settings/ThreeFingerDragSettings.xaml | 110 +++++---- .../settings/ThreeFingerDragSettings.xaml.cs | 27 +-- .../settings/TouchpadSettings.xaml.cs | 3 + .../profiles/ThreeFingerDragProfile.cs | 26 ++- .../threefingerdrag/DistanceManager.cs | 16 +- .../threefingerdrag/ThreeFingerDrag.cs | 7 +- .../touchpad/ContactsManager.cs | 7 +- .../touchpad/HandlerWindow.xaml.cs | 21 ++ .../touchpad/MouseSpeedSettings.cs | 82 ------- .../touchpad/TouchpadDeviceInfo.cs | 2 + .../touchpad/TouchpadHelper.cs | 219 +++++++++++++++++- 17 files changed, 501 insertions(+), 255 deletions(-) create mode 100644 "; git status --short } finally { if ($\357\200\277) { echo \357\200\2421fd6400720414134a8ce15cd4b7bce24=$\357\200\277\357\200\242 } else { echo \357\200\2421fd6400720414134a8ce15cd4b7bce24=$\357\200\277\357\200\242 } }" delete mode 100644 ThreeFingerDragOnWindows/touchpad/MouseSpeedSettings.cs diff --git "a/; git status --short } finally { if ($\357\200\277) { echo \357\200\2421fd6400720414134a8ce15cd4b7bce24=$\357\200\277\357\200\242 } else { echo \357\200\2421fd6400720414134a8ce15cd4b7bce24=$\357\200\277\357\200\242 } }" "b/; git status --short } finally { if ($\357\200\277) { echo \357\200\2421fd6400720414134a8ce15cd4b7bce24=$\357\200\277\357\200\242 } else { echo \357\200\2421fd6400720414134a8ce15cd4b7bce24=$\357\200\277\357\200\242 } }" new file mode 100644 index 0000000..b0c9457 --- /dev/null +++ "b/; git status --short } finally { if ($\357\200\277) { echo \357\200\2421fd6400720414134a8ce15cd4b7bce24=$\357\200\277\357\200\242 } else { echo \357\200\2421fd6400720414134a8ce15cd4b7bce24=$\357\200\277\357\200\242 } }" @@ -0,0 +1,11 @@ + + SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS + + Commands marked with * may be preceded by a number, _N. + Notes in parentheses indicate the behavior if _N is given. + A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K. + + h H Display this help. + q :q Q :Q ZZ Exit. + --------------------------------------------------------------------------- + diff --git a/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml b/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml index a24f580..588a89b 100644 --- a/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml +++ b/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml @@ -133,6 +133,18 @@ Toggled="SmartSwitchingToggle_Toggled"/> + + + + + + + + diff --git a/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml.cs b/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml.cs index c4827c1..7399f88 100644 --- a/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml.cs +++ b/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml.cs @@ -10,6 +10,7 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using ThreeFingerDragOnWindows.settings.profiles; +using ThreeFingerDragOnWindows.touchpad; using ThreeFingerDragOnWindows.utils; using Windows.Storage; using Windows.Storage.Pickers; @@ -79,12 +80,49 @@ public sealed partial class ProfilesSettings : Page private ProfileViewModel _selectedProfile; private ProfileViewModel _rightClickedProfile; private ProfileViewModel _previousSelection; + private string _currentDeviceId = ""; + private string _currentDeviceName = "No device detected"; public ProfilesSettings() { InitializeComponent(); LoadProfiles(); LoadDetectionSettings(); + UpdateCurrentDevice(); + } + + private void UpdateCurrentDevice() + { + var devices = TouchpadHelper.GetAllDeivceInfos(); + var device = devices.FirstOrDefault(); + if (device != null) + { + _currentDeviceId = device.deviceId; + _currentDeviceName = $"{device.productId}:{device.vendorId}"; + } + else + { + _currentDeviceId = "default"; + _currentDeviceName = "No device detected"; + } + + CurrentDeviceTextBlock.Text = _currentDeviceName; + Logger.Log($"[ProfilesSettings] Current device: {_currentDeviceName} ({_currentDeviceId})"); + } + + /// + /// Refresh device info when devices change + /// + public void RefreshDeviceInfo() + { + Logger.Log("[ProfilesSettings] RefreshDeviceInfo called"); + UpdateCurrentDevice(); + + // Reload the current profile settings for the new device + if (_selectedProfile != null) + { + LoadProfileSettings(_selectedProfile); + } } private void LoadProfiles() @@ -105,12 +143,16 @@ private void LoadProfiles() try { var profile = ThreeFingerDragProfile.Load(file); + + // Check if ANY device has smart switching enabled for this profile + bool hasAnySmartSwitching = profile.DeviceSmartSwitchingConfigs?.Any(c => c.SmartSwitchingEnabled) ?? false; + _profiles.Add(new ProfileViewModel { Name = profile.ProfileName, FilePath = file, IsActive = file == activeProfilePath ? Visibility.Visible : Visibility.Collapsed, - HasSmartSwitching = profile.SmartSwitchingEnabled ? Visibility.Visible : Visibility.Collapsed + HasSmartSwitching = hasAnySmartSwitching ? Visibility.Visible : Visibility.Collapsed }); } catch (Exception ex) @@ -162,19 +204,31 @@ private void LoadProfileSettings(ProfileViewModel profileVm) var profile = ThreeFingerDragProfile.Load(profileVm.FilePath); ProfileNameTextBox.Text = profile.ProfileName; - SmartSwitchingToggle.IsOn = profile.SmartSwitchingEnabled; - SmartSwitchingOptions.Visibility = profile.SmartSwitchingEnabled ? Visibility.Visible : Visibility.Collapsed; + + // Use current device + var deviceConfig = profile.GetDeviceSmartSwitchingConfig(_currentDeviceId); + + SmartSwitchingToggle.IsOn = deviceConfig.SmartSwitchingEnabled; + SmartSwitchingOptions.Visibility = deviceConfig.SmartSwitchingEnabled ? Visibility.Visible : Visibility.Collapsed; // Auto-expand Smart Switching section if enabled - SmartSwitchingExpander.IsExpanded = profile.SmartSwitchingEnabled; + SmartSwitchingExpander.IsExpanded = deviceConfig.SmartSwitchingEnabled; - AssociatedProgramsListView.ItemsSource = new ObservableCollection(profile.AssociatedPrograms); + AssociatedProgramsListView.ItemsSource = new ObservableCollection(deviceConfig.AssociatedPrograms); + + Logger.Log($"[ProfilesSettings] Loaded profile '{profile.ProfileName}' for device {_currentDeviceName}"); + Logger.Log($"[ProfilesSettings] Smart switching: {deviceConfig.SmartSwitchingEnabled}, Programs: {deviceConfig.AssociatedPrograms.Count}"); } catch (Exception ex) { Logger.Log($"Error loading profile settings: {ex.Message}"); } } + + private string GetCurrentDeviceId() + { + return _currentDeviceId; + } private void ProfileNameTextBox_LostFocus(object sender, RoutedEventArgs e) { @@ -390,8 +444,13 @@ private async void DuplicateProfile_Click(object sender, RoutedEventArgs e) var duplicatedProfile = new ThreeFingerDragProfile { ProfileName = newProfileName, - SmartSwitchingEnabled = currentProfile.SmartSwitchingEnabled, - AssociatedPrograms = new List(currentProfile.AssociatedPrograms), + // Copy all device smart switching configs + DeviceSmartSwitchingConfigs = currentProfile.DeviceSmartSwitchingConfigs?.Select(c => new ThreeFingerDragProfile.DeviceSmartSwitchingConfig + { + DeviceId = c.DeviceId, + SmartSwitchingEnabled = c.SmartSwitchingEnabled, + AssociatedPrograms = new List(c.AssociatedPrograms) + }).ToList() ?? new List(), ThreeFingerDrag = currentProfile.ThreeFingerDrag, ThreeFingerDragButton = currentProfile.ThreeFingerDragButton, ThreeFingerDragAllowReleaseAndRestart = currentProfile.ThreeFingerDragAllowReleaseAndRestart, @@ -482,13 +541,18 @@ private void SmartSwitchingToggle_Toggled(object sender, RoutedEventArgs e) try { var profile = ThreeFingerDragProfile.Load(_selectedProfile.FilePath); - profile.SmartSwitchingEnabled = SmartSwitchingToggle.IsOn; + var currentDevice = GetCurrentDeviceId(); + var deviceConfig = profile.GetDeviceSmartSwitchingConfig(currentDevice); + + deviceConfig.SmartSwitchingEnabled = SmartSwitchingToggle.IsOn; profile.Save(_selectedProfile.FilePath); - _selectedProfile.HasSmartSwitching = SmartSwitchingToggle.IsOn ? Visibility.Visible : Visibility.Collapsed; + // Update visibility indicator + bool hasAnySmartSwitching = profile.DeviceSmartSwitchingConfigs?.Any(c => c.SmartSwitchingEnabled) ?? false; + _selectedProfile.HasSmartSwitching = hasAnySmartSwitching ? Visibility.Visible : Visibility.Collapsed; SmartSwitchingOptions.Visibility = SmartSwitchingToggle.IsOn ? Visibility.Visible : Visibility.Collapsed; - Logger.Log($"Smart switching {(SmartSwitchingToggle.IsOn ? "enabled" : "disabled")} for {_selectedProfile.Name}"); + Logger.Log($"Smart switching {(SmartSwitchingToggle.IsOn ? "enabled" : "disabled")} for {_selectedProfile.Name} on device {currentDevice}"); } catch (Exception ex) { @@ -565,16 +629,18 @@ private void AddProgramToProfile(string exePath) try { var profile = ThreeFingerDragProfile.Load(_selectedProfile.FilePath); + var currentDevice = GetCurrentDeviceId(); + var deviceConfig = profile.GetDeviceSmartSwitchingConfig(currentDevice); - if (!profile.AssociatedPrograms.Contains(exePath)) + if (!deviceConfig.AssociatedPrograms.Contains(exePath)) { - profile.AssociatedPrograms.Add(exePath); + deviceConfig.AssociatedPrograms.Add(exePath); profile.Save(_selectedProfile.FilePath); // Refresh UI LoadProfileSettings(_selectedProfile); - Logger.Log($"Added program {exePath} to profile {_selectedProfile.Name}"); + Logger.Log($"Added program {exePath} to profile {_selectedProfile.Name} for device {currentDevice}"); } } catch (Exception ex) @@ -593,13 +659,16 @@ private void RemoveProgram_Click(object sender, RoutedEventArgs e) try { var profile = ThreeFingerDragProfile.Load(_selectedProfile.FilePath); - profile.AssociatedPrograms.Remove(exePath); + var currentDevice = GetCurrentDeviceId(); + var deviceConfig = profile.GetDeviceSmartSwitchingConfig(currentDevice); + + deviceConfig.AssociatedPrograms.Remove(exePath); profile.Save(_selectedProfile.FilePath); // Refresh UI LoadProfileSettings(_selectedProfile); - Logger.Log($"Removed program {exePath} from profile {_selectedProfile.Name}"); + Logger.Log($"Removed program {exePath} from profile {_selectedProfile.Name} for device {currentDevice}"); } catch (Exception ex) { diff --git a/ThreeFingerDragOnWindows/settings/SettingsData.cs b/ThreeFingerDragOnWindows/settings/SettingsData.cs index afea9fd..baab2dd 100644 --- a/ThreeFingerDragOnWindows/settings/SettingsData.cs +++ b/ThreeFingerDragOnWindows/settings/SettingsData.cs @@ -188,40 +188,6 @@ private static string GetProfilesDirectory() return dirPath; } - public class ThreeFingerDragConfig - { - public ThreeFingerDragConfig() - { - } - - public ThreeFingerDragConfig(bool cursorMoveProperty, float cursorSpeedProperty, float cursorAccelerationProperty) - { - ThreeFingerDragCursorMove = cursorMoveProperty; - ThreeFingerDragCursorSpeed = cursorSpeedProperty; - ThreeFingerDragCursorAcceleration = cursorAccelerationProperty; - } - - public bool ThreeFingerDragCursorMove { get; set; } = true; - public float ThreeFingerDragCursorSpeed { get; set; } = 30; - public float ThreeFingerDragCursorAcceleration { get; set; } = 10; - } - - public Dictionary ThreeFingerDeviceDragCursorConfigs { get; set; } = new(); - - public ThreeFingerDragConfig GetDeviceDragConfig(string deviceId) - { - if (ThreeFingerDeviceDragCursorConfigs != null && ThreeFingerDeviceDragCursorConfigs.TryGetValue(deviceId, out var config)) - { - return config; - } - - return new ThreeFingerDragConfig( - ActiveProfile.ThreeFingerDragCursorMove, - ActiveProfile.ThreeFingerDragCursorSpeed, - ActiveProfile.ThreeFingerDragCursorAcceleration - ); - } - public enum StartupActionType { NONE, @@ -267,11 +233,6 @@ public static SettingsData load() } up.Profiles ??= new List(); - if (up.ThreeFingerDeviceDragCursorConfigs == null) - { - up.ThreeFingerDeviceDragCursorConfigs = new Dictionary(2); - up.save(); - } if (up.SettingsVersion < 1) { diff --git a/ThreeFingerDragOnWindows/settings/SettingsWindow.xaml.cs b/ThreeFingerDragOnWindows/settings/SettingsWindow.xaml.cs index 40f843d..e14fc87 100644 --- a/ThreeFingerDragOnWindows/settings/SettingsWindow.xaml.cs +++ b/ThreeFingerDragOnWindows/settings/SettingsWindow.xaml.cs @@ -346,6 +346,20 @@ private void RefreshCurrentPage(){ ContentFrame.Navigate(typeof(OtherSettings)); } } + + /// + /// Called when touchpad devices change (connect/disconnect) + /// + public void OnDeviceChanged(){ + Logger.Log("[SettingsWindow] Device changed, refreshing current page"); + + // Refresh the current page if it's ProfilesSettings or TouchpadSettings + if(ContentFrame.Content is ProfilesSettings profilesSettings){ + profilesSettings.RefreshDeviceInfo(); + } else if(ContentFrame.Content is TouchpadSettings touchpadSettings){ + touchpadSettings.OnTouchpadInitialized(); + } + } ////////// Close & quit ////////// diff --git a/ThreeFingerDragOnWindows/settings/SmartProfileSwitcher.cs b/ThreeFingerDragOnWindows/settings/SmartProfileSwitcher.cs index 78362ce..dfb1550 100644 --- a/ThreeFingerDragOnWindows/settings/SmartProfileSwitcher.cs +++ b/ThreeFingerDragOnWindows/settings/SmartProfileSwitcher.cs @@ -7,6 +7,7 @@ using System.Text; using System.Timers; using ThreeFingerDragOnWindows.settings.profiles; +using ThreeFingerDragOnWindows.touchpad; using ThreeFingerDragOnWindows.utils; namespace ThreeFingerDragOnWindows.settings; @@ -37,6 +38,23 @@ private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, Int private string _lastProcessPath = ""; private bool _isEnabled = false; private int _checkCount = 0; +private string _currentDeviceId = ""; + +public void SetCurrentDevice(string deviceId) +{ + if (_currentDeviceId != deviceId) + { + Logger.Log($"[SmartSwitcher] ►►► DEVICE CHANGED: '{_currentDeviceId}' -> '{deviceId}'"); + _currentDeviceId = deviceId; + + // Re-check foreground window with new device + if (_isEnabled) + { + _lastProcessPath = ""; // Force re-check + CheckForegroundWindow(); + } + } +} public SmartProfileSwitcher(){ _checkTimer = new Timer(); @@ -170,6 +188,26 @@ private string GetProcessPath(Process process){ } private void CheckAndSwitchProfile(string processPath){ + Logger.Log($"[SmartSwitcher] === CheckAndSwitchProfile called ==="); + Logger.Log($"[SmartSwitcher] Process: {processPath}"); + Logger.Log($"[SmartSwitcher] Current device: '{_currentDeviceId}'"); + + if (string.IsNullOrEmpty(_currentDeviceId)) + { + // Try to auto-detect device + var devices = TouchpadHelper.GetAllDeivceInfos(); + if (devices.Count > 0) + { + _currentDeviceId = devices[0].deviceId; + Logger.Log($"[SmartSwitcher] ✓ Auto-detected device: {_currentDeviceId}"); + } + else + { + Logger.Log("[SmartSwitcher] ⚠️ No current device set and no devices detected, skipping profile check"); + return; + } + } + var profilesDir = Path.Combine(Windows.Storage.ApplicationData.Current.LocalFolder.Path, "profiles"); if(!Directory.Exists(profilesDir)){ Logger.Log("[SmartSwitcher] ERROR: Profiles directory not found!"); @@ -179,7 +217,7 @@ private void CheckAndSwitchProfile(string processPath){ var profileFiles = Directory.GetFiles(profilesDir, "*.xml"); var currentProfilePath = App.SettingsData.ActiveProfilePath; - Logger.Log($"[SmartSwitcher] Checking {profileFiles.Length} profiles..."); + Logger.Log($"[SmartSwitcher] Checking {profileFiles.Length} profiles for device: {_currentDeviceId}..."); Logger.Log($"[SmartSwitcher] Current active: {Path.GetFileName(currentProfilePath)}"); int profilesWithSmart = 0; @@ -187,20 +225,23 @@ private void CheckAndSwitchProfile(string processPath){ ThreeFingerDragProfile matchedProfile = null; string matchedProfileFile = null; - // First pass: find matching profile + // First pass: find matching profile for this device foreach(var profileFile in profileFiles){ try{ var profile = ThreeFingerDragProfile.Load(profileFile); var isActive = profileFile == currentProfilePath; + // Get device-specific smart switching config + var deviceConfig = profile.GetDeviceSmartSwitchingConfig(_currentDeviceId); + Logger.Log($"[SmartSwitcher] Profile: '{profile.ProfileName}' {(isActive ? "(ACTIVE)" : "")}"); - Logger.Log($"[SmartSwitcher] Smart switching: {(profile.SmartSwitchingEnabled ? "YES" : "NO")}"); + Logger.Log($"[SmartSwitcher] Device smart switching: {(deviceConfig.SmartSwitchingEnabled ? "YES" : "NO")}"); - if(profile.SmartSwitchingEnabled){ + if(deviceConfig.SmartSwitchingEnabled){ profilesWithSmart++; - Logger.Log($"[SmartSwitcher] Associated programs ({profile.AssociatedPrograms.Count}):"); + Logger.Log($"[SmartSwitcher] Associated programs for device ({deviceConfig.AssociatedPrograms.Count}):"); - foreach(var prog in profile.AssociatedPrograms){ + foreach(var prog in deviceConfig.AssociatedPrograms){ totalPrograms++; var matches = string.Equals(prog, processPath, StringComparison.OrdinalIgnoreCase); Logger.Log($"[SmartSwitcher] {(matches ? "✓ MATCH" : "-")} {prog}"); @@ -208,7 +249,7 @@ private void CheckAndSwitchProfile(string processPath){ if(matches && !isActive){ matchedProfile = profile; matchedProfileFile = profileFile; - Logger.Log($"[SmartSwitcher] >>> Found match in '{profile.ProfileName}'!"); + Logger.Log($"[SmartSwitcher] >>> Found match in '{profile.ProfileName}' for device!"); } } } @@ -219,7 +260,7 @@ private void CheckAndSwitchProfile(string processPath){ // If we found a match, switch to it if(matchedProfile != null && matchedProfileFile != null){ - Logger.Log($"[SmartSwitcher] ►►► SWITCHING to '{matchedProfile.ProfileName}'!"); + Logger.Log($"[SmartSwitcher] ►►► SWITCHING to '{matchedProfile.ProfileName}' for device {_currentDeviceId}!"); App.SettingsData.SwitchToProfile(matchedProfileFile); // Refresh settings window if open @@ -229,8 +270,8 @@ private void CheckAndSwitchProfile(string processPath){ } }); } else { - Logger.Log($"[SmartSwitcher] Summary: {profilesWithSmart} profiles with smart switching, {totalPrograms} total programs"); - Logger.Log($"[SmartSwitcher] No matching profile found for current process"); + Logger.Log($"[SmartSwitcher] Summary: {profilesWithSmart} profiles with smart switching for device, {totalPrograms} total programs"); + Logger.Log($"[SmartSwitcher] No matching profile found for current process on device {_currentDeviceId}"); } } } diff --git a/ThreeFingerDragOnWindows/settings/ThreeFingerDragSettings.xaml b/ThreeFingerDragOnWindows/settings/ThreeFingerDragSettings.xaml index 2e9ce88..efa0ce8 100644 --- a/ThreeFingerDragOnWindows/settings/ThreeFingerDragSettings.xaml +++ b/ThreeFingerDragOnWindows/settings/ThreeFingerDragSettings.xaml @@ -103,69 +103,63 @@ + - - - - - - - - - - - + + + + + + + + - - - - - - - - + + + + + + + + - - - - + + + + - - - - - + MouseSpeedSettingItems - { - get - { - var settings = new ObservableCollection(); - var allDeviceInfos = TouchpadHelper.GetAllDeivceInfos(); - - foreach (var device in allDeviceInfos) - { - var config = App.SettingsData.GetDeviceDragConfig(device.deviceId); - settings.Add(new MouseSpeedSettings(config, device)); - } - - settings.CollectionChanged += OnCollectionChanged; - return settings; - } - } - public float CursorAccelerationProperty { get{ return App.SettingsData.ActiveProfile.ThreeFingerDragCursorAcceleration; } set{ @@ -108,11 +88,6 @@ public float CursorAccelerationProperty { } } - private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(MouseSpeedSettingItems)); - } - public int CursorAveragingProperty { get{ return App.SettingsData.ActiveProfile.ThreeFingerDragCursorAveraging; } set{ diff --git a/ThreeFingerDragOnWindows/settings/TouchpadSettings.xaml.cs b/ThreeFingerDragOnWindows/settings/TouchpadSettings.xaml.cs index 5f97f52..9a10e9f 100644 --- a/ThreeFingerDragOnWindows/settings/TouchpadSettings.xaml.cs +++ b/ThreeFingerDragOnWindows/settings/TouchpadSettings.xaml.cs @@ -25,6 +25,9 @@ public void OnTouchpadInitialized(){ Loader.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed; TouchpadStatus.Visibility = Microsoft.UI.Xaml.Visibility.Visible; + // Clean up any disconnected devices before displaying + TouchpadHelper.CleanupDisconnectedDevices(); + if(App.Instance.HandlerWindow.TouchpadExists){ if(App.Instance.HandlerWindow.InputReceiverInstalled) { string deviceInfosString = String.Join("\n", TouchpadHelper.GetAllDeivceInfos().Select(deviceInfo => deviceInfo.ToString())); diff --git a/ThreeFingerDragOnWindows/settings/profiles/ThreeFingerDragProfile.cs b/ThreeFingerDragOnWindows/settings/profiles/ThreeFingerDragProfile.cs index 30d81eb..35d0a05 100644 --- a/ThreeFingerDragOnWindows/settings/profiles/ThreeFingerDragProfile.cs +++ b/ThreeFingerDragOnWindows/settings/profiles/ThreeFingerDragProfile.cs @@ -12,9 +12,29 @@ public class ThreeFingerDragProfile{ // Profile metadata public string ProfileName { get; set; } = "Default"; -// Smart Profile Switching -public bool SmartSwitchingEnabled { get; set; } = false; -public List AssociatedPrograms { get; set; } = new List(); +// Smart Profile Switching - per device +public class DeviceSmartSwitchingConfig +{ + public string DeviceId { get; set; } = ""; + public bool SmartSwitchingEnabled { get; set; } = false; + public List AssociatedPrograms { get; set; } = new List(); +} + +public List DeviceSmartSwitchingConfigs { get; set; } = new List(); + +/// +/// Get or create smart switching config for a specific device +/// +public DeviceSmartSwitchingConfig GetDeviceSmartSwitchingConfig(string deviceId) +{ + var config = DeviceSmartSwitchingConfigs.FirstOrDefault(c => c.DeviceId == deviceId); + if (config == null) + { + config = new DeviceSmartSwitchingConfig { DeviceId = deviceId }; + DeviceSmartSwitchingConfigs.Add(config); + } + return config; +} // Three finger drag Settings public bool ThreeFingerDrag { get; set; } = true; diff --git a/ThreeFingerDragOnWindows/threefingerdrag/DistanceManager.cs b/ThreeFingerDragOnWindows/threefingerdrag/DistanceManager.cs index 3d609f7..366f13e 100644 --- a/ThreeFingerDragOnWindows/threefingerdrag/DistanceManager.cs +++ b/ThreeFingerDragOnWindows/threefingerdrag/DistanceManager.cs @@ -4,7 +4,6 @@ using System.Linq; using ThreeFingerDragEngine.utils; using ThreeFingerDragOnWindows.settings; -using ThreeFingerDragOnWindows.touchpad; using ThreeFingerDragOnWindows.utils; namespace ThreeFingerDragOnWindows.threefingerdrag; @@ -85,11 +84,8 @@ public class DistanceManager { public static Point ApplySpeedAndAcc(IntPtr currentDevice, Point delta, int elapsed){ - var deviceInfo = TouchpadHelper.GetDeivceInfo(currentDevice); - var config = App.SettingsData.GetDeviceDragConfig(deviceInfo.deviceId); - - // Apply Speed - delta.Multiply(config.ThreeFingerDragCursorSpeed / 120); + // Apply Speed from active profile + delta.Multiply(App.SettingsData.ActiveProfile.ThreeFingerDragCursorSpeed / 120); // Calculate the mouse velocity : sort of a relative speed between 0 and 4, 1 being the average speed. var mouseVelocity = Math.Min(delta.Length() / elapsed, 4); @@ -97,10 +93,10 @@ public static Point ApplySpeedAndAcc(IntPtr currentDevice, Point delta, int elap float pointerVelocity = 1; - if(config.ThreeFingerDragCursorAcceleration != 0){ + if(App.SettingsData.ActiveProfile.ThreeFingerDragCursorAcceleration != 0){ // Apply acceleration : function that transform the mouseVelocity into a pointerVelocity : 0.7+zs\left(2.6a\left(x-1+\frac{3-\ln\left(\frac{z}{0.3}-1\right)}{2.6a}\right)-3\right) // See https://www.desmos.com/calculator/khtj85jopn - float a = config.ThreeFingerDragCursorAcceleration / 10f; // Acceleration is multiplied by 10 in settings. + float a = App.SettingsData.ActiveProfile.ThreeFingerDragCursorAcceleration / 10f; // Acceleration is multiplied by 10 in settings. pointerVelocity = (float)(0.7 + 0.8 * Sigmoid(2.6 * a * (mouseVelocity - 1 + (3 - Math.Log2(0.8 / 0.3 - 1)) / (2.6 * a)) - 3)); // No need to clamp, the function gives values between 0.7 and 1.5. Logger.Log(" pointerVelocity: " + pointerVelocity + " (mouseVelocity: " + mouseVelocity + ")"); @@ -113,9 +109,7 @@ public static Point ApplySpeedAndAcc(IntPtr currentDevice, Point delta, int elap } public static float ApplySpeed(IntPtr currentDevice, float distance){ - var deviceInfo = TouchpadHelper.GetDeivceInfo(currentDevice); - var config = App.SettingsData.GetDeviceDragConfig(deviceInfo.deviceId); - distance *= config.ThreeFingerDragCursorSpeed / 60; + distance *= App.SettingsData.ActiveProfile.ThreeFingerDragCursorSpeed / 60; return distance; } diff --git a/ThreeFingerDragOnWindows/threefingerdrag/ThreeFingerDrag.cs b/ThreeFingerDragOnWindows/threefingerdrag/ThreeFingerDrag.cs index 4296758..a531896 100644 --- a/ThreeFingerDragOnWindows/threefingerdrag/ThreeFingerDrag.cs +++ b/ThreeFingerDragOnWindows/threefingerdrag/ThreeFingerDrag.cs @@ -4,7 +4,6 @@ using System.Timers; using ThreeFingerDragEngine.utils; using ThreeFingerDragOnWindows.settings; -using ThreeFingerDragOnWindows.touchpad; using ThreeFingerDragOnWindows.utils; namespace ThreeFingerDragOnWindows.threefingerdrag; @@ -27,8 +26,6 @@ public ThreeFingerDrag(){ private int _averagingCount = 0; public void OnTouchpadContact(IntPtr currentDevice, TouchpadContact[] oldContacts, TouchpadContact[] contacts, long elapsed){ - var deviceInfo = TouchpadHelper.GetDeivceInfo(currentDevice); - var deviceConfig = App.SettingsData.GetDeviceDragConfig(deviceInfo.deviceId); bool hasFingersReleased = elapsed > RELEASE_FINGERS_THRESHOLD_MS; Logger.Log("TFD: " + string.Join(", ", oldContacts.Select(c => c.ToString())) + " | " + string.Join(", ", contacts.Select(c => c.ToString())) + " | " + elapsed); @@ -56,8 +53,8 @@ public void OnTouchpadContact(IntPtr currentDevice, TouchpadContact[] oldContact Logger.Log(" STOP DRAG, click up"); StopDrag(); } else if(fingersCount >= 2 && originalFingersCount == 3 && areContactsIdsCommons && _isDragging){ - // Dragging - if(deviceConfig.ThreeFingerDragCursorMove){ + // Dragging - use profile settings + if(App.SettingsData.ActiveProfile.ThreeFingerDragCursorMove){ if(App.SettingsData.ActiveProfile.ThreeFingerDragMaxFingerMoveDistance != 0 && longestDist2D > App.SettingsData.ActiveProfile.ThreeFingerDragMaxFingerMoveDistance){ Logger.Log(" DISCARDING MOVE, (x, y) = (" + longestDistDelta.x + ", " + longestDistDelta.y + ")"); } else if(!longestDistDelta.IsNull()){ diff --git a/ThreeFingerDragOnWindows/touchpad/ContactsManager.cs b/ThreeFingerDragOnWindows/touchpad/ContactsManager.cs index e9825b9..c340f66 100644 --- a/ThreeFingerDragOnWindows/touchpad/ContactsManager.cs +++ b/ThreeFingerDragOnWindows/touchpad/ContactsManager.cs @@ -37,7 +37,12 @@ private IntPtr WindowProcess(IntPtr hwnd, uint message, IntPtr wParam, IntPtr lP ReceiveTouchpadContacts(currentDevice, contacts, count); break; case TouchpadHelper.WM_INPUT_DEVICE_CHANGE: - _source.OnTouchpadInitialized(TouchpadHelper.Exists(lParam), true); + Logger.Log($"[ContactsManager] Device change detected, wParam: {wParam}, lParam: {lParam}"); + // Refresh the device list to handle reconnections (e.g., Bluetooth to wired mode switch) + TouchpadHelper.RefreshDevices(); + // Check if ANY touchpad exists after refresh, not just the specific device from the event + // (lParam might be the disconnecting device, not the newly connected one) + _source.OnTouchpadInitialized(TouchpadHelper.Exists(), true); break; } diff --git a/ThreeFingerDragOnWindows/touchpad/HandlerWindow.xaml.cs b/ThreeFingerDragOnWindows/touchpad/HandlerWindow.xaml.cs index 522011a..b976578 100644 --- a/ThreeFingerDragOnWindows/touchpad/HandlerWindow.xaml.cs +++ b/ThreeFingerDragOnWindows/touchpad/HandlerWindow.xaml.cs @@ -6,6 +6,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Input; using ThreeFingerDragEngine.utils; +using ThreeFingerDragOnWindows.settings; using ThreeFingerDragOnWindows.threefingerdrag; using ThreeFingerDragOnWindows.utils; @@ -57,7 +58,23 @@ public void OnTouchpadInitialized(bool touchpadExists, bool inputReceiverInstall else Logger.Log("Touchpad is detected and registered."); TouchpadInitialized = true; + + // Initialize SmartProfileSwitcher with device info + if(touchpadExists && inputReceiverInstalled){ + var devices = TouchpadHelper.GetAllDeivceInfos(); + if(devices.Count > 0){ + var device = devices[0]; + Logger.Log($"[HandlerWindow] Setting initial device: {device.deviceId}"); + App.SmartProfileSwitcher?.SetCurrentDevice(device.deviceId); + } + } + _app.OnTouchpadInitialized(); + + // Notify SettingsWindow about device change + if(App.SettingsWindow is SettingsWindow settingsWindow){ + settingsWindow.OnDeviceChanged(); + } } // Called when a new set of contacts has been registered @@ -70,6 +87,10 @@ public void OnTouchpadContact(IntPtr currentDevice, List contac _threeFingersDrag.OnTouchpadContact(currentDevice, _oldContacts, contacts.ToArray(), Ctms() - _lastContactCtms); } + // Update smart profile switcher with current device + var deviceInfo = TouchpadHelper.GetDeivceInfo(currentDevice); + App.SmartProfileSwitcher?.SetCurrentDevice(deviceInfo.deviceId); + _app.OnTouchpadContact(currentDevice, contacts.ToArray()); // Transfer to App for displaying contacts in SettingsWindow _lastContactCtms = Ctms(); _oldContacts = contacts.ToArray(); diff --git a/ThreeFingerDragOnWindows/touchpad/MouseSpeedSettings.cs b/ThreeFingerDragOnWindows/touchpad/MouseSpeedSettings.cs deleted file mode 100644 index f38ea7a..0000000 --- a/ThreeFingerDragOnWindows/touchpad/MouseSpeedSettings.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Runtime.CompilerServices; -using ThreeFingerDragOnWindows.settings; - -namespace ThreeFingerDragOnWindows.touchpad; - -public class MouseSpeedSettings : INotifyPropertyChanged -{ - private bool _cursorMoveProperty; - private float _cursorSpeedProperty; - private float _cursorAccelerationProperty; - public TouchpadDeviceInfo TouchpadDevice { get; set; } - public string Header { get; } - - public MouseSpeedSettings(SettingsData.ThreeFingerDragConfig dragConfig, TouchpadDeviceInfo device) - { - TouchpadDevice = device; - Header = "Enable three finger mouse move (" + TouchpadDevice + ")"; - - CursorMoveProperty = dragConfig.ThreeFingerDragCursorMove; - CursorSpeedProperty = dragConfig.ThreeFingerDragCursorSpeed; - CursorAccelerationProperty = dragConfig.ThreeFingerDragCursorAcceleration; - } - - private void UpdateDragConfig() - { - App.SettingsData.ThreeFingerDeviceDragCursorConfigs[TouchpadDevice.deviceId] = new SettingsData.ThreeFingerDragConfig(_cursorMoveProperty, _cursorSpeedProperty, _cursorAccelerationProperty); - App.SettingsData.save(); - } - - public bool CursorMoveProperty - { - get => _cursorMoveProperty; - set - { - if (_cursorMoveProperty != value) - { - _cursorMoveProperty = value; - - OnPropertyChanged(nameof(CursorMoveProperty)); - } - - } - } - - public float CursorSpeedProperty - { - get => _cursorSpeedProperty; - set - { - if (_cursorSpeedProperty != value) - { - _cursorSpeedProperty = value; - OnPropertyChanged(nameof(CursorSpeedProperty)); - } - } - } - - public float CursorAccelerationProperty - { - get => _cursorAccelerationProperty; - set - { - if (_cursorAccelerationProperty != value) - { - _cursorAccelerationProperty = value; - OnPropertyChanged(nameof(CursorAccelerationProperty)); - } - - } - } - - public event PropertyChangedEventHandler? PropertyChanged; - - private void OnPropertyChanged(string propertyName){ - UpdateDragConfig(); - - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } -} \ No newline at end of file diff --git a/ThreeFingerDragOnWindows/touchpad/TouchpadDeviceInfo.cs b/ThreeFingerDragOnWindows/touchpad/TouchpadDeviceInfo.cs index 6e0eae8..25d01c4 100644 --- a/ThreeFingerDragOnWindows/touchpad/TouchpadDeviceInfo.cs +++ b/ThreeFingerDragOnWindows/touchpad/TouchpadDeviceInfo.cs @@ -6,6 +6,8 @@ public class TouchpadDeviceInfo { public String deviceId { get; set; } + public String deviceName { get; set; } // Raw device path for connection type detection + public String vendorId { get; set; } public String productId { get; set; } diff --git a/ThreeFingerDragOnWindows/touchpad/TouchpadHelper.cs b/ThreeFingerDragOnWindows/touchpad/TouchpadHelper.cs index 888b257..9fea2e4 100644 --- a/ThreeFingerDragOnWindows/touchpad/TouchpadHelper.cs +++ b/ThreeFingerDragOnWindows/touchpad/TouchpadHelper.cs @@ -17,6 +17,9 @@ internal static class TouchpadHelper { public const int RIM_INPUTSINK = 1; private static Dictionary availableDeviceInfos = new Dictionary(2); + private static Dictionary deviceLastSeenTime = new Dictionary(2); + private static Dictionary deviceHasSentInput = new Dictionary(2); + private static readonly TimeSpan DeviceRemovalGracePeriod = TimeSpan.FromSeconds(1); private static TouchpadDeviceInfo GetDeviceInfoFromHid(IntPtr hwnd) { @@ -35,7 +38,13 @@ private static TouchpadDeviceInfo GetDeviceInfoFromHid(IntPtr hwnd) { if (GetRawInputDeviceInfo(hwnd, RIDI_DEVICENAME, ptr, ref nameSize) != unchecked((uint)-1)) { - touchpadDevice.deviceId = ComputeMD5(Marshal.PtrToStringAnsi(ptr)); + var deviceName = Marshal.PtrToStringAnsi(ptr); + touchpadDevice.deviceId = ComputeMD5(deviceName); + + // Store the raw device name for connection type detection + // Device name format: \\?\HID#VID_xxxx&PID_yyyy&... + // Bluetooth devices typically have different patterns in their device paths + touchpadDevice.deviceName = deviceName; } } finally @@ -69,12 +78,22 @@ public static bool Exists(IntPtr hwnd) if (deviceInfo.hid.usUsagePage == 0x000D && deviceInfo.hid.usUsage == 0x0005) { - if (!availableDeviceInfos.ContainsKey(hwnd)) + bool isNewDevice = !availableDeviceInfos.ContainsKey(hwnd); + + if (isNewDevice) { - availableDeviceInfos[hwnd] = GetDeviceInfoFromHid(hwnd); - availableDeviceInfos[hwnd].vendorId = deviceInfo.hid.dwVendorId.ToString(); - availableDeviceInfos[hwnd].productId = deviceInfo.hid.dwProductId.ToString(); + var newDeviceInfo = GetDeviceInfoFromHid(hwnd); + newDeviceInfo.vendorId = deviceInfo.hid.dwVendorId.ToString(); + newDeviceInfo.productId = deviceInfo.hid.dwProductId.ToString(); + + availableDeviceInfos[hwnd] = newDeviceInfo; + deviceLastSeenTime[hwnd] = DateTime.Now; // Only set timestamp for NEW devices + deviceHasSentInput[hwnd] = false; // New devices haven't sent input yet + Logger.Log($"[TouchpadHelper] Added device: {hwnd}, deviceId: {newDeviceInfo.deviceId}, VID/PID: {newDeviceInfo.vendorId}/{newDeviceInfo.productId}"); } + // Note: We don't update timestamp for existing devices during enumeration + // Timestamps should only be updated when devices actually send input + return true; } return false; @@ -139,11 +158,194 @@ public static TouchpadDeviceInfo GetDeivceInfo(IntPtr currentDevice) return null; } + /// + /// Validates that a device handle is still valid by checking if it still exists in the system + /// + private static bool IsDeviceStillValid(IntPtr hDevice) + { + uint deviceInfoSize = 0; + // Try to get device info - if it fails, the device is no longer valid + return GetRawInputDeviceInfo(hDevice, RIDI_DEVICEINFO, IntPtr.Zero, ref deviceInfoSize) == 0; + } + + /// + /// Removes disconnected devices from the cache, but only after a grace period + /// to handle device reconnections during mode switches (e.g., Bluetooth to wired) + /// + public static void CleanupDisconnectedDevices() + { + var now = DateTime.Now; + var devicesToRemove = new List(); + + foreach (var kvp in availableDeviceInfos) + { + var device = kvp.Key; + + // Check if device is still valid + if (!IsDeviceStillValid(device)) + { + // If we haven't tracked this device's last seen time, set it now + if (!deviceLastSeenTime.ContainsKey(device)) + { + deviceLastSeenTime[device] = now; + } + // Only remove if grace period has elapsed + else if (now - deviceLastSeenTime[device] > DeviceRemovalGracePeriod) + { + devicesToRemove.Add(device); + } + } + else + { + // Device is valid, update last seen time + deviceLastSeenTime[device] = now; + } + } + + foreach (var device in devicesToRemove) + { + var deviceInfo = availableDeviceInfos[device]; + availableDeviceInfos.Remove(device); + deviceLastSeenTime.Remove(device); + deviceHasSentInput.Remove(device); + Logger.Log($"[TouchpadHelper] Removed disconnected device after grace period: {device}, deviceId: {deviceInfo.deviceId}"); + } + } + public static List GetAllDeivceInfos() { + // Clean up stale entries before returning + CleanupDisconnectedDevices(); return availableDeviceInfos.Values.ToList(); } + /// + /// Extracts a normalized hardware identifier from the device name to match Bluetooth and wired connections + /// of the same physical device. Returns the VID and PID extracted from the device path. + /// + private static string GetNormalizedHardwareId(string deviceName, string vendorId, string productId) + { + if (string.IsNullOrEmpty(deviceName)) + return $"{vendorId}_{productId}"; + + // Try to extract VID and PID from device name path + // Format examples: + // \\?\HID#VID_046D&PID_B036&MI_01&Col02#... (USB/wired) + // \\?\HID#VID_046D&PID_B036&... (Bluetooth) + // We want to extract the VID_xxxx&PID_yyyy part as the hardware identifier + + var vidIndex = deviceName.IndexOf("VID_", StringComparison.OrdinalIgnoreCase); + var pidIndex = deviceName.IndexOf("PID_", StringComparison.OrdinalIgnoreCase); + + if (vidIndex >= 0 && pidIndex >= 0) + { + // Extract VID (4 hex digits after VID_) + var vid = deviceName.Substring(vidIndex + 4, 4); + // Extract PID (4 hex digits after PID_) + var pid = deviceName.Substring(pidIndex + 4, 4); + return $"VID_{vid}_PID_{pid}"; + } + + // Fallback to vendorId + productId + return $"{vendorId}_{productId}"; + } + + /// + /// Re-enumerates all touchpad devices. Should be called when WM_INPUT_DEVICE_CHANGE is received + /// to handle device reconnections (e.g., switching between Bluetooth and wired mode) + /// + public static void RefreshDevices() + { + Logger.Log("[TouchpadHelper] Refreshing device list..."); + + uint deviceListCount = 0; + var rawInputDeviceListSize = (uint)Marshal.SizeOf(); + + if (GetRawInputDeviceList(null, ref deviceListCount, rawInputDeviceListSize) != 0) + { + Logger.Log("[TouchpadHelper] Failed to get device count"); + return; + } + + var devices = new RAWINPUTDEVICELIST[deviceListCount]; + + if (GetRawInputDeviceList(devices, ref deviceListCount, rawInputDeviceListSize) != deviceListCount) + { + Logger.Log("[TouchpadHelper] Failed to enumerate devices"); + return; + } + + // Track which handles are currently valid in the system and which are new + var currentHandles = new HashSet(); + var newlyAddedHandles = new HashSet(); + + // Check each HID device to see if it's a touchpad + foreach (var device in devices.Where(x => x.dwType == RIM_TYPEHID)) + { + bool wasNew = !availableDeviceInfos.ContainsKey(device.hDevice); + if (Exists(device.hDevice)) // This will add/update the device in our cache + { + currentHandles.Add(device.hDevice); + if (wasNew) + { + newlyAddedHandles.Add(device.hDevice); + } + } + } + + // Remove any devices from our cache that are not in the current enumeration + // This ensures we don't keep stale devices when hardware disconnects + var devicesToRemove = availableDeviceInfos.Keys.Where(handle => !currentHandles.Contains(handle)).ToList(); + + foreach (var handle in devicesToRemove) + { + var deviceInfo = availableDeviceInfos[handle]; + availableDeviceInfos.Remove(handle); + deviceLastSeenTime.Remove(handle); + deviceHasSentInput.Remove(handle); + Logger.Log($"[TouchpadHelper] Removed device not found in enumeration: {handle}, deviceId: {deviceInfo.deviceId}, VID/PID: {deviceInfo.vendorId}/{deviceInfo.productId}"); + } + + // Deduplicate devices with the same hardware ID (extracted from device name) + // This handles Bluetooth vs wired connections which may have different VID/PID reported by Windows + // but share the same hardware identifiers in their device path + var devicesByHardwareId = availableDeviceInfos + .GroupBy(kvp => GetNormalizedHardwareId(kvp.Value.deviceName, kvp.Value.vendorId, kvp.Value.productId)) + .Where(g => g.Count() > 1) + .ToList(); + + foreach (var group in devicesByHardwareId) + { + // Three-tier sorting priority: + // 1. Devices that have sent actual input (active devices) + // 2. Newly added devices (likely the new connection) + // 3. Most recent timestamp (fallback) + var sortedDevices = group.OrderByDescending(kvp => deviceHasSentInput.ContainsKey(kvp.Key) && deviceHasSentInput[kvp.Key] ? 1 : 0) + .ThenByDescending(kvp => newlyAddedHandles.Contains(kvp.Key) ? 1 : 0) + .ThenByDescending(kvp => deviceLastSeenTime.ContainsKey(kvp.Key) ? deviceLastSeenTime[kvp.Key] : DateTime.MinValue) + .ToList(); + + var keepDevice = sortedDevices.First(); + var hasSentInput = deviceHasSentInput.ContainsKey(keepDevice.Key) && deviceHasSentInput[keepDevice.Key]; + var wasNewDevice = newlyAddedHandles.Contains(keepDevice.Key); + + string reason = hasSentInput ? "has sent input" : (wasNewDevice ? "newly added" : "most recent"); + Logger.Log($"[TouchpadHelper] Found {group.Count()} devices with hardware ID: {group.Key}, keeping ({reason}): {keepDevice.Key}"); + + // Remove all except the first one + foreach (var device in sortedDevices.Skip(1)) + { + var deviceHadSentInput = deviceHasSentInput.ContainsKey(device.Key) && deviceHasSentInput[device.Key]; + Logger.Log($"[TouchpadHelper] Removing duplicate device: {device.Key}, deviceId: {device.Value.deviceId}, VID/PID: {device.Value.vendorId}/{device.Value.productId}, hadSentInput: {deviceHadSentInput}"); + availableDeviceInfos.Remove(device.Key); + deviceLastSeenTime.Remove(device.Key); + deviceHasSentInput.Remove(device.Key); + } + } + + Logger.Log($"[TouchpadHelper] Refresh complete. Active devices: {availableDeviceInfos.Count}"); + } + public static bool RegisterInput(IntPtr hwndTarget){ // 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 @@ -331,6 +533,13 @@ public static (IntPtr, List, uint) ParseInput(IntPtr lParam){ Logger.Log(toLog); + // Update the timestamp for this device since it just sent input + if (availableDeviceInfos.ContainsKey(currentDevice)) + { + deviceLastSeenTime[currentDevice] = DateTime.Now; + deviceHasSentInput[currentDevice] = true; // Mark that this device has sent actual input + } + return (currentDevice, contacts, contactCount); } finally{ Marshal.FreeHGlobal(rawHidRawDataPointer); From bdba86cb47fd8eb7484472451aca2c303233ff2c Mon Sep 17 00:00:00 2001 From: Cookiesmuch Date: Fri, 23 Jan 2026 14:44:15 +0800 Subject: [PATCH 4/4] Fix GitHub Copilot review issues: thread safety, null checks, code quality - Add thread synchronization locks around deviceLastSeenTime dictionary access - Add null checks for GetDeivceInfo() result to prevent NullReferenceException - Add null check for App.Instance.DispatcherQueue in SmartProfileSwitcher - Remove redundant File.Exists check (File.Delete handles non-existent files) - Replace inefficient ContainsKey+indexer pattern with TryGetValue - Mark fields as readonly: deviceLastSeenTime, deviceHasSentInput, _hookDelegate, _profiles - Improve comment placement for better code clarity Addresses all issues identified by GitHub Copilot automated code review. --- .../settings/ProfilesSettings.xaml.cs | 2 +- .../settings/SettingsData.cs | 18 ++--- .../settings/SmartProfileSwitcher.cs | 22 ++++-- .../touchpad/HandlerWindow.xaml.cs | 5 +- .../touchpad/TouchpadHelper.cs | 78 ++++++++++--------- 5 files changed, 71 insertions(+), 54 deletions(-) diff --git a/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml.cs b/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml.cs index 7399f88..726b127 100644 --- a/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml.cs +++ b/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml.cs @@ -76,7 +76,7 @@ protected void OnPropertyChanged([CallerMemberName] string propertyName = null) public sealed partial class ProfilesSettings : Page { - private ObservableCollection _profiles = new ObservableCollection(); + private readonly ObservableCollection _profiles = new ObservableCollection(); private ProfileViewModel _selectedProfile; private ProfileViewModel _rightClickedProfile; private ProfileViewModel _previousSelection; diff --git a/ThreeFingerDragOnWindows/settings/SettingsData.cs b/ThreeFingerDragOnWindows/settings/SettingsData.cs index baab2dd..7320ea1 100644 --- a/ThreeFingerDragOnWindows/settings/SettingsData.cs +++ b/ThreeFingerDragOnWindows/settings/SettingsData.cs @@ -141,17 +141,15 @@ public void RenameActiveProfile(string newName) // Save to new location _activeProfile.Save(newPath); - if (File.Exists(oldPath)) + // Delete old file - File.Delete doesn't throw if file doesn't exist + try { - try - { - File.Delete(oldPath); - Logger.Log($"Renamed profile file from {oldPath} to {newPath}"); - } - catch (Exception e) - { - Logger.Log($"Error deleting old profile file: {e.Message}"); - } + File.Delete(oldPath); + Logger.Log($"Renamed profile file from {oldPath} to {newPath}"); + } + catch (Exception e) + { + Logger.Log($"Error deleting old profile file: {e.Message}"); } ActiveProfilePath = newPath; diff --git a/ThreeFingerDragOnWindows/settings/SmartProfileSwitcher.cs b/ThreeFingerDragOnWindows/settings/SmartProfileSwitcher.cs index dfb1550..e03eff8 100644 --- a/ThreeFingerDragOnWindows/settings/SmartProfileSwitcher.cs +++ b/ThreeFingerDragOnWindows/settings/SmartProfileSwitcher.cs @@ -32,8 +32,8 @@ private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPt private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime); -private readonly Timer _checkTimer; -private WinEventDelegate _hookDelegate; + private readonly Timer _checkTimer; + private readonly WinEventDelegate _hookDelegate; private IntPtr _hookHandle; private string _lastProcessPath = ""; private bool _isEnabled = false; @@ -264,11 +264,19 @@ private void CheckAndSwitchProfile(string processPath){ App.SettingsData.SwitchToProfile(matchedProfileFile); // Refresh settings window if open - App.Instance.DispatcherQueue.TryEnqueue(() => { - if(App.SettingsWindow is SettingsWindow settingsWindow){ - settingsWindow.LoadProfiles(); - } - }); + var dispatcherQueue = App.Instance?.DispatcherQueue; + if (dispatcherQueue != null) + { + dispatcherQueue.TryEnqueue(() => { + if(App.SettingsWindow is SettingsWindow settingsWindow){ + settingsWindow.LoadProfiles(); + } + }); + } + else + { + Logger.Log("[SmartSwitcher] DispatcherQueue is not available; skipping settings window refresh."); + } } else { Logger.Log($"[SmartSwitcher] Summary: {profilesWithSmart} profiles with smart switching for device, {totalPrograms} total programs"); Logger.Log($"[SmartSwitcher] No matching profile found for current process on device {_currentDeviceId}"); diff --git a/ThreeFingerDragOnWindows/touchpad/HandlerWindow.xaml.cs b/ThreeFingerDragOnWindows/touchpad/HandlerWindow.xaml.cs index b976578..2608ded 100644 --- a/ThreeFingerDragOnWindows/touchpad/HandlerWindow.xaml.cs +++ b/ThreeFingerDragOnWindows/touchpad/HandlerWindow.xaml.cs @@ -89,7 +89,10 @@ public void OnTouchpadContact(IntPtr currentDevice, List contac // Update smart profile switcher with current device var deviceInfo = TouchpadHelper.GetDeivceInfo(currentDevice); - App.SmartProfileSwitcher?.SetCurrentDevice(deviceInfo.deviceId); + if (deviceInfo != null) + { + App.SmartProfileSwitcher?.SetCurrentDevice(deviceInfo.deviceId); + } _app.OnTouchpadContact(currentDevice, contacts.ToArray()); // Transfer to App for displaying contacts in SettingsWindow _lastContactCtms = Ctms(); diff --git a/ThreeFingerDragOnWindows/touchpad/TouchpadHelper.cs b/ThreeFingerDragOnWindows/touchpad/TouchpadHelper.cs index 9fea2e4..375559f 100644 --- a/ThreeFingerDragOnWindows/touchpad/TouchpadHelper.cs +++ b/ThreeFingerDragOnWindows/touchpad/TouchpadHelper.cs @@ -17,8 +17,8 @@ internal static class TouchpadHelper { public const int RIM_INPUTSINK = 1; private static Dictionary availableDeviceInfos = new Dictionary(2); - private static Dictionary deviceLastSeenTime = new Dictionary(2); - private static Dictionary deviceHasSentInput = new Dictionary(2); + private static readonly Dictionary deviceLastSeenTime = new Dictionary(2); + private static readonly Dictionary deviceHasSentInput = new Dictionary(2); private static readonly TimeSpan DeviceRemovalGracePeriod = TimeSpan.FromSeconds(1); private static TouchpadDeviceInfo GetDeviceInfoFromHid(IntPtr hwnd) @@ -86,8 +86,10 @@ public static bool Exists(IntPtr hwnd) newDeviceInfo.vendorId = deviceInfo.hid.dwVendorId.ToString(); newDeviceInfo.productId = deviceInfo.hid.dwProductId.ToString(); + availableDeviceInfos[hwnd] = newDeviceInfo; - deviceLastSeenTime[hwnd] = DateTime.Now; // Only set timestamp for NEW devices + // Only set timestamp for NEW devices + deviceLastSeenTime[hwnd] = DateTime.Now; deviceHasSentInput[hwnd] = false; // New devices haven't sent input yet Logger.Log($"[TouchpadHelper] Added device: {hwnd}, deviceId: {newDeviceInfo.deviceId}, VID/PID: {newDeviceInfo.vendorId}/{newDeviceInfo.productId}"); } @@ -174,42 +176,45 @@ private static bool IsDeviceStillValid(IntPtr hDevice) /// public static void CleanupDisconnectedDevices() { - var now = DateTime.Now; - var devicesToRemove = new List(); - - foreach (var kvp in availableDeviceInfos) + lock (deviceLastSeenTime) { - var device = kvp.Key; - - // Check if device is still valid - if (!IsDeviceStillValid(device)) + var now = DateTime.Now; + var devicesToRemove = new List(); + + foreach (var kvp in availableDeviceInfos) { - // If we haven't tracked this device's last seen time, set it now - if (!deviceLastSeenTime.ContainsKey(device)) + var device = kvp.Key; + + // Check if device is still valid + if (!IsDeviceStillValid(device)) { - deviceLastSeenTime[device] = now; + // If we haven't tracked this device's last seen time, set it now + if (!deviceLastSeenTime.ContainsKey(device)) + { + deviceLastSeenTime[device] = now; + } + // Only remove if grace period has elapsed + else if (now - deviceLastSeenTime[device] > DeviceRemovalGracePeriod) + { + devicesToRemove.Add(device); + } } - // Only remove if grace period has elapsed - else if (now - deviceLastSeenTime[device] > DeviceRemovalGracePeriod) + else { - devicesToRemove.Add(device); + // Device is valid, update last seen time + deviceLastSeenTime[device] = now; } } - else + + foreach (var device in devicesToRemove) { - // Device is valid, update last seen time - deviceLastSeenTime[device] = now; + var deviceInfo = availableDeviceInfos[device]; + availableDeviceInfos.Remove(device); + deviceLastSeenTime.Remove(device); + deviceHasSentInput.Remove(device); + Logger.Log($"[TouchpadHelper] Removed disconnected device after grace period: {device}, deviceId: {deviceInfo.deviceId}"); } } - - foreach (var device in devicesToRemove) - { - var deviceInfo = availableDeviceInfos[device]; - availableDeviceInfos.Remove(device); - deviceLastSeenTime.Remove(device); - deviceHasSentInput.Remove(device); - Logger.Log($"[TouchpadHelper] Removed disconnected device after grace period: {device}, deviceId: {deviceInfo.deviceId}"); - } } public static List GetAllDeivceInfos() @@ -320,13 +325,13 @@ public static void RefreshDevices() // 1. Devices that have sent actual input (active devices) // 2. Newly added devices (likely the new connection) // 3. Most recent timestamp (fallback) - var sortedDevices = group.OrderByDescending(kvp => deviceHasSentInput.ContainsKey(kvp.Key) && deviceHasSentInput[kvp.Key] ? 1 : 0) + var sortedDevices = group.OrderByDescending(kvp => deviceHasSentInput.TryGetValue(kvp.Key, out var hasSent) && hasSent ? 1 : 0) .ThenByDescending(kvp => newlyAddedHandles.Contains(kvp.Key) ? 1 : 0) - .ThenByDescending(kvp => deviceLastSeenTime.ContainsKey(kvp.Key) ? deviceLastSeenTime[kvp.Key] : DateTime.MinValue) + .ThenByDescending(kvp => deviceLastSeenTime.TryGetValue(kvp.Key, out var lastSeen) ? lastSeen : DateTime.MinValue) .ToList(); var keepDevice = sortedDevices.First(); - var hasSentInput = deviceHasSentInput.ContainsKey(keepDevice.Key) && deviceHasSentInput[keepDevice.Key]; + var hasSentInput = deviceHasSentInput.TryGetValue(keepDevice.Key, out var keepHasSent) && keepHasSent; var wasNewDevice = newlyAddedHandles.Contains(keepDevice.Key); string reason = hasSentInput ? "has sent input" : (wasNewDevice ? "newly added" : "most recent"); @@ -335,7 +340,7 @@ public static void RefreshDevices() // Remove all except the first one foreach (var device in sortedDevices.Skip(1)) { - var deviceHadSentInput = deviceHasSentInput.ContainsKey(device.Key) && deviceHasSentInput[device.Key]; + var deviceHadSentInput = deviceHasSentInput.TryGetValue(device.Key, out var hadSent) && hadSent; Logger.Log($"[TouchpadHelper] Removing duplicate device: {device.Key}, deviceId: {device.Value.deviceId}, VID/PID: {device.Value.vendorId}/{device.Value.productId}, hadSentInput: {deviceHadSentInput}"); availableDeviceInfos.Remove(device.Key); deviceLastSeenTime.Remove(device.Key); @@ -536,8 +541,11 @@ public static (IntPtr, List, uint) ParseInput(IntPtr lParam){ // Update the timestamp for this device since it just sent input if (availableDeviceInfos.ContainsKey(currentDevice)) { - deviceLastSeenTime[currentDevice] = DateTime.Now; - deviceHasSentInput[currentDevice] = true; // Mark that this device has sent actual input + lock (deviceLastSeenTime) + { + deviceLastSeenTime[currentDevice] = DateTime.Now; + deviceHasSentInput[currentDevice] = true; // Mark that this device has sent actual input + } } return (currentDevice, contacts, contactCount);