PageViewModelBase.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Collections.Generic; 7 using System.Diagnostics; 8 using System.Linq; 9 using System.Threading.Tasks; 10 using Microsoft.PowerToys.Settings.UI.Helpers; 11 using Microsoft.PowerToys.Settings.UI.Library; 12 using Microsoft.PowerToys.Settings.UI.Library.Helpers; 13 using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; 14 using Microsoft.PowerToys.Settings.UI.Services; 15 16 namespace Microsoft.PowerToys.Settings.UI.ViewModels 17 { 18 public abstract class PageViewModelBase : Observable, IDisposable 19 { 20 private readonly Dictionary<string, bool> _hotkeyConflictStatus = new Dictionary<string, bool>(); 21 private readonly Dictionary<string, string> _hotkeyConflictTooltips = new Dictionary<string, string>(); 22 private bool _disposed; 23 24 protected abstract string ModuleName { get; } 25 26 protected PageViewModelBase() 27 { 28 if (GlobalHotkeyConflictManager.Instance != null) 29 { 30 GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated; 31 } 32 } 33 34 public virtual void OnPageLoaded() 35 { 36 Debug.WriteLine($"=== PAGE LOADED: {ModuleName} ==="); 37 GlobalHotkeyConflictManager.Instance?.RequestAllConflicts(); 38 } 39 40 /// <summary> 41 /// Handles updates to hotkey conflicts for the module. This method is called when the 42 /// <see cref="GlobalHotkeyConflictManager"/> raises the <c>ConflictsUpdated</c> event. 43 /// </summary> 44 /// <param name="sender">The source of the event, typically the <see cref="GlobalHotkeyConflictManager"/> instance.</param> 45 /// <param name="e">An <see cref="AllHotkeyConflictsEventArgs"/> object containing details about the hotkey conflicts.</param> 46 /// <remarks> 47 /// Derived classes can override this method to provide custom handling for hotkey conflicts. 48 /// Ensure that the overridden method maintains the expected behavior of processing and logging conflict data. 49 /// </remarks> 50 protected virtual void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e) 51 { 52 UpdateHotkeyConflictStatus(e.Conflicts); 53 var allHotkeySettings = GetAllHotkeySettings(); 54 55 void UpdateConflictProperties() 56 { 57 if (allHotkeySettings != null) 58 { 59 foreach (KeyValuePair<string, HotkeySettings[]> kvp in allHotkeySettings) 60 { 61 var module = kvp.Key; 62 var hotkeySettingsList = kvp.Value; 63 64 for (int i = 0; i < hotkeySettingsList.Length; i++) 65 { 66 var key = $"{module.ToLowerInvariant()}_{i}"; 67 hotkeySettingsList[i].HasConflict = GetHotkeyConflictStatus(key); 68 hotkeySettingsList[i].ConflictDescription = GetHotkeyConflictTooltip(key); 69 } 70 } 71 } 72 } 73 74 _ = Task.Run(() => 75 { 76 try 77 { 78 var settingsWindow = App.GetSettingsWindow(); 79 settingsWindow.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, UpdateConflictProperties); 80 } 81 catch 82 { 83 UpdateConflictProperties(); 84 } 85 }); 86 } 87 88 public virtual Dictionary<string, HotkeySettings[]> GetAllHotkeySettings() 89 { 90 return null; 91 } 92 93 protected ModuleConflictsData GetModuleRelatedConflicts(AllHotkeyConflictsData allConflicts) 94 { 95 var moduleConflicts = new ModuleConflictsData(); 96 97 if (allConflicts.InAppConflicts != null) 98 { 99 foreach (var conflict in allConflicts.InAppConflicts) 100 { 101 if (IsModuleInvolved(conflict)) 102 { 103 moduleConflicts.InAppConflicts.Add(conflict); 104 } 105 } 106 } 107 108 if (allConflicts.SystemConflicts != null) 109 { 110 foreach (var conflict in allConflicts.SystemConflicts) 111 { 112 if (IsModuleInvolved(conflict)) 113 { 114 moduleConflicts.SystemConflicts.Add(conflict); 115 } 116 } 117 } 118 119 return moduleConflicts; 120 } 121 122 private void ProcessMouseUtilsConflictGroup(HotkeyConflictGroupData conflict, HashSet<string> mouseUtilsModules, bool isSysConflict) 123 { 124 // Check if any of the modules in this conflict are MouseUtils submodules 125 var involvedMouseUtilsModules = conflict.Modules 126 .Where(module => mouseUtilsModules.Contains(module.ModuleName)) 127 .ToList(); 128 129 if (involvedMouseUtilsModules.Count != 0) 130 { 131 // For each involved MouseUtils module, mark the hotkey as having a conflict 132 foreach (var module in involvedMouseUtilsModules) 133 { 134 string hotkeyKey = $"{module.ModuleName.ToLowerInvariant()}_{module.HotkeyID}"; 135 _hotkeyConflictStatus[hotkeyKey] = true; 136 _hotkeyConflictTooltips[hotkeyKey] = isSysConflict 137 ? ResourceLoaderInstance.ResourceLoader.GetString("SysHotkeyConflictTooltipText") 138 : ResourceLoaderInstance.ResourceLoader.GetString("InAppHotkeyConflictTooltipText"); 139 } 140 } 141 } 142 143 protected virtual void UpdateHotkeyConflictStatus(AllHotkeyConflictsData allConflicts) 144 { 145 _hotkeyConflictStatus.Clear(); 146 _hotkeyConflictTooltips.Clear(); 147 148 // Since MouseUtils in Settings consolidates four modules: Find My Mouse, Mouse Highlighter, Mouse Pointer Crosshairs, and Mouse Jump 149 // We need to handle this case separately here. 150 if (string.Equals(ModuleName, "MouseUtils", StringComparison.OrdinalIgnoreCase)) 151 { 152 var mouseUtilsModules = new HashSet<string> 153 { 154 FindMyMouseSettings.ModuleName, 155 MouseHighlighterSettings.ModuleName, 156 MousePointerCrosshairsSettings.ModuleName, 157 MouseJumpSettings.ModuleName, 158 }; 159 160 // Process in-app conflicts 161 foreach (var conflict in allConflicts.InAppConflicts) 162 { 163 ProcessMouseUtilsConflictGroup(conflict, mouseUtilsModules, false); 164 } 165 166 // Process system conflicts 167 foreach (var conflict in allConflicts.SystemConflicts) 168 { 169 ProcessMouseUtilsConflictGroup(conflict, mouseUtilsModules, true); 170 } 171 } 172 else 173 { 174 if (allConflicts.InAppConflicts.Count > 0) 175 { 176 foreach (var conflictGroup in allConflicts.InAppConflicts) 177 { 178 foreach (var conflict in conflictGroup.Modules) 179 { 180 if (string.Equals(conflict.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase)) 181 { 182 var keyName = $"{conflict.ModuleName.ToLowerInvariant()}_{conflict.HotkeyID}"; 183 _hotkeyConflictStatus[keyName] = true; 184 _hotkeyConflictTooltips[keyName] = ResourceLoaderInstance.ResourceLoader.GetString("InAppHotkeyConflictTooltipText"); 185 } 186 } 187 } 188 } 189 190 if (allConflicts.SystemConflicts.Count > 0) 191 { 192 foreach (var conflictGroup in allConflicts.SystemConflicts) 193 { 194 foreach (var conflict in conflictGroup.Modules) 195 { 196 if (string.Equals(conflict.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase)) 197 { 198 var keyName = $"{conflict.ModuleName.ToLowerInvariant()}_{conflict.HotkeyID}"; 199 _hotkeyConflictStatus[keyName] = true; 200 _hotkeyConflictTooltips[keyName] = ResourceLoaderInstance.ResourceLoader.GetString("SysHotkeyConflictTooltipText"); 201 } 202 } 203 } 204 } 205 } 206 } 207 208 protected virtual bool GetHotkeyConflictStatus(string key) 209 { 210 return _hotkeyConflictStatus.ContainsKey(key) && _hotkeyConflictStatus[key]; 211 } 212 213 protected virtual string GetHotkeyConflictTooltip(string key) 214 { 215 return _hotkeyConflictTooltips.TryGetValue(key, out string value) ? value : null; 216 } 217 218 private bool IsModuleInvolved(HotkeyConflictGroupData conflict) 219 { 220 if (conflict.Modules == null) 221 { 222 return false; 223 } 224 225 return conflict.Modules.Any(module => 226 string.Equals(module.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase)); 227 } 228 229 public virtual void Dispose() 230 { 231 Dispose(true); 232 GC.SuppressFinalize(this); 233 } 234 235 protected virtual void Dispose(bool disposing) 236 { 237 if (!_disposed) 238 { 239 if (disposing) 240 { 241 if (GlobalHotkeyConflictManager.Instance != null) 242 { 243 GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated; 244 } 245 } 246 247 _disposed = true; 248 } 249 } 250 } 251 }