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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml.cs b/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml.cs
new file mode 100644
index 0000000..726b127
--- /dev/null
+++ b/ThreeFingerDragOnWindows/settings/ProfilesSettings.xaml.cs
@@ -0,0 +1,741 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.UI.Xaml;
+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;
+using WinUICommunity;
+using WinRT.Interop;
+
+namespace ThreeFingerDragOnWindows.settings;
+
+public class ProfileViewModel : INotifyPropertyChanged
+{
+ private string _name;
+ private Visibility _isActive = Visibility.Collapsed;
+ private Visibility _hasSmartSwitching = Visibility.Collapsed;
+
+ public string FilePath { get; set; }
+
+ public string Name
+ {
+ get => _name;
+ set
+ {
+ if (_name != value)
+ {
+ _name = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public Visibility IsActive
+ {
+ get => _isActive;
+ set
+ {
+ if (_isActive != value)
+ {
+ _isActive = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public Visibility HasSmartSwitching
+ {
+ get => _hasSmartSwitching;
+ set
+ {
+ if (_hasSmartSwitching != value)
+ {
+ _hasSmartSwitching = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
+
+public sealed partial class ProfilesSettings : Page
+{
+ private readonly ObservableCollection _profiles = new ObservableCollection();
+ 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()
+ {
+ _profiles.Clear();
+
+ var profilesDir = Path.Combine(ApplicationData.Current.LocalFolder.Path, "profiles");
+ if (!Directory.Exists(profilesDir))
+ {
+ return;
+ }
+
+ var activeProfilePath = App.SettingsData.ActiveProfilePath;
+ var profileFiles = Directory.GetFiles(profilesDir, "*.xml");
+
+ foreach (var file in profileFiles)
+ {
+ 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 = hasAnySmartSwitching ? Visibility.Visible : Visibility.Collapsed
+ });
+ }
+ catch (Exception ex)
+ {
+ Logger.Log($"Error loading profile {file}: {ex.Message}");
+ }
+ }
+
+ ProfilesListView.ItemsSource = _profiles;
+
+ // Select active profile
+ var activeProfile = _profiles.FirstOrDefault(p => p.IsActive == Visibility.Visible);
+ if (activeProfile != null)
+ {
+ ProfilesListView.SelectedItem = activeProfile;
+ }
+
+ UpdateButtonStates();
+ }
+
+ private void ProfilesListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ _selectedProfile = ProfilesListView.SelectedItem as ProfileViewModel;
+
+ if (_selectedProfile != null)
+ {
+ SmartSwitchingSection.Visibility = Visibility.Visible;
+ LoadProfileSettings(_selectedProfile);
+ }
+ else
+ {
+ SmartSwitchingSection.Visibility = Visibility.Collapsed;
+ }
+
+ UpdateButtonStates();
+ }
+
+ private void UpdateButtonStates()
+ {
+ var hasSelection = _selectedProfile != null;
+ DuplicateButton.IsEnabled = hasSelection;
+ DeleteButton.IsEnabled = hasSelection;
+ }
+
+ private void LoadProfileSettings(ProfileViewModel profileVm)
+ {
+ try
+ {
+ var profile = ThreeFingerDragProfile.Load(profileVm.FilePath);
+
+ ProfileNameTextBox.Text = profile.ProfileName;
+
+ // 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 = deviceConfig.SmartSwitchingEnabled;
+
+ 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)
+ {
+ if (_selectedProfile != null && !string.IsNullOrWhiteSpace(ProfileNameTextBox.Text))
+ {
+ var newName = ProfileNameTextBox.Text.Trim();
+ if (newName != _selectedProfile.Name)
+ {
+ RenameProfile(_selectedProfile, newName);
+ }
+ }
+ }
+
+ private async void RenameProfile(ProfileViewModel profileVm, string newName)
+ {
+ try
+ {
+ var profile = ThreeFingerDragProfile.Load(profileVm.FilePath);
+ var oldPath = profileVm.FilePath;
+ var newPath = SettingsData.GetProfileFilePath(newName);
+
+ if (File.Exists(newPath) && newPath != oldPath)
+ {
+ await ShowErrorDialog($"Profile with name '{newName}' already exists");
+ profileVm.Name = profile.ProfileName; // Revert
+ return;
+ }
+
+ profile.ProfileName = newName;
+ profile.Save(newPath);
+
+ if (oldPath != newPath && File.Exists(oldPath))
+ {
+ File.Delete(oldPath);
+ }
+
+ profileVm.Name = newName;
+ profileVm.FilePath = newPath;
+
+ // Update active profile path if this was the active profile
+ if (profileVm.IsActive == Visibility.Visible)
+ {
+ App.SettingsData.ActiveProfilePath = newPath;
+ App.SettingsData.save();
+ }
+
+ // Update profiles list in settings data
+ var profileInfo = App.SettingsData.Profiles.FirstOrDefault(p => p.FilePath == oldPath);
+ if (profileInfo != null)
+ {
+ profileInfo.Name = newName;
+ profileInfo.FilePath = newPath;
+ App.SettingsData.save();
+ }
+
+ // Refresh SettingsWindow profile selector
+ if (App.SettingsWindow is SettingsWindow settingsWindow)
+ {
+ settingsWindow.LoadProfiles();
+ }
+
+ Logger.Log($"Profile renamed to {newName}");
+ }
+ catch (Exception ex)
+ {
+ Logger.Log($"Error renaming profile: {ex.Message}");
+ await ShowErrorDialog($"Error renaming profile: {ex.Message}");
+ }
+ }
+
+ private void ProfilesListView_RightTapped(object sender, RightTappedRoutedEventArgs e)
+ {
+ var item = (e.OriginalSource as FrameworkElement)?.DataContext as ProfileViewModel;
+ if (item != null)
+ {
+ // Store current selection and prevent selection change
+ _previousSelection = _selectedProfile;
+ _rightClickedProfile = item;
+ // Don't change selection - just store the right-clicked item for context menu
+ }
+ }
+
+ private async void NewProfile_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"
+ };
+
+ // Auto-select all text when the dialog opens
+ textBox.Loaded += (s, e) => {
+ textBox.SelectAll();
+ textBox.Focus(FocusState.Programmatic);
+ };
+
+ 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;
+ }
+
+ var newProfile = new ThreeFingerDragProfile
+ {
+ ProfileName = newProfileName
+ };
+ newProfile.Save(newProfilePath);
+
+ App.SettingsData.Profiles.Add(new SettingsData.ProfileInfo
+ {
+ Name = newProfileName,
+ FilePath = newProfilePath
+ });
+ App.SettingsData.save();
+
+ LoadProfiles();
+ ProfilesListView.SelectedItem = _profiles.FirstOrDefault(p => p.Name == newProfileName);
+
+ // Refresh SettingsWindow profile selector
+ if (App.SettingsWindow is SettingsWindow settingsWindow)
+ {
+ settingsWindow.LoadProfiles();
+ }
+
+ Logger.Log($"Created new profile: {newProfileName}");
+ }
+ }
+
+ private void ActivateProfile_Click(object sender, RoutedEventArgs e)
+ {
+ var targetProfile = _rightClickedProfile ?? _selectedProfile;
+ if (targetProfile != null)
+ {
+ App.SettingsData.SwitchToProfile(targetProfile.FilePath);
+
+ // Update UI
+ foreach (var profile in _profiles)
+ {
+ profile.IsActive = profile == targetProfile ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ // Refresh SettingsWindow profile selector to show the activated profile
+ if (App.SettingsWindow is SettingsWindow settingsWindow)
+ {
+ settingsWindow.LoadProfiles();
+ }
+
+ Logger.Log($"Activated profile: {targetProfile.Name}");
+ }
+ _rightClickedProfile = null;
+ }
+
+ private async void DuplicateProfile_Click(object sender, RoutedEventArgs e)
+ {
+ if (_selectedProfile == null) return;
+
+ var currentProfile = ThreeFingerDragProfile.Load(_selectedProfile.FilePath);
+
+ 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 = currentProfile.ProfileName + " Copy"
+ };
+
+ // Auto-select all text when the dialog opens
+ textBox.Loaded += (s, e) => {
+ textBox.SelectAll();
+ textBox.Focus(FocusState.Programmatic);
+ };
+
+ 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;
+ }
+
+ var duplicatedProfile = new ThreeFingerDragProfile
+ {
+ ProfileName = newProfileName,
+ // 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,
+ 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);
+
+ App.SettingsData.Profiles.Add(new SettingsData.ProfileInfo
+ {
+ Name = newProfileName,
+ FilePath = newProfilePath
+ });
+ App.SettingsData.save();
+
+ LoadProfiles();
+ ProfilesListView.SelectedItem = _profiles.FirstOrDefault(p => p.Name == newProfileName);
+
+ Logger.Log($"Duplicated profile to {newProfileName}");
+ }
+ }
+
+ private async void DeleteProfile_Click(object sender, RoutedEventArgs e)
+ {
+ var targetProfile = _rightClickedProfile ?? _selectedProfile;
+ if (targetProfile == null) return;
+
+ if (targetProfile.IsActive == Visibility.Visible)
+ {
+ await ShowErrorDialog("Cannot delete the active profile. Please activate another profile first.");
+ _rightClickedProfile = null;
+ return;
+ }
+
+ var dialog = new ContentDialog
+ {
+ XamlRoot = Content.XamlRoot,
+ Title = "Delete Profile",
+ Content = $"Are you sure you want to delete the profile '{targetProfile.Name}'?",
+ PrimaryButtonText = "Delete",
+ CloseButtonText = "Cancel",
+ DefaultButton = ContentDialogButton.Close
+ };
+
+ var result = await dialog.ShowAsyncDraggable();
+
+ if (result == ContentDialogResult.Primary)
+ {
+ try
+ {
+ File.Delete(targetProfile.FilePath);
+
+ var profileInfo = App.SettingsData.Profiles.FirstOrDefault(p => p.FilePath == targetProfile.FilePath);
+ if (profileInfo != null)
+ {
+ App.SettingsData.Profiles.Remove(profileInfo);
+ App.SettingsData.save();
+ }
+
+ LoadProfiles();
+
+ // Refresh SettingsWindow profile selector
+ if (App.SettingsWindow is SettingsWindow settingsWindow)
+ {
+ settingsWindow.LoadProfiles();
+ }
+
+ Logger.Log($"Deleted profile: {targetProfile.Name}");
+ }
+ catch (Exception ex)
+ {
+ await ShowErrorDialog($"Error deleting profile: {ex.Message}");
+ }
+ }
+ _rightClickedProfile = null;
+ }
+
+ private void SmartSwitchingToggle_Toggled(object sender, RoutedEventArgs e)
+ {
+ if (_selectedProfile == null) return;
+
+ try
+ {
+ var profile = ThreeFingerDragProfile.Load(_selectedProfile.FilePath);
+ var currentDevice = GetCurrentDeviceId();
+ var deviceConfig = profile.GetDeviceSmartSwitchingConfig(currentDevice);
+
+ deviceConfig.SmartSwitchingEnabled = SmartSwitchingToggle.IsOn;
+ profile.Save(_selectedProfile.FilePath);
+
+ // 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} on device {currentDevice}");
+ }
+ catch (Exception ex)
+ {
+ Logger.Log($"Error toggling smart switching: {ex.Message}");
+ }
+ }
+
+ private async void AddProgram_Click(object sender, RoutedEventArgs e)
+ {
+ var picker = new FileOpenPicker();
+ picker.FileTypeFilter.Add(".exe");
+
+ var hwnd = WindowNative.GetWindowHandle(App.SettingsWindow);
+ InitializeWithWindow.Initialize(picker, hwnd);
+
+ var file = await picker.PickSingleFileAsync();
+ if (file != null)
+ {
+ AddProgramToProfile(file.Path);
+ }
+ }
+
+ private async void AddFromRunning_Click(object sender, RoutedEventArgs e)
+ {
+ var runningProcesses = Process.GetProcesses()
+ .Where(p => !string.IsNullOrEmpty(p.MainWindowTitle))
+ .Select(p => {
+ try
+ {
+ return new { p.ProcessName, Path = p.MainModule?.FileName };
+ }
+ catch
+ {
+ return null;
+ }
+ })
+ .Where(p => p != null && !string.IsNullOrEmpty(p.Path))
+ .DistinctBy(p => p.Path)
+ .OrderBy(p => p.ProcessName)
+ .ToList();
+
+ var dialog = new ContentDialog
+ {
+ XamlRoot = Content.XamlRoot,
+ Title = "Select Running Program",
+ PrimaryButtonText = "Add",
+ CloseButtonText = "Cancel",
+ DefaultButton = ContentDialogButton.Primary
+ };
+
+ var listView = new ListView
+ {
+ ItemsSource = runningProcesses,
+ SelectionMode = ListViewSelectionMode.Single,
+ DisplayMemberPath = "ProcessName",
+ Height = 400
+ };
+
+ dialog.Content = listView;
+
+ var result = await dialog.ShowAsyncDraggable();
+
+ if (result == ContentDialogResult.Primary && listView.SelectedItem != null)
+ {
+ dynamic selected = listView.SelectedItem;
+ AddProgramToProfile(selected.Path);
+ }
+ }
+
+ private void AddProgramToProfile(string exePath)
+ {
+ if (_selectedProfile == null || string.IsNullOrEmpty(exePath)) return;
+
+ try
+ {
+ var profile = ThreeFingerDragProfile.Load(_selectedProfile.FilePath);
+ var currentDevice = GetCurrentDeviceId();
+ var deviceConfig = profile.GetDeviceSmartSwitchingConfig(currentDevice);
+
+ if (!deviceConfig.AssociatedPrograms.Contains(exePath))
+ {
+ deviceConfig.AssociatedPrograms.Add(exePath);
+ profile.Save(_selectedProfile.FilePath);
+
+ // Refresh UI
+ LoadProfileSettings(_selectedProfile);
+
+ Logger.Log($"Added program {exePath} to profile {_selectedProfile.Name} for device {currentDevice}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Log($"Error adding program: {ex.Message}");
+ }
+ }
+
+ private void RemoveProgram_Click(object sender, RoutedEventArgs e)
+ {
+ var button = sender as Button;
+ var exePath = button?.Tag as string;
+
+ if (_selectedProfile == null || string.IsNullOrEmpty(exePath)) return;
+
+ try
+ {
+ var profile = ThreeFingerDragProfile.Load(_selectedProfile.FilePath);
+ 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} for device {currentDevice}");
+ }
+ catch (Exception ex)
+ {
+ Logger.Log($"Error removing program: {ex.Message}");
+ }
+ }
+
+ 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();
+ }
+
+ // Detection Mode Settings
+ private void LoadDetectionSettings()
+ {
+ var mode = App.SettingsData.DetectionMode;
+ DetectionModeComboBox.SelectedIndex = mode == SettingsData.ProfileSwitchingDetectionMode.WindowsHook ? 0 : 1;
+ IntervalNumberBox.Value = App.SettingsData.DetectionInterval;
+ IntervalSettingsPanel.Visibility = mode == SettingsData.ProfileSwitchingDetectionMode.IntervalBased
+ ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ private void DetectionMode_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (DetectionModeComboBox.SelectedItem is ComboBoxItem item)
+ {
+ var mode = item.Tag.ToString() == "WindowsHook"
+ ? SettingsData.ProfileSwitchingDetectionMode.WindowsHook
+ : SettingsData.ProfileSwitchingDetectionMode.IntervalBased;
+
+ App.SettingsData.DetectionMode = mode;
+ App.SettingsData.save();
+
+ IntervalSettingsPanel.Visibility = mode == SettingsData.ProfileSwitchingDetectionMode.IntervalBased
+ ? Visibility.Visible : Visibility.Collapsed;
+
+ // Restart the switcher with new settings
+ App.SmartProfileSwitcher?.Restart();
+
+ Logger.Log($"Detection mode changed to: {mode}");
+ }
+ }
+
+ private void IntervalNumberBox_ValueChanged(NumberBox sender, NumberBoxValueChangedEventArgs args)
+ {
+ if (double.IsNaN(args.NewValue)) return;
+
+ var interval = (int)args.NewValue;
+ App.SettingsData.DetectionInterval = interval;
+ App.SettingsData.save();
+
+ // Restart the switcher with new interval
+ if (App.SettingsData.DetectionMode == SettingsData.ProfileSwitchingDetectionMode.IntervalBased)
+ {
+ App.SmartProfileSwitcher?.Restart();
+ }
+
+ Logger.Log($"Detection interval changed to: {interval}ms");
+ }
+}
+
+
+
diff --git a/ThreeFingerDragOnWindows/settings/SettingsData.cs b/ThreeFingerDragOnWindows/settings/SettingsData.cs
index bd0f4c2..7320ea1 100644
--- a/ThreeFingerDragOnWindows/settings/SettingsData.cs
+++ b/ThreeFingerDragOnWindows/settings/SettingsData.cs
@@ -1,78 +1,193 @@
using System;
-
using System.Collections.Generic;
-
using System.Diagnostics;
-
using System.IO;
-
+using System.Linq;
using System.Text.Json;
-
-using System.Threading.Tasks;
-
+using System.Text.Json.Serialization;
using Windows.Storage;
-
using Microsoft.UI.Xaml;
-
using Microsoft.UI.Xaml.Controls;
-
+using ThreeFingerDragOnWindows.settings.profiles;
using ThreeFingerDragOnWindows.utils;
-
using WinUICommunity;
namespace ThreeFingerDragOnWindows.settings;
-public class SettingsData{
- private static int CURRENT_SETTINGS_VERSION = 5;
+public class SettingsData
+{
+ private const int CURRENT_SETTINGS_VERSION = 5;
// Other
public static bool DidVersionChanged { get; set; } = false;
public int SettingsVersion { get; set; } = 0;
- // Three finger drag Settings
- public bool ThreeFingerDrag { get; set; } = true;
+ // Profile management
+ public class ProfileInfo
+ {
+ public string Name { get; set; } = "Default";
+ public string FilePath { get; set; } = string.Empty;
+ }
+
+ public List Profiles { get; set; } = new();
+ public string ActiveProfilePath { get; set; } = string.Empty;
- public enum ThreeFingerDragButtonType {
- NONE,
- LEFT,
- RIGHT,
- MIDDLE,
+ [JsonIgnore]
+ private ThreeFingerDragProfile _activeProfile;
+
+ [JsonIgnore]
+ public ThreeFingerDragProfile ActiveProfile
+ {
+ get
+ {
+ if (_activeProfile == null)
+ {
+ LoadActiveProfile();
+ }
+ return _activeProfile;
+ }
}
- public ThreeFingerDragButtonType ThreeFingerDragButton { get; set; } = ThreeFingerDragButtonType.LEFT;
- public bool ThreeFingerDragAllowReleaseAndRestart { get; set; } = true;
- public int ThreeFingerDragReleaseDelay { get; set; } = 500;
+ private void LoadActiveProfile()
+ {
+ Profiles ??= new List();
+
+ bool needsSave = false;
+
+ if (string.IsNullOrEmpty(ActiveProfilePath) || !File.Exists(ActiveProfilePath))
+ {
+ var defaultProfilePath = GetProfileFilePath("Default");
+ _activeProfile = ThreeFingerDragProfile.Load(defaultProfilePath);
+ _activeProfile.ProfileName = "Default";
+ _activeProfile.Save(defaultProfilePath);
+
+ ActiveProfilePath = defaultProfilePath;
+ needsSave = true;
+ }
+ else
+ {
+ _activeProfile = ThreeFingerDragProfile.Load(ActiveProfilePath);
+ }
+
+ if (_activeProfile != null)
+ {
+ var profileEntry = Profiles.FirstOrDefault(p => p.FilePath == ActiveProfilePath);
+ if (profileEntry == null)
+ {
+ Profiles.Add(new ProfileInfo { Name = _activeProfile.ProfileName, FilePath = ActiveProfilePath });
+ needsSave = true;
+ }
+ else if (profileEntry.Name != _activeProfile.ProfileName)
+ {
+ profileEntry.Name = _activeProfile.ProfileName;
+ needsSave = true;
+ }
+ }
+
+ if (needsSave)
+ {
+ save();
+ }
+ }
-
- public class ThreeFingerDragConfig
+ public void SaveActiveProfile()
{
- public ThreeFingerDragConfig()
+ if (_activeProfile != null && !string.IsNullOrEmpty(ActiveProfilePath))
{
-
+ _activeProfile.Save(ActiveProfilePath);
}
- public ThreeFingerDragConfig(bool cursorMoveProperty, float cursorSpeedProperty, float cursorAccelerationProperty)
+ }
+
+ ///
+ /// Switches to a different profile
+ ///
+ public void SwitchToProfile(string profilePath)
+ {
+ // Save current profile before switching
+ SaveActiveProfile();
+
+ // Load new profile
+ ActiveProfilePath = profilePath;
+ _activeProfile = ThreeFingerDragProfile.Load(profilePath);
+
+ if (!Profiles.Any(p => p.FilePath == profilePath))
{
- ThreeFingerDragCursorMove = cursorMoveProperty;
- ThreeFingerDragCursorSpeed = cursorSpeedProperty;
- ThreeFingerDragCursorAcceleration = cursorAccelerationProperty;
+ Profiles.Add(new ProfileInfo { Name = _activeProfile.ProfileName, FilePath = profilePath });
}
- public bool ThreeFingerDragCursorMove { get; set; } = true;
- public float ThreeFingerDragCursorSpeed { get; set; } = 30;
- public float ThreeFingerDragCursorAcceleration { get; set; } = 10;
+ save();
+ Logger.Log($"Switched to profile: {_activeProfile.ProfileName}");
}
- public Dictionary ThreeFingerDeviceDragCursorConfigs { get; set; }
-
- public int ThreeFingerDragCursorAveraging { get; set; } = 1;
- public int ThreeFingerDragMaxFingerMoveDistance{ get; set; } = 0;
+ ///
+ /// Renames the active profile and updates the filename to match
+ ///
+ public void RenameActiveProfile(string newName)
+ {
+ if (_activeProfile == null || string.IsNullOrEmpty(ActiveProfilePath))
+ {
+ return;
+ }
+
+ var oldPath = ActiveProfilePath;
+ var newPath = GetProfileFilePath(newName);
- public int ThreeFingerDragStartThreshold { get; set; } = 100;
- public int ThreeFingerDragStopThreshold { get; set; } = 10;
-
- // Other settings
+ // Update the profile name
+ _activeProfile.ProfileName = newName;
- public enum StartupActionType{
+ // If the path would be different, rename the file
+ if (oldPath != newPath)
+ {
+ // Save to new location
+ _activeProfile.Save(newPath);
+
+ // Delete old file - File.Delete doesn't throw if file doesn't exist
+ 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}");
+ }
+
+ ActiveProfilePath = newPath;
+
+ var profileInfo = Profiles.FirstOrDefault(p => p.FilePath == oldPath);
+ if (profileInfo != null)
+ {
+ profileInfo.Name = newName;
+ profileInfo.FilePath = newPath;
+ }
+
+ save();
+ }
+ else
+ {
+ // Just save the profile with the new name
+ _activeProfile.Save(ActiveProfilePath);
+ }
+ }
+
+ public static string GetProfileFilePath(string profileName)
+ {
+ var sanitizedName = ThreeFingerDragProfile.SanitizeProfileName(profileName);
+ return Path.Combine(GetProfilesDirectory(), $"{sanitizedName}.xml");
+ }
+
+ private static string GetProfilesDirectory()
+ {
+ var dirPath = Path.Combine(ApplicationData.Current.LocalFolder.Path, "profiles");
+ if (!Directory.Exists(dirPath))
+ {
+ Directory.CreateDirectory(dirPath);
+ }
+ return dirPath;
+ }
+
+ public enum StartupActionType
+ {
NONE,
ENABLE_ELEVATED_RUN_WITH_STARTUP,
DISABLE_ELEVATED_RUN_WITH_STARTUP,
@@ -86,48 +201,64 @@ public enum StartupActionType{
public bool RecordLogs { get; set; } = false;
+ public enum ProfileSwitchingDetectionMode
+ {
+ WindowsHook,
+ IntervalBased
+ }
+
+ public ProfileSwitchingDetectionMode DetectionMode { get; set; } = ProfileSwitchingDetectionMode.WindowsHook;
+ public int DetectionInterval { get; set; } = 2000;
- public static SettingsData load(){
+ public static SettingsData load()
+ {
Logger.Log("Loading settings...");
var filePath = getPath(true);
SettingsData up;
- try{
- var jsonString = File.ReadAllText(filePath);
- up = JsonSerializer.Deserialize(jsonString);
+ try
+ {
+ var jsonString = File.ReadAllText(filePath);
+ up = JsonSerializer.Deserialize(jsonString) ?? new SettingsData();
Logger.Log($"Settings loaded, version = {up.SettingsVersion}");
- } catch(Exception e){
+ }
+ catch (Exception e)
+ {
Console.WriteLine(e);
up = new SettingsData();
up.save();
}
- if (up.ThreeFingerDeviceDragCursorConfigs == null)
- {
- up.ThreeFingerDeviceDragCursorConfigs = new Dictionary(2);
- up.save();
- }
+ up.Profiles ??= new List();
- if(up.SettingsVersion < 1){
+ if (up.SettingsVersion < 1)
+ {
Logger.Log("Updating settings to version 1");
up.save();
}
- if(up.SettingsVersion < 2){
+ if (up.SettingsVersion < 2)
+ {
Logger.Log("Updating settings to version 2");
- if(up.RunElevated && StartupManager.IsElevatedStartupOn()){
-
- if(Utils.IsAppRunningAsAdministrator()){
+ if (up.RunElevated && StartupManager.IsElevatedStartupOn())
+ {
+ if (Utils.IsAppRunningAsAdministrator())
+ {
StartupManager.DisableElevatedStartup();
StartupManager.EnableElevatedStartup();
- } else{
- Utils.runOnMainThreadAfter(2000, () => {
- if(App.SettingsWindow?.Content?.XamlRoot == null){
+ }
+ else
+ {
+ Utils.runOnMainThreadAfter(2000, () =>
+ {
+ if (App.SettingsWindow?.Content?.XamlRoot == null)
+ {
Logger.Log("SettingsWindow not ready, skipping v2.0.3 upgrade dialog");
return;
}
- ContentDialog dialog = new ContentDialog{
+ ContentDialog dialog = new ContentDialog
+ {
XamlRoot = App.SettingsWindow.Content.XamlRoot,
Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style,
Title = "Fixing startup task issue",
@@ -138,44 +269,48 @@ public static SettingsData load(){
});
}
}
+ }
+ if (up.SettingsVersion < 5)
+ {
+ Logger.Log("Updating settings to version 5 - migrating to profiles");
}
- if(up.SettingsVersion != CURRENT_SETTINGS_VERSION){
+ if (up.SettingsVersion != CURRENT_SETTINGS_VERSION)
+ {
DidVersionChanged = true;
up.save();
}
+ up.LoadActiveProfile();
+
return up;
}
- public void save(){
-
+ public void save()
+ {
SettingsVersion = CURRENT_SETTINGS_VERSION;
var options = new JsonSerializerOptions { WriteIndented = true };
-
var jsonString = JsonSerializer.Serialize(this, options);
var filePath = getPath(false);
-
File.WriteAllText(filePath, jsonString);
-
}
- private static string getPath(bool createIfEmpty){
-
+ private static string getPath(bool createIfEmpty)
+ {
var dirPath = ApplicationData.Current.LocalFolder.Path;
-
var filePath = Path.Combine(dirPath, "preferences.json");
-
+
Logger.Log("filepath: " + filePath);
- if(!Directory.Exists(dirPath) || !File.Exists(filePath)){
+ if (!Directory.Exists(dirPath) || !File.Exists(filePath))
+ {
Logger.Log("First run: creating settings file");
@@ -183,7 +318,7 @@ private static string getPath(bool createIfEmpty){
DidVersionChanged = true;
- if(createIfEmpty) new SettingsData().save(); // Wait for the async save to complete
+ if (createIfEmpty) new SettingsData().save(); // Wait for the async save to complete
}
diff --git a/ThreeFingerDragOnWindows/settings/SettingsWindow.xaml b/ThreeFingerDragOnWindows/settings/SettingsWindow.xaml
index 4ff363f..195c509 100644
--- a/ThreeFingerDragOnWindows/settings/SettingsWindow.xaml
+++ b/ThreeFingerDragOnWindows/settings/SettingsWindow.xaml
@@ -11,28 +11,87 @@
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
@@ -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;
}