From 2d00566975ef2dfcd349f8514e95471ae0c5bdd1 Mon Sep 17 00:00:00 2001 From: Shuai Yuan Date: Mon, 14 Jul 2025 21:54:47 +0800 Subject: [PATCH] Added support for changing shortcut in conflict window. Signed-off-by: Shuai Yuan --- .../HotkeyConflicts/ModuleHotkeyData.cs | 65 +- .../Settings.UI/PowerToys.Settings.csproj | 4 - .../Dashboard/ShortcutConflictControl.xaml.cs | 24 +- .../ShortcutConflictDialogContentControl.xaml | 198 ---- ...ortcutConflictDialogContentControl.xaml.cs | 269 ----- .../Dashboard/ShortcutConflictWindow.xaml | 180 ++++ .../Dashboard/ShortcutConflictWindow.xaml.cs | 121 +++ .../ShortcutControl/ShortcutControl.xaml.cs | 2 +- .../ViewModels/ShortcutConflictViewModel.cs | 980 ++++++++++++++++++ 9 files changed, 1347 insertions(+), 496 deletions(-) delete mode 100644 src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictDialogContentControl.xaml delete mode 100644 src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictDialogContentControl.xaml.cs create mode 100644 src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml create mode 100644 src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs create mode 100644 src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleHotkeyData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleHotkeyData.cs index 2b8de94846..a52ae9d47f 100644 --- a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleHotkeyData.cs +++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleHotkeyData.cs @@ -4,20 +4,75 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts { - public class ModuleHotkeyData + public class ModuleHotkeyData : INotifyPropertyChanged { - public string ModuleName { get; set; } + private string _moduleName; + private string _hotkeyName; + private HotkeySettings _hotkeySettings; + private bool _isSystemConflict; - public string HotkeyName { get; set; } + public event PropertyChangedEventHandler PropertyChanged; - public HotkeySettings HotkeySettings { get; set; } + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } - public bool IsSystemConflict { get; set; } + public string ModuleName + { + get => _moduleName; + set + { + if (_moduleName != value) + { + _moduleName = value; + } + } + } + + public string HotkeyName + { + get => _hotkeyName; + set + { + if (_hotkeyName != value) + { + _hotkeyName = value; + } + } + } + + public HotkeySettings HotkeySettings + { + get => _hotkeySettings; + set + { + if (_hotkeySettings != value) + { + _hotkeySettings = value; + OnPropertyChanged(); + } + } + } + + public bool IsSystemConflict + { + get => _isSystemConflict; + set + { + if (_isSystemConflict != value) + { + _isSystemConflict = value; + } + } + } } } diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj index b880154c44..1c9cc28ff6 100644 --- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj +++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj @@ -24,7 +24,6 @@ - @@ -135,9 +134,6 @@ Always - - MSBuild:Compile - MSBuild:Compile diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs index 3b7d6bc5ed..d1c4ea256d 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs @@ -106,32 +106,18 @@ namespace Microsoft.PowerToys.Settings.UI.Controls Visibility = HasConflicts ? Visibility.Visible : Visibility.Collapsed; } - private async void ShortcutConflictBtn_Click(object sender, RoutedEventArgs e) + private void ShortcutConflictBtn_Click(object sender, RoutedEventArgs e) { if (AllHotkeyConflictsData == null || !HasConflicts) { return; } - var contentControl = new ShortcutConflictDialogContentControl - { - ConflictsData = AllHotkeyConflictsData, - }; + // Create and show the new window instead of dialog + var conflictWindow = new ShortcutConflictWindow(); - var conflictDialog = new ContentDialog - { - Content = contentControl, - XamlRoot = this.XamlRoot, - RequestedTheme = this.ActualTheme, - }; - - // Handle navigation request to close dialog - contentControl.DialogCloseRequested += (s, args) => - { - conflictDialog.Hide(); - }; - - await conflictDialog.ShowAsync(); + // Show the window + conflictWindow.Activate(); } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictDialogContentControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictDialogContentControl.xaml deleted file mode 100644 index cd2cd33288..0000000000 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictDialogContentControl.xaml +++ /dev/null @@ -1,198 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictDialogContentControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictDialogContentControl.xaml.cs deleted file mode 100644 index e13ac61ec6..0000000000 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictDialogContentControl.xaml.cs +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using CommunityToolkit.WinUI.Controls; -using Microsoft.PowerToys.Settings.UI.Helpers; -using Microsoft.PowerToys.Settings.UI.Library; -using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; -using Microsoft.PowerToys.Settings.UI.Services; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Controls.Primitives; -using Microsoft.UI.Xaml.Data; -using Microsoft.UI.Xaml.Input; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Navigation; -using Windows.Foundation; -using Windows.Foundation.Collections; - -namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard -{ - public sealed partial class ShortcutConflictDialogContentControl : UserControl, INotifyPropertyChanged - { - public static readonly DependencyProperty ConflictsDataProperty = - DependencyProperty.Register( - nameof(ConflictsData), - typeof(AllHotkeyConflictsData), - typeof(ShortcutConflictDialogContentControl), - new PropertyMetadata(null, OnConflictsDataChanged)); - - public AllHotkeyConflictsData ConflictsData - { - get => (AllHotkeyConflictsData)GetValue(ConflictsDataProperty); - set => SetValue(ConflictsDataProperty, value); - } - - public List ConflictItems { get; private set; } = new List(); - - // Event to close the dialog when navigation occurs - public event EventHandler DialogCloseRequested; - - private static void OnConflictsDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is ShortcutConflictDialogContentControl content) - { - content.UpdateConflictItems(); - } - } - - public ShortcutConflictDialogContentControl() - { - InitializeComponent(); - DataContext = this; - } - - private void UpdateConflictItems() - { - var items = new List(); - - if (ConflictsData?.InAppConflicts != null) - { - foreach (var conflict in ConflictsData.InAppConflicts) - { - // Ensure each module has HotkeySettings for the ShortcutControl - foreach (var module in conflict.Modules) - { - if (module.HotkeySettings == null) - { - // Create HotkeySettings from the conflict hotkey data - module.HotkeySettings = ConvertToHotkeySettings(conflict.Hotkey, module.HotkeyName, module.ModuleName, false); - } - - // Mark as having conflict and set conflict properties - module.HotkeySettings.HasConflict = true; - module.HotkeySettings.IsSystemConflict = false; - module.HotkeySettings.ConflictDescription = GetConflictDescription(conflict, module, false); - module.IsSystemConflict = false; // In-app conflicts are not system conflicts - } - } - - items.AddRange(ConflictsData.InAppConflicts); - } - - if (ConflictsData?.SystemConflicts != null) - { - foreach (var conflict in ConflictsData.SystemConflicts) - { - // Ensure each module has HotkeySettings for the ShortcutControl - foreach (var module in conflict.Modules) - { - if (module.HotkeySettings == null) - { - // Create HotkeySettings from the conflict hotkey data - module.HotkeySettings = ConvertToHotkeySettings(conflict.Hotkey, module.HotkeyName, module.ModuleName, true); - } - - // Mark as having conflict and set conflict properties - module.HotkeySettings.HasConflict = true; - module.HotkeySettings.IsSystemConflict = true; - module.HotkeySettings.ConflictDescription = GetConflictDescription(conflict, module, true); - module.IsSystemConflict = true; // System conflicts - } - } - - items.AddRange(ConflictsData.SystemConflicts); - } - - ConflictItems = items; - OnPropertyChanged(nameof(ConflictItems)); - } - - private HotkeySettings ConvertToHotkeySettings(HotkeyData hotkeyData, string hotkeyName, string moduleName, bool isSystemConflict) - { - // Convert HotkeyData to HotkeySettings using actual data from hotkeyData - return new HotkeySettings( - win: hotkeyData.Win, - ctrl: hotkeyData.Ctrl, - alt: hotkeyData.Alt, - shift: hotkeyData.Shift, - code: hotkeyData.Key, - hotkeyName: hotkeyName, - ownerModuleName: moduleName, - hasConflict: true) // Always set to true since this is a conflict dialog - { - IsSystemConflict = isSystemConflict, - }; - } - - private void SettingsCard_Loaded(object sender, RoutedEventArgs e) - { - if (sender is SettingsCard card && card.DataContext is ModuleHotkeyData moduleData) - { - var iconPath = GetModuleIconPath(moduleData.ModuleName); - card.HeaderIcon = new BitmapIcon - { - UriSource = new Uri(iconPath), - ShowAsMonochrome = false, - }; - } - } - - private string GetModuleIconPath(string moduleName) - { - return moduleName?.ToLowerInvariant() switch - { - "advancedpaste" => "ms-appx:///Assets/Settings/Icons/AdvancedPaste.png", - "alwaysontop" => "ms-appx:///Assets/Settings/Icons/AlwaysOnTop.png", - "awake" => "ms-appx:///Assets/Settings/Icons/Awake.png", - "cmdpal" => "ms-appx:///Assets/Settings/Icons/CmdPal.png", - "colorpicker" => "ms-appx:///Assets/Settings/Icons/ColorPicker.png", - "cropandlock" => "ms-appx:///Assets/Settings/Icons/CropAndLock.png", - "environmentvariables" => "ms-appx:///Assets/Settings/Icons/EnvironmentVariables.png", - "fancyzones" => "ms-appx:///Assets/Settings/Icons/FancyZones.png", - "filelocksmith" => "ms-appx:///Assets/Settings/Icons/FileLocksmith.png", - "findmymouse" => "ms-appx:///Assets/Settings/Icons/FindMyMouse.png", - "hosts" => "ms-appx:///Assets/Settings/Icons/Hosts.png", - "imageresizer" => "ms-appx:///Assets/Settings/Icons/ImageResizer.png", - "keyboardmanager" => "ms-appx:///Assets/Settings/Icons/KeyboardManager.png", - "measuretool" => "ms-appx:///Assets/Settings/Icons/ScreenRuler.png", - "mousehighlighter" => "ms-appx:///Assets/Settings/Icons/MouseHighlighter.png", - "mousejump" => "ms-appx:///Assets/Settings/Icons/MouseJump.png", - "mousepointer" => "ms-appx:///Assets/Settings/Icons/MouseCrosshairs.png", - "mousepointeraccessibility" => "ms-appx:///Assets/Settings/Icons/MouseCrosshairs.png", - "mousepointercrosshairs" => "ms-appx:///Assets/Settings/Icons/MouseCrosshairs.png", - "mousewithoutborders" => "ms-appx:///Assets/Settings/Icons/MouseWithoutBorders.png", - "newplus" => "ms-appx:///Assets/Settings/Icons/NewPlus.png", - "peek" => "ms-appx:///Assets/Settings/Icons/Peek.png", - "poweraccent" => "ms-appx:///Assets/Settings/Icons/QuickAccent.png", - "powerlauncher" => "ms-appx:///Assets/Settings/Icons/PowerToysRun.png", - "powerocr" => "ms-appx:///Assets/Settings/Icons/TextExtractor.png", - "powerpreview" => "ms-appx:///Assets/Settings/Icons/PowerPreview.png", - "powerrename" => "ms-appx:///Assets/Settings/Icons/PowerRename.png", - "registrypreview" => "ms-appx:///Assets/Settings/Icons/RegistryPreview.png", - "shortcutguide" => "ms-appx:///Assets/Settings/Icons/ShortcutGuide.png", - "workspaces" => "ms-appx:///Assets/Settings/Icons/Workspaces.png", - "zoomit" => "ms-appx:///Assets/Settings/Icons/ZoomIt.png", - _ => "ms-appx:///Assets/Settings/Icons/PowerToys.png", - }; - } - - private string GetConflictDescription(HotkeyConflictGroupData conflict, ModuleHotkeyData currentModule, bool isSystemConflict) - { - if (isSystemConflict) - { - return "Conflicts with system shortcut"; - } - - // For in-app conflicts, list other conflicting modules - var otherModules = conflict.Modules - .Where(m => m.ModuleName != currentModule.ModuleName) - .Select(m => m.ModuleName) - .ToList(); - - if (otherModules.Count == 1) - { - return $"Conflicts with {otherModules[0]}"; - } - else if (otherModules.Count > 1) - { - return $"Conflicts with: {string.Join(", ", otherModules)}"; - } - - return "Shortcut conflict detected"; - } - - private void SettingsCard_Click(object sender, RoutedEventArgs e) - { - if (sender is SettingsCard settingsCard && settingsCard.DataContext is ModuleHotkeyData moduleData) - { - var moduleName = moduleData.ModuleName; - - // Navigate to the module's settings page - if (ModuleNavigationHelper.NavigateToModulePage(moduleName)) - { - // Successfully navigated, close the dialog - DialogCloseRequested?.Invoke(this, EventArgs.Empty); - } - else - { - // If navigation fails, try to handle special cases - HandleSpecialModuleNavigation(moduleName); - } - } - } - - private void HandleSpecialModuleNavigation(string moduleName) - { - // Handle special cases for modules that might have different navigation logic - switch (moduleName?.ToLowerInvariant()) - { - case "mouse highlighter": - case "mouse jump": - case "mouse pointer crosshairs": - case "find my mouse": - // These are all part of MouseUtils - if (ModuleNavigationHelper.NavigateToModulePage("MouseHighlighter")) - { - DialogCloseRequested?.Invoke(this, EventArgs.Empty); - } - - break; - - case "system": - case "windows": - // System conflicts - cannot navigate to a specific page - // Show a message or do nothing - break; - - default: - // Try a fallback navigation or show an error message - System.Diagnostics.Debug.WriteLine($"Could not navigate to settings page for module: {moduleName}"); - break; - } - } - - public event PropertyChangedEventHandler PropertyChanged; - - private void OnPropertyChanged(string propertyName) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - } -} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml new file mode 100644 index 0000000000..be10b0cac4 --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs new file mode 100644 index 0000000000..aa25f4a735 --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using CommunityToolkit.WinUI.Controls; +using Microsoft.PowerToys.Settings.UI.Helpers; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; +using Microsoft.PowerToys.Settings.UI.ViewModels; +using Microsoft.PowerToys.Settings.UI.Views; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.Graphics; + +namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard +{ + public sealed partial class ShortcutConflictWindow : Window + { + public ShortcutConflictViewModel DataContext { get; } + + public ShortcutConflictViewModel ViewModel { get; private set; } + + public ShortcutConflictWindow() + { + var settingsUtils = new SettingsUtils(); + ViewModel = new ShortcutConflictViewModel( + settingsUtils, + SettingsRepository.GetInstance(settingsUtils), + ShellPage.SendDefaultIPCMessage); + + DataContext = ViewModel; + InitializeComponent(); + + // Set window size using AppWindow API + this.AppWindow.Resize(new SizeInt32(900, 1200)); + + // Set window properties + this.AppWindow.SetIcon("Assets/Settings/Icons/PowerToys.ico"); + this.AppWindow.TitleBar.ExtendsContentIntoTitleBar = true; + this.AppWindow.TitleBar.ButtonBackgroundColor = Microsoft.UI.Colors.Transparent; + this.AppWindow.TitleBar.ButtonInactiveBackgroundColor = Microsoft.UI.Colors.Transparent; + + // Center the window on screen + this.CenterOnScreen(); + + ViewModel.OnPageLoaded(); + + Closed += (s, e) => ViewModel?.Dispose(); + } + + private void CenterOnScreen() + { + var displayArea = DisplayArea.GetFromWindowId(this.AppWindow.Id, DisplayAreaFallback.Nearest); + if (displayArea != null) + { + var windowSize = this.AppWindow.Size; + var centeredPosition = new PointInt32 + { + X = (displayArea.WorkArea.Width - windowSize.Width) / 2, + Y = (displayArea.WorkArea.Height - windowSize.Height) / 2, + }; + this.AppWindow.Move(centeredPosition); + } + } + + private void SettingsCard_Click(object sender, RoutedEventArgs e) + { + if (sender is SettingsCard settingsCard && + settingsCard.DataContext is ModuleHotkeyData moduleData) + { + var moduleName = moduleData.ModuleName; + + // Navigate to the module's settings page + if (ModuleNavigationHelper.NavigateToModulePage(moduleName)) + { + this.Close(); + } + } + } + + private void SettingsCard_Loaded(object sender, RoutedEventArgs e) + { + if (sender is SettingsCard card && card.DataContext is ModuleHotkeyData moduleData) + { + var iconPath = GetModuleIconPath(moduleData.ModuleName); + card.HeaderIcon = new BitmapIcon + { + UriSource = new Uri(iconPath), + ShowAsMonochrome = false, + }; + } + } + + private string GetModuleIconPath(string moduleName) + { + return moduleName?.ToLowerInvariant() switch + { + "advancedpaste" => "ms-appx:///Assets/Settings/Icons/AdvancedPaste.png", + "alwaysontop" => "ms-appx:///Assets/Settings/Icons/AlwaysOnTop.png", + "colorpicker" => "ms-appx:///Assets/Settings/Icons/ColorPicker.png", + "cropandlock" => "ms-appx:///Assets/Settings/Icons/CropAndLock.png", + "fancyzones" => "ms-appx:///Assets/Settings/Icons/FancyZones.png", + "mousehighlighter" => "ms-appx:///Assets/Settings/Icons/MouseHighlighter.png", + "mousepointercrosshairs" => "ms-appx:///Assets/Settings/Icons/MouseCrosshairs.png", + "findmymouse" => "ms-appx:///Assets/Settings/Icons/FindMyMouse.png", + "mousejump" => "ms-appx:///Assets/Settings/Icons/MouseJump.png", + "peek" => "ms-appx:///Assets/Settings/Icons/Peek.png", + "powerlauncher" => "ms-appx:///Assets/Settings/Icons/PowerToysRun.png", + "measuretool" => "ms-appx:///Assets/Settings/Icons/ScreenRuler.png", + "shortcutguide" => "ms-appx:///Assets/Settings/Icons/ShortcutGuide.png", + "powerocr" => "ms-appx:///Assets/Settings/Icons/TextExtractor.png", + "workspaces" => "ms-appx:///Assets/Settings/Icons/Workspaces.png", + "cmdpal" => "ms-appx:///Assets/Settings/Icons/CmdPal.png", + _ => "ms-appx:///Assets/Settings/Icons/PowerToys.png", + }; + } + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs index 615bd79dda..2c7c3acebd 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs @@ -665,7 +665,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls private void SetKeys() { - var keys = HotkeySettings.GetKeysList(); + var keys = HotkeySettings?.GetKeysList(); if (keys != null && keys.Count > 0) { diff --git a/src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs new file mode 100644 index 0000000000..2499643042 --- /dev/null +++ b/src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs @@ -0,0 +1,980 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Windows.Threading; +using Microsoft.PowerToys.Settings.UI.Controls; +using Microsoft.PowerToys.Settings.UI.Helpers; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Microsoft.PowerToys.Settings.UI.Views; + +namespace Microsoft.PowerToys.Settings.UI.ViewModels +{ + public class ShortcutConflictViewModel : PageViewModelBase, IDisposable + { + private readonly ISettingsUtils _settingsUtils; + private readonly ISettingsRepository _generalSettingsRepository; + private readonly Dictionary _moduleViewModels = new(); + private readonly Dictionary> _viewModelFactories = new(); + private readonly Dictionary _originalSettings = new(); + + private AllHotkeyConflictsData _conflictsData = new(); + private ObservableCollection _conflictItems = new(); + private bool _hasModifications; + private bool _hasConflicts; + + private Dispatcher dispatcher; + + public ShortcutConflictViewModel( + ISettingsUtils settingsUtils, + ISettingsRepository settingsRepository, + Func ipcMSGCallBackFunc) + : base(ipcMSGCallBackFunc) + { + dispatcher = Dispatcher.CurrentDispatcher; + _settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils)); + _generalSettingsRepository = settingsRepository ?? throw new ArgumentNullException(nameof(settingsRepository)); + + SendConfigMSG = ipcMSGCallBackFunc; + + InitializeViewModelFactories(); + } + + public AllHotkeyConflictsData ConflictsData + { + get => _conflictsData; + set + { + if (Set(ref _conflictsData, value)) + { + UpdateConflictItems(); + OnPropertyChanged(); + } + } + } + + public ObservableCollection ConflictItems + { + get => _conflictItems; + private set => Set(ref _conflictItems, value); + } + + public bool HasModifications + { + get => _hasModifications; + private set => Set(ref _hasModifications, value); + } + + public bool HasConflicts + { + get => _hasConflicts; + private set => Set(ref _hasConflicts, value); + } + + protected override string ModuleName => "ShortcutConflicts"; + + private Func SendConfigMSG { get; } + + private void InitializeViewModelFactories() + { + try + { + _viewModelFactories["advancedpaste"] = () => new AdvancedPasteViewModel( + _settingsUtils, + _generalSettingsRepository, + SettingsRepository.GetInstance(_settingsUtils), + SendConfigMSG); + + _viewModelFactories["alwaysontop"] = () => new AlwaysOnTopViewModel( + _settingsUtils, + _generalSettingsRepository, + SettingsRepository.GetInstance(_settingsUtils), + SendConfigMSG); + + _viewModelFactories["colorpicker"] = () => new ColorPickerViewModel( + _settingsUtils, + _generalSettingsRepository, + SettingsRepository.GetInstance(_settingsUtils), + SendConfigMSG); + + _viewModelFactories["cropandlock"] = () => new CropAndLockViewModel( + _settingsUtils, + _generalSettingsRepository, + SettingsRepository.GetInstance(_settingsUtils), + SendConfigMSG); + + _viewModelFactories["measuretool"] = () => new MeasureToolViewModel( + _settingsUtils, + _generalSettingsRepository, + SettingsRepository.GetInstance(_settingsUtils), + SendConfigMSG); + + _viewModelFactories["shortcutguide"] = () => new ShortcutGuideViewModel( + _settingsUtils, + _generalSettingsRepository, + SettingsRepository.GetInstance(_settingsUtils), + SendConfigMSG); + + _viewModelFactories["powerocr"] = () => new PowerOcrViewModel( + _settingsUtils, + _generalSettingsRepository, + SettingsRepository.GetInstance(_settingsUtils), + SendConfigMSG); + + _viewModelFactories["workspaces"] = () => new WorkspacesViewModel( + _settingsUtils, + _generalSettingsRepository, + SettingsRepository.GetInstance(_settingsUtils), + SendConfigMSG); + + _viewModelFactories["peek"] = () => new PeekViewModel( + _settingsUtils, + _generalSettingsRepository, + SendConfigMSG, + Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread()); + + _viewModelFactories["mouseutils"] = () => new MouseUtilsViewModel( + _settingsUtils, + _generalSettingsRepository, + SettingsRepository.GetInstance(_settingsUtils), + SettingsRepository.GetInstance(_settingsUtils), + SettingsRepository.GetInstance(_settingsUtils), + SettingsRepository.GetInstance(_settingsUtils), + SendConfigMSG); + + /*_viewModelFactories["fancyzones"] = () => new FancyZonesViewModel( + _settingsUtils, + _generalSettingsRepository, + SettingsRepository.GetInstance(_settingsUtils), + SendConfigMSG); + + _viewModelFactories["powerlauncher"] = () => new PowerLauncherViewModel( + _settingsUtils, + _generalSettingsRepository, + SettingsRepository.GetInstance(_settingsUtils), + SendConfigMSG); + + _viewModelFactories["cmdpal"] = () => new CmdPalViewModel( + _settingsUtils, + _generalSettingsRepository, + SendConfigMSG);*/ + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error initializing ViewModel factories: {ex.Message}"); + } + } + + private PageViewModelBase GetOrCreateViewModel(string moduleKey) + { + if (!_moduleViewModels.TryGetValue(moduleKey, out var viewModel)) + { + if (_viewModelFactories.TryGetValue(moduleKey, out var factory)) + { + try + { + viewModel = factory(); + _moduleViewModels[moduleKey] = viewModel; + + System.Diagnostics.Debug.WriteLine($"Lazy-loaded ViewModel for module: {moduleKey}"); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error creating ViewModel for {moduleKey}: {ex.Message}"); + return null; + } + } + else + { + System.Diagnostics.Debug.WriteLine($"No factory found for module: {moduleKey}"); + return null; + } + } + + return viewModel; + } + + protected override void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e) + { + dispatcher.BeginInvoke(() => + { + ConflictsData = e.Conflicts ?? new AllHotkeyConflictsData(); + }); + } + + private void UpdateConflictItems() + { + var items = new ObservableCollection(); + _originalSettings.Clear(); + + if (ConflictsData?.InAppConflicts != null) + { + foreach (var conflict in ConflictsData.InAppConflicts) + { + ProcessConflictGroup(conflict, false); + items.Add(conflict); + } + } + + if (ConflictsData?.SystemConflicts != null) + { + foreach (var conflict in ConflictsData.SystemConflicts) + { + ProcessConflictGroup(conflict, true); + items.Add(conflict); + } + } + + ConflictItems = items; + HasConflicts = items.Count > 0; + OnPropertyChanged(nameof(ConflictItems)); + } + + private void ProcessConflictGroup(HotkeyConflictGroupData conflict, bool isSystemConflict) + { + foreach (var module in conflict.Modules) + { + module.PropertyChanged += OnModuleHotkeyDataPropertyChanged; + + module.HotkeySettings = GetHotkeySettingsFromViewModel(module.ModuleName, module.HotkeyName); + + if (module.HotkeySettings != null) + { + // Store original settings for rollback + var key = $"{module.ModuleName}_{module.HotkeyName}"; + _originalSettings[key] = module.HotkeySettings with { }; + + // Set conflict properties + module.HotkeySettings.HasConflict = true; + module.HotkeySettings.IsSystemConflict = isSystemConflict; + module.HotkeySettings.ConflictDescription = GetConflictDescription(conflict, module, isSystemConflict); + } + + module.IsSystemConflict = isSystemConflict; + } + } + + private void OnModuleHotkeyDataPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (sender is ModuleHotkeyData moduleData && e.PropertyName == nameof(ModuleHotkeyData.HotkeySettings)) + { + var key = $"{moduleData.ModuleName}_{moduleData.HotkeyName}"; + + UpdateModuleViewModelHotkeySettings(moduleData.ModuleName, moduleData.HotkeyName, moduleData.HotkeySettings); + } + } + + private void UpdateModuleViewModelHotkeySettings(string moduleName, string hotkeyName, HotkeySettings newHotkeySettings) + { + try + { + var moduleKey = GetModuleKey(moduleName); + var viewModel = GetOrCreateViewModel(moduleKey); + if (viewModel == null) + { + System.Diagnostics.Debug.WriteLine($"Failed to get or create ViewModel for {moduleName}"); + return; + } + + switch (moduleKey) + { + case "advancedpaste": + UpdateAdvancedPasteHotkeySettings(viewModel as AdvancedPasteViewModel, hotkeyName, newHotkeySettings); + break; + case "alwaysontop": + UpdateAlwaysOnTopHotkeySettings(viewModel as AlwaysOnTopViewModel, hotkeyName, newHotkeySettings); + break; + case "colorpicker": + UpdateColorPickerHotkeySettings(viewModel as ColorPickerViewModel, hotkeyName, newHotkeySettings); + break; + case "cropandlock": + UpdateCropAndLockHotkeySettings(viewModel as CropAndLockViewModel, hotkeyName, newHotkeySettings); + break; + case "fancyzones": + UpdateFancyZonesHotkeySettings(viewModel as FancyZonesViewModel, hotkeyName, newHotkeySettings); + break; + case "measuretool": + UpdateMeasureToolHotkeySettings(viewModel as MeasureToolViewModel, hotkeyName, newHotkeySettings); + break; + case "shortcutguide": + UpdateShortcutGuideHotkeySettings(viewModel as ShortcutGuideViewModel, hotkeyName, newHotkeySettings); + break; + case "powerocr": + UpdatePowerOcrHotkeySettings(viewModel as PowerOcrViewModel, hotkeyName, newHotkeySettings); + break; + case "workspaces": + UpdateWorkspacesHotkeySettings(viewModel as WorkspacesViewModel, hotkeyName, newHotkeySettings); + break; + case "peek": + UpdatePeekHotkeySettings(viewModel as PeekViewModel, hotkeyName, newHotkeySettings); + break; + case "powerlauncher": + UpdatePowerLauncherHotkeySettings(viewModel as PowerLauncherViewModel, hotkeyName, newHotkeySettings); + break; + case "mouseutils": + UpdateMouseUtilsHotkeySettings(viewModel as MouseUtilsViewModel, moduleName, hotkeyName, newHotkeySettings); + break; + default: + System.Diagnostics.Debug.WriteLine($"Unknown module key: {moduleKey}"); + break; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error updating hotkey settings for {moduleName}.{hotkeyName}: {ex.Message}"); + } + } + + // Update methods for each module + private void UpdateAdvancedPasteHotkeySettings(AdvancedPasteViewModel viewModel, string hotkeyName, HotkeySettings newHotkeySettings) + { + if (viewModel == null) + { + return; + } + + switch (hotkeyName?.ToLowerInvariant()) + { + case "advancedpasteui" or "advancedpasteuishortcut" or "activation_shortcut": + if (!AreHotkeySettingsEqual(viewModel.AdvancedPasteUIShortcut, newHotkeySettings)) + { + viewModel.AdvancedPasteUIShortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated AdvancedPaste AdvancedPasteUIShortcut"); + } + + break; + + case "pasteasplaintext" or "pasteasplaintextshortcut": + if (!AreHotkeySettingsEqual(viewModel.PasteAsPlainTextShortcut, newHotkeySettings)) + { + viewModel.PasteAsPlainTextShortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated AdvancedPaste PasteAsPlainTextShortcut"); + } + + break; + + case "pasteasmarkdown" or "pasteasmarkdownshortcut": + if (!AreHotkeySettingsEqual(viewModel.PasteAsMarkdownShortcut, newHotkeySettings)) + { + viewModel.PasteAsMarkdownShortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated AdvancedPaste PasteAsMarkdownShortcut"); + } + + break; + + case "pasteasjson" or "pasteasjsonshortcut": + if (!AreHotkeySettingsEqual(viewModel.PasteAsJsonShortcut, newHotkeySettings)) + { + viewModel.PasteAsJsonShortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated AdvancedPaste PasteAsJsonShortcut"); + } + + break; + + case "imagetotext" or "imagetotextshortcut": + if (viewModel.AdditionalActions?.ImageToText != null && + !AreHotkeySettingsEqual(viewModel.AdditionalActions.ImageToText.Shortcut, newHotkeySettings)) + { + viewModel.AdditionalActions.ImageToText.Shortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated AdvancedPaste ImageToText shortcut"); + } + + break; + + case "pasteastxtfile" or "pasteastxtfileshortcut": + if (viewModel.AdditionalActions?.PasteAsFile?.PasteAsTxtFile != null && + !AreHotkeySettingsEqual(viewModel.AdditionalActions.PasteAsFile.PasteAsTxtFile.Shortcut, newHotkeySettings)) + { + viewModel.AdditionalActions.PasteAsFile.PasteAsTxtFile.Shortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated AdvancedPaste PasteAsTxtFile shortcut"); + } + + break; + + case "pasteaspngfile" or "pasteaspngfileshortcut": + if (viewModel.AdditionalActions?.PasteAsFile?.PasteAsPngFile != null && + !AreHotkeySettingsEqual(viewModel.AdditionalActions.PasteAsFile.PasteAsPngFile.Shortcut, newHotkeySettings)) + { + viewModel.AdditionalActions.PasteAsFile.PasteAsPngFile.Shortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated AdvancedPaste PasteAsPngFile shortcut"); + } + + break; + + case "pasteashtmlfile" or "pasteashtmlfileshortcut": + if (viewModel.AdditionalActions?.PasteAsFile?.PasteAsHtmlFile != null && + !AreHotkeySettingsEqual(viewModel.AdditionalActions.PasteAsFile.PasteAsHtmlFile.Shortcut, newHotkeySettings)) + { + viewModel.AdditionalActions.PasteAsFile.PasteAsHtmlFile.Shortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated AdvancedPaste PasteAsHtmlFile shortcut"); + } + + break; + + case "transcodetomp3" or "transcodetomp3shortcut": + if (viewModel.AdditionalActions?.Transcode?.TranscodeToMp3 != null && + !AreHotkeySettingsEqual(viewModel.AdditionalActions.Transcode.TranscodeToMp3.Shortcut, newHotkeySettings)) + { + viewModel.AdditionalActions.Transcode.TranscodeToMp3.Shortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated AdvancedPaste TranscodeToMp3 shortcut"); + } + + break; + + case "transcodetomp4" or "transcodetomp4shortcut": + if (viewModel.AdditionalActions?.Transcode?.TranscodeToMp4 != null && + !AreHotkeySettingsEqual(viewModel.AdditionalActions.Transcode.TranscodeToMp4.Shortcut, newHotkeySettings)) + { + viewModel.AdditionalActions.Transcode.TranscodeToMp4.Shortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated AdvancedPaste TranscodeToMp4 shortcut"); + } + + break; + + case var customActionName when customActionName.StartsWith("customaction_", StringComparison.OrdinalIgnoreCase): + var parts = customActionName.Split('_'); + if (parts.Length == 2 && int.TryParse(parts[1], out int customActionId)) + { + var customAction = viewModel.CustomActions?.FirstOrDefault(ca => ca.Id == customActionId); + if (customAction != null && !AreHotkeySettingsEqual(customAction.Shortcut, newHotkeySettings)) + { + customAction.Shortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated AdvancedPaste CustomAction_{customActionId} shortcut"); + } + } + + break; + + default: + System.Diagnostics.Debug.WriteLine($"Unknown AdvancedPaste hotkey name: {hotkeyName}"); + break; + } + } + + private void UpdateAlwaysOnTopHotkeySettings(AlwaysOnTopViewModel viewModel, string hotkeyName, HotkeySettings newHotkeySettings) + { + if (viewModel == null) + { + return; + } + + // AlwaysOnTop module only has one hotkey setting + if (!AreHotkeySettingsEqual(viewModel.Hotkey, newHotkeySettings)) + { + viewModel.Hotkey = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated AlwaysOnTop hotkey settings"); + } + } + + private void UpdateColorPickerHotkeySettings(ColorPickerViewModel viewModel, string hotkeyName, HotkeySettings newHotkeySettings) + { + if (viewModel == null) + { + return; + } + + // ColorPicker module only has one activation shortcut + if (!AreHotkeySettingsEqual(viewModel.ActivationShortcut, newHotkeySettings)) + { + viewModel.ActivationShortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated ColorPicker hotkey settings"); + } + } + + private void UpdateCropAndLockHotkeySettings(CropAndLockViewModel viewModel, string hotkeyName, HotkeySettings newHotkeySettings) + { + if (viewModel == null) + { + return; + } + + // Update based on hotkey name for CropAndLock module + switch (hotkeyName?.ToLowerInvariant()) + { + case "thumbnail" or "thumbnailhotkey": + if (!AreHotkeySettingsEqual(viewModel.ThumbnailActivationShortcut, newHotkeySettings)) + { + viewModel.ThumbnailActivationShortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated CropAndLock ThumbnailActivationShortcut"); + } + + break; + + case "reparent" or "reparenthotkey": + if (!AreHotkeySettingsEqual(viewModel.ReparentActivationShortcut, newHotkeySettings)) + { + viewModel.ReparentActivationShortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated CropAndLock ReparentActivationShortcut"); + } + + break; + + default: + System.Diagnostics.Debug.WriteLine($"Unknown CropAndLock hotkey name: {hotkeyName}"); + break; + } + } + + private void UpdateFancyZonesHotkeySettings(FancyZonesViewModel viewModel, string hotkeyName, HotkeySettings newHotkeySettings) + { + if (viewModel == null) + { + return; + } + + // FancyZones module only has one editor hotkey + if (!AreHotkeySettingsEqual(viewModel.EditorHotkey, newHotkeySettings)) + { + viewModel.EditorHotkey = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated FancyZones EditorHotkey"); + } + } + + private void UpdateMeasureToolHotkeySettings(MeasureToolViewModel viewModel, string hotkeyName, HotkeySettings newHotkeySettings) + { + if (viewModel == null) + { + return; + } + + // MeasureTool module only has one activation shortcut + if (!AreHotkeySettingsEqual(viewModel.ActivationShortcut, newHotkeySettings)) + { + viewModel.ActivationShortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated MeasureTool ActivationShortcut"); + } + } + + private void UpdateShortcutGuideHotkeySettings(ShortcutGuideViewModel viewModel, string hotkeyName, HotkeySettings newHotkeySettings) + { + if (viewModel == null) + { + return; + } + + // ShortcutGuide module only has one shortcut to open the guide + if (!AreHotkeySettingsEqual(viewModel.OpenShortcutGuide, newHotkeySettings)) + { + viewModel.OpenShortcutGuide = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated ShortcutGuide OpenShortcutGuide"); + } + } + + private void UpdatePowerOcrHotkeySettings(PowerOcrViewModel viewModel, string hotkeyName, HotkeySettings newHotkeySettings) + { + if (viewModel == null) + { + return; + } + + // PowerOCR module only has one activation shortcut + if (!AreHotkeySettingsEqual(viewModel.ActivationShortcut, newHotkeySettings)) + { + viewModel.ActivationShortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated PowerOCR ActivationShortcut"); + } + } + + private void UpdateWorkspacesHotkeySettings(WorkspacesViewModel viewModel, string hotkeyName, HotkeySettings newHotkeySettings) + { + if (viewModel == null) + { + return; + } + + // Workspaces module only has one hotkey + if (!AreHotkeySettingsEqual(viewModel.Hotkey, newHotkeySettings)) + { + viewModel.Hotkey = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated Workspaces Hotkey"); + } + } + + private void UpdatePeekHotkeySettings(PeekViewModel viewModel, string hotkeyName, HotkeySettings newHotkeySettings) + { + if (viewModel == null) + { + return; + } + + // Peek module only has one activation shortcut + if (!AreHotkeySettingsEqual(viewModel.ActivationShortcut, newHotkeySettings)) + { + viewModel.ActivationShortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated Peek ActivationShortcut"); + } + } + + private void UpdatePowerLauncherHotkeySettings(PowerLauncherViewModel viewModel, string hotkeyName, HotkeySettings newHotkeySettings) + { + if (viewModel == null) + { + return; + } + + // PowerLauncher module only has one shortcut to open the launcher + if (!AreHotkeySettingsEqual(viewModel.OpenPowerLauncher, newHotkeySettings)) + { + viewModel.OpenPowerLauncher = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated PowerLauncher OpenPowerLauncher"); + } + } + + private void UpdateMouseUtilsHotkeySettings(MouseUtilsViewModel viewModel, string moduleName, string hotkeyName, HotkeySettings newHotkeySettings) + { + if (viewModel == null) + { + return; + } + + // Update based on specific mouse utility module name + switch (moduleName?.ToLowerInvariant()) + { + case "mousehighlighter": + if (!AreHotkeySettingsEqual(viewModel.MouseHighlighterActivationShortcut, newHotkeySettings)) + { + viewModel.MouseHighlighterActivationShortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated MouseUtils MouseHighlighterActivationShortcut"); + } + + break; + + case "mousejump": + if (!AreHotkeySettingsEqual(viewModel.MouseJumpActivationShortcut, newHotkeySettings)) + { + viewModel.MouseJumpActivationShortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated MouseUtils MouseJumpActivationShortcut"); + } + + break; + + case "mousepointercrosshairs": + if (!AreHotkeySettingsEqual(viewModel.MousePointerCrosshairsActivationShortcut, newHotkeySettings)) + { + viewModel.MousePointerCrosshairsActivationShortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated MouseUtils MousePointerCrosshairsActivationShortcut"); + } + + break; + + case "findmymouse": + if (!AreHotkeySettingsEqual(viewModel.FindMyMouseActivationShortcut, newHotkeySettings)) + { + viewModel.FindMyMouseActivationShortcut = newHotkeySettings; + System.Diagnostics.Debug.WriteLine($"Updated MouseUtils FindMyMouseActivationShortcut"); + } + + break; + + default: + System.Diagnostics.Debug.WriteLine($"Unknown MouseUtils module name: {moduleName}"); + break; + } + } + + // Helper methods + private bool AreHotkeySettingsEqual(HotkeySettings settings1, HotkeySettings settings2) + { + if (settings1 == null && settings2 == null) + { + return true; + } + + if (settings1 == null || settings2 == null) + { + return false; + } + + return settings1.Win == settings2.Win && + settings1.Ctrl == settings2.Ctrl && + settings1.Alt == settings2.Alt && + settings1.Shift == settings2.Shift && + settings1.Code == settings2.Code; + } + + private void UpdateHotkeySettingsProperties(HotkeySettings target, HotkeySettings source) + { + if (target == null || source == null) + { + return; + } + + target.Win = source.Win; + target.Ctrl = source.Ctrl; + target.Alt = source.Alt; + target.Shift = source.Shift; + target.Code = source.Code; + target.Key = source.Key; + } + + private ModuleHotkeyData FindModuleDataForHotkeySettings(HotkeySettings hotkeySettings) + { + foreach (var conflictGroup in ConflictItems) + { + foreach (var module in conflictGroup.Modules) + { + if (ReferenceEquals(module.HotkeySettings, hotkeySettings)) + { + return module; + } + } + } + + return null; + } + + private ModuleHotkeyData FindModuleDataByKey(string moduleName, string hotkeyName) + { + foreach (var conflictGroup in ConflictItems) + { + foreach (var module in conflictGroup.Modules) + { + if (module.ModuleName.Equals(moduleName, StringComparison.OrdinalIgnoreCase) && + module.HotkeyName.Equals(hotkeyName, StringComparison.OrdinalIgnoreCase)) + { + return module; + } + } + } + + return null; + } + + private HotkeySettings GetHotkeySettingsFromViewModel(string moduleName, string hotkeyName) + { + try + { + var moduleKey = GetModuleKey(moduleName); + var viewModel = GetOrCreateViewModel(moduleKey); + if (viewModel == null) + { + return null; + } + + return moduleKey switch + { + "advancedpaste" => GetAdvancedPasteHotkeySettings(viewModel as AdvancedPasteViewModel, hotkeyName), + "alwaysontop" => GetAlwaysOnTopHotkeySettings(viewModel as AlwaysOnTopViewModel, hotkeyName), + "colorpicker" => GetColorPickerHotkeySettings(viewModel as ColorPickerViewModel, hotkeyName), + "cropandlock" => GetCropAndLockHotkeySettings(viewModel as CropAndLockViewModel, hotkeyName), + "fancyzones" => GetFancyZonesHotkeySettings(viewModel as FancyZonesViewModel, hotkeyName), + "measuretool" => GetMeasureToolHotkeySettings(viewModel as MeasureToolViewModel, hotkeyName), + "shortcutguide" => GetShortcutGuideHotkeySettings(viewModel as ShortcutGuideViewModel, hotkeyName), + "powerocr" => GetPowerOcrHotkeySettings(viewModel as PowerOcrViewModel, hotkeyName), + "workspaces" => GetWorkspacesHotkeySettings(viewModel as WorkspacesViewModel, hotkeyName), + "peek" => GetPeekHotkeySettings(viewModel as PeekViewModel, hotkeyName), + "powerlauncher" => GetPowerLauncherHotkeySettings(viewModel as PowerLauncherViewModel, hotkeyName), + "mouseutils" => GetMouseUtilsHotkeySettings(viewModel as MouseUtilsViewModel, moduleName, hotkeyName), + "cmdpal" => GetCmdPalHotkeySettings(viewModel as CmdPalViewModel, hotkeyName), + _ => null, + }; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error getting hotkey settings for {moduleName}.{hotkeyName}: {ex.Message}"); + return null; + } + } + + private string GetModuleKey(string moduleName) + { + return moduleName?.ToLowerInvariant() switch + { + "mousehighlighter" or "mousejump" or "mousepointercrosshairs" or "findmymouse" => "mouseutils", + _ => moduleName?.ToLowerInvariant(), + }; + } + + // Get methods that return direct references to ViewModel properties for two-way binding + private HotkeySettings GetAdvancedPasteHotkeySettings(AdvancedPasteViewModel viewModel, string hotkeyName) + { + if (viewModel == null) + { + return null; + } + + return hotkeyName?.ToLowerInvariant() switch + { + "advancedpasteui" or "advancedpasteuishortcut" or "activation_shortcut" => viewModel.AdvancedPasteUIShortcut, + "pasteasplaintext" or "pasteasplaintextshortcut" => viewModel.PasteAsPlainTextShortcut, + "pasteasmarkdown" or "pasteasmarkdownshortcut" => viewModel.PasteAsMarkdownShortcut, + "pasteasjson" or "pasteasjsonshortcut" => viewModel.PasteAsJsonShortcut, + "imagetotext" or "imagetotextshortcut" => GetAdditionalActionShortcut(viewModel, "ImageToText"), + "pasteastxtfile" or "pasteastxtfileshortcut" => GetAdditionalActionShortcut(viewModel, "PasteAsTxtFile"), + "pasteaspngfile" or "pasteaspngfileshortcut" => GetAdditionalActionShortcut(viewModel, "PasteAsPngFile"), + "pasteashtmlfile" or "pasteashtmlfileshortcut" => GetAdditionalActionShortcut(viewModel, "PasteAsHtmlFile"), + "transcodetomp3" or "transcodetomp3shortcut" => GetAdditionalActionShortcut(viewModel, "TranscodeToMp3"), + "transcodetomp4" or "transcodetomp4shortcut" => GetAdditionalActionShortcut(viewModel, "TranscodeToMp4"), + _ when hotkeyName.StartsWith("customaction_", StringComparison.OrdinalIgnoreCase) => GetCustomActionShortcut(viewModel, hotkeyName), + _ => null, + }; + } + + private HotkeySettings GetAdditionalActionShortcut(AdvancedPasteViewModel viewModel, string actionName) + { + if (viewModel?.AdditionalActions == null) + { + return null; + } + + return actionName switch + { + "ImageToText" => viewModel.AdditionalActions.ImageToText?.Shortcut, + "PasteAsTxtFile" => viewModel.AdditionalActions.PasteAsFile?.PasteAsTxtFile?.Shortcut, + "PasteAsPngFile" => viewModel.AdditionalActions.PasteAsFile?.PasteAsPngFile?.Shortcut, + "PasteAsHtmlFile" => viewModel.AdditionalActions.PasteAsFile?.PasteAsHtmlFile?.Shortcut, + "TranscodeToMp3" => viewModel.AdditionalActions.Transcode?.TranscodeToMp3?.Shortcut, + "TranscodeToMp4" => viewModel.AdditionalActions.Transcode?.TranscodeToMp4?.Shortcut, + _ => null, + }; + } + + private HotkeySettings GetCustomActionShortcut(AdvancedPasteViewModel viewModel, string hotkeyName) + { + if (viewModel?.CustomActions == null) + { + return null; + } + + var parts = hotkeyName.Split('_'); + if (parts.Length == 2 && int.TryParse(parts[1], out int customActionId)) + { + var customAction = viewModel.CustomActions.FirstOrDefault(ca => ca.Id == customActionId); + return customAction?.Shortcut; + } + + return null; + } + + private HotkeySettings GetAlwaysOnTopHotkeySettings(AlwaysOnTopViewModel viewModel, string hotkeyName) + { + return viewModel?.Hotkey; + } + + private HotkeySettings GetColorPickerHotkeySettings(ColorPickerViewModel viewModel, string hotkeyName) + { + return viewModel?.ActivationShortcut; + } + + private HotkeySettings GetCropAndLockHotkeySettings(CropAndLockViewModel viewModel, string hotkeyName) + { + if (viewModel == null) + { + return null; + } + + return hotkeyName?.ToLowerInvariant() switch + { + "thumbnail" or "thumbnailhotkey" => viewModel.ThumbnailActivationShortcut, + "reparent" or "reparenthotkey" => viewModel.ReparentActivationShortcut, + _ => null, + }; + } + + private HotkeySettings GetFancyZonesHotkeySettings(FancyZonesViewModel viewModel, string hotkeyName) + { + return viewModel?.EditorHotkey; + } + + private HotkeySettings GetMeasureToolHotkeySettings(MeasureToolViewModel viewModel, string hotkeyName) + { + return viewModel?.ActivationShortcut; + } + + private HotkeySettings GetShortcutGuideHotkeySettings(ShortcutGuideViewModel viewModel, string hotkeyName) + { + return viewModel?.OpenShortcutGuide; + } + + private HotkeySettings GetPowerOcrHotkeySettings(PowerOcrViewModel viewModel, string hotkeyName) + { + return viewModel?.ActivationShortcut; + } + + private HotkeySettings GetWorkspacesHotkeySettings(WorkspacesViewModel viewModel, string hotkeyName) + { + return viewModel?.Hotkey; + } + + private HotkeySettings GetPeekHotkeySettings(PeekViewModel viewModel, string hotkeyName) + { + return viewModel?.ActivationShortcut; + } + + private HotkeySettings GetPowerLauncherHotkeySettings(PowerLauncherViewModel viewModel, string hotkeyName) + { + return viewModel?.OpenPowerLauncher; + } + + private HotkeySettings GetMouseUtilsHotkeySettings(MouseUtilsViewModel viewModel, string moduleName, string hotkeyName) + { + if (viewModel == null) + { + return null; + } + + return moduleName?.ToLowerInvariant() switch + { + "mousehighlighter" => viewModel.MouseHighlighterActivationShortcut, + "mousejump" => viewModel.MouseJumpActivationShortcut, + "mousepointercrosshairs" => viewModel.MousePointerCrosshairsActivationShortcut, + "findmymouse" => viewModel.FindMyMouseActivationShortcut, + _ => null, + }; + } + + private HotkeySettings GetCmdPalHotkeySettings(CmdPalViewModel viewModel, string hotkeyName) + { + return viewModel?.Hotkey; + } + + private string GetConflictDescription(HotkeyConflictGroupData conflict, ModuleHotkeyData currentModule, bool isSystemConflict) + { + if (isSystemConflict) + { + return "Conflicts with system shortcut"; + } + + var otherModules = conflict.Modules + .Where(m => m.ModuleName != currentModule.ModuleName) + .Select(m => m.ModuleName) + .ToList(); + + return otherModules.Count switch + { + 1 => $"Conflicts with {otherModules[0]}", + > 1 => $"Conflicts with: {string.Join(", ", otherModules)}", + _ => "Shortcut conflict detected", + }; + } + + public override void Dispose() + { + // Unsubscribe from property change events + foreach (var conflictGroup in ConflictItems) + { + foreach (var module in conflictGroup.Modules) + { + module.PropertyChanged -= OnModuleHotkeyDataPropertyChanged; + } + } + + // Dispose all created module ViewModels + foreach (var viewModel in _moduleViewModels.Values) + { + viewModel?.Dispose(); + } + + base.Dispose(); + } + } +}