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/App.xaml.cs b/ThreeFingerDragOnWindows/App.xaml.cs index 4a7ed8c..bf67d8e 100644 --- a/ThreeFingerDragOnWindows/App.xaml.cs +++ b/ThreeFingerDragOnWindows/App.xaml.cs @@ -11,13 +11,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; @@ -65,6 +66,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/settings/ProfilesSettings.xaml b/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml new file mode 100644 index 0000000..588a89b --- /dev/null +++ b/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + @@ -73,6 +132,11 @@ + + + + + diff --git a/ThreeFingerDragOnWindows/settings/SettingsWindow.xaml.cs b/ThreeFingerDragOnWindows/settings/SettingsWindow.xaml.cs index 4ce2a23..e14fc87 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,250 @@ 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)); + } + } + + /// + /// 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 ////////// 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..e03eff8 --- /dev/null +++ b/ThreeFingerDragOnWindows/settings/SmartProfileSwitcher.cs @@ -0,0 +1,285 @@ +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.touchpad; +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 readonly WinEventDelegate _hookDelegate; +private IntPtr _hookHandle; +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(); + _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){ + 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!"); + return; + } + + var profileFiles = Directory.GetFiles(profilesDir, "*.xml"); + var currentProfilePath = App.SettingsData.ActiveProfilePath; + + Logger.Log($"[SmartSwitcher] Checking {profileFiles.Length} profiles for device: {_currentDeviceId}..."); + 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 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] Device smart switching: {(deviceConfig.SmartSwitchingEnabled ? "YES" : "NO")}"); + + if(deviceConfig.SmartSwitchingEnabled){ + profilesWithSmart++; + Logger.Log($"[SmartSwitcher] Associated programs for device ({deviceConfig.AssociatedPrograms.Count}):"); + + foreach(var prog in deviceConfig.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}' for device!"); + } + } + } + } 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}' for device {_currentDeviceId}!"); + App.SettingsData.SwitchToProfile(matchedProfileFile); + + // Refresh settings window if open + 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/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 - { - ObservableCollection settings = new ObservableCollection(new Collection()); - List allDeviceInfos = TouchpadHelper.GetAllDeivceInfos(); - foreach (TouchpadDeviceInfo device in allDeviceInfos) - { - var mouseSetting = new MouseSpeedSettings( - App.SettingsData.ThreeFingerDeviceDragCursorConfigs.GetValueOrDefault(device.deviceId, - new SettingsData.ThreeFingerDragConfig()), device); - settings.Add(mouseSetting); - } - settings.CollectionChanged += OnCollectionChanged; - return settings; + public bool CursorMoveProperty { + get{ return App.SettingsData.ActiveProfile.ThreeFingerDragCursorMove; } + set{ + App.SettingsData.ActiveProfile.ThreeFingerDragCursorMove = value; + App.SettingsData.SaveActiveProfile(); } } - - private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(MouseSpeedSettingItems)); + + + public float CursorSpeedProperty { + get{ return App.SettingsData.ActiveProfile.ThreeFingerDragCursorSpeed; } + set{ + if(App.SettingsData.ActiveProfile.ThreeFingerDragCursorSpeed != value){ + App.SettingsData.ActiveProfile.ThreeFingerDragCursorSpeed = value; + App.SettingsData.SaveActiveProfile(); + OnPropertyChanged(nameof(CursorSpeedProperty)); + } + } + } + + public float CursorAccelerationProperty { + get{ return App.SettingsData.ActiveProfile.ThreeFingerDragCursorAcceleration; } + set{ + 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/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 new file mode 100644 index 0000000..35d0a05 --- /dev/null +++ b/ThreeFingerDragOnWindows/settings/profiles/ThreeFingerDragProfile.cs @@ -0,0 +1,133 @@ +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 - 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; + +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 2519471..366f13e 100644 --- a/ThreeFingerDragOnWindows/threefingerdrag/DistanceManager.cs +++ b/ThreeFingerDragOnWindows/threefingerdrag/DistanceManager.cs @@ -84,9 +84,8 @@ public class DistanceManager { public static Point ApplySpeedAndAcc(IntPtr currentDevice, Point delta, int elapsed){ - // Apply Speed - var deviceInfo = TouchpadHelper.GetDeivceInfo(currentDevice); - delta.Multiply(App.SettingsData.ThreeFingerDeviceDragCursorConfigs.GetValueOrDefault(deviceInfo.deviceId, new SettingsData.ThreeFingerDragConfig()).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); @@ -94,15 +93,13 @@ public static Point ApplySpeedAndAcc(IntPtr currentDevice, Point delta, int elap float pointerVelocity = 1; - if(App.SettingsData.ThreeFingerDeviceDragCursorConfigs.ContainsKey(deviceInfo.deviceId)){ + 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.ThreeFingerDeviceDragCursorConfigs.GetValueOrDefault(deviceInfo.deviceId, new SettingsData.ThreeFingerDragConfig()).ThreeFingerDragCursorAcceleration / 10f; // Acceleration is multiplied by 10 in settings. - if (a != 0) { - 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 + ")"); - } + 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 + ")"); } // Apply acceleration @@ -112,8 +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); - distance *= App.SettingsData.ThreeFingerDeviceDragCursorConfigs.GetValueOrDefault(deviceInfo.deviceId, new SettingsData.ThreeFingerDragConfig()).ThreeFingerDragCursorSpeed / 60; + distance *= App.SettingsData.ActiveProfile.ThreeFingerDragCursorSpeed / 60; return distance; } diff --git a/ThreeFingerDragOnWindows/threefingerdrag/FingerCounter.cs b/ThreeFingerDragOnWindows/threefingerdrag/FingerCounter.cs index d474bd4..ddc047e 100644 --- a/ThreeFingerDragOnWindows/threefingerdrag/FingerCounter.cs +++ b/ThreeFingerDragOnWindows/threefingerdrag/FingerCounter.cs @@ -24,9 +24,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. @@ -49,12 +49,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 73a0ff3..a531896 100644 --- a/ThreeFingerDragOnWindows/threefingerdrag/ThreeFingerDrag.cs +++ b/ThreeFingerDragOnWindows/threefingerdrag/ThreeFingerDrag.cs @@ -26,7 +26,6 @@ public ThreeFingerDrag(){ private int _averagingCount = 0; public void OnTouchpadContact(IntPtr currentDevice, TouchpadContact[] oldContacts, TouchpadContact[] contacts, long elapsed){ - var deviceInfo = TouchpadHelper.GetDeivceInfo(currentDevice); 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); @@ -54,19 +53,18 @@ public void OnTouchpadContact(IntPtr currentDevice, TouchpadContact[] oldContact Logger.Log(" STOP DRAG, click up"); StopDrag(); } else if(fingersCount >= 2 && originalFingersCount == 3 && areContactsIdsCommons && _isDragging){ - // Dragging - if(App.SettingsData.ThreeFingerDeviceDragCursorConfigs.ContainsKey(deviceInfo.deviceId) - && App.SettingsData.ThreeFingerDeviceDragCursorConfigs.GetValueOrDefault(deviceInfo.deviceId, new SettingsData.ThreeFingerDragConfig()).ThreeFingerDragCursorMove){ - if(App.SettingsData.ThreeFingerDragMaxFingerMoveDistance != 0 && longestDist2D > App.SettingsData.ThreeFingerDragMaxFingerMoveDistance){ + // 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()){ Point delta = DistanceManager.ApplySpeedAndAcc(currentDevice, 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; @@ -102,8 +100,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/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 9763320..2608ded 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 @@ -66,10 +83,17 @@ public void OnTouchpadInitialized(bool touchpadExists, bool inputReceiverInstall private long _lastContactCtms = Ctms(); public void OnTouchpadContact(IntPtr currentDevice, List contacts){ - if(App.SettingsData.ThreeFingerDrag){ + if(App.SettingsData.ActiveProfile.ThreeFingerDrag){ _threeFingersDrag.OnTouchpadContact(currentDevice, _oldContacts, contacts.ToArray(), Ctms() - _lastContactCtms); } + // Update smart profile switcher with current device + var deviceInfo = TouchpadHelper.GetDeivceInfo(currentDevice); + if (deviceInfo != null) + { + 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 5eb0d15..0000000 --- a/ThreeFingerDragOnWindows/touchpad/MouseSpeedSettings.cs +++ /dev/null @@ -1,81 +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); - } - - 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..375559f 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 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) { @@ -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,24 @@ 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; + // 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}"); } + // 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 +160,197 @@ 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() + { + lock (deviceLastSeenTime) + { + 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.TryGetValue(kvp.Key, out var hasSent) && hasSent ? 1 : 0) + .ThenByDescending(kvp => newlyAddedHandles.Contains(kvp.Key) ? 1 : 0) + .ThenByDescending(kvp => deviceLastSeenTime.TryGetValue(kvp.Key, out var lastSeen) ? lastSeen : DateTime.MinValue) + .ToList(); + + var keepDevice = sortedDevices.First(); + 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"); + 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.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); + 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 +538,16 @@ 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)) + { + lock (deviceLastSeenTime) + { + deviceLastSeenTime[currentDevice] = DateTime.Now; + deviceHasSentInput[currentDevice] = true; // Mark that this device has sent actual input + } + } + return (currentDevice, contacts, contactCount); } finally{ Marshal.FreeHGlobal(rawHidRawDataPointer); 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; }