ShortcutConflictViewModel.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.Collections.ObjectModel; 8 using System.ComponentModel; 9 using System.Globalization; 10 using System.Linq; 11 using System.Reflection; 12 using System.Text.Json; 13 using System.Text.Json.Serialization; 14 using System.Text.Json.Serialization.Metadata; 15 using System.Windows.Threading; 16 using ManagedCommon; 17 using Microsoft.PowerToys.Settings.UI.Helpers; 18 using Microsoft.PowerToys.Settings.UI.Library; 19 using Microsoft.PowerToys.Settings.UI.Library.Helpers; 20 using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; 21 using Microsoft.PowerToys.Settings.UI.Library.Interfaces; 22 using Microsoft.PowerToys.Settings.UI.SerializationContext; 23 using Microsoft.PowerToys.Settings.UI.Services; 24 using Microsoft.Windows.ApplicationModel.Resources; 25 26 namespace Microsoft.PowerToys.Settings.UI.ViewModels 27 { 28 public class ShortcutConflictViewModel : PageViewModelBase 29 { 30 private readonly SettingsFactory _settingsFactory; 31 private readonly Func<string, int> _ipcMSGCallBackFunc; 32 private readonly Dispatcher _dispatcher; 33 34 private bool _disposed; 35 private AllHotkeyConflictsData _conflictsData = new(); 36 private ObservableCollection<HotkeyConflictGroupData> _conflictItems = new(); 37 private ResourceLoader resourceLoader; 38 39 public ShortcutConflictViewModel( 40 SettingsUtils settingsUtils, 41 ISettingsRepository<GeneralSettings> settingsRepository, 42 Func<string, int> ipcMSGCallBackFunc) 43 { 44 _dispatcher = Dispatcher.CurrentDispatcher; 45 _ipcMSGCallBackFunc = ipcMSGCallBackFunc ?? throw new ArgumentNullException(nameof(ipcMSGCallBackFunc)); 46 resourceLoader = ResourceLoaderInstance.ResourceLoader; 47 48 // Create SettingsFactory 49 _settingsFactory = new SettingsFactory(settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils))); 50 } 51 52 public AllHotkeyConflictsData ConflictsData 53 { 54 get => _conflictsData; 55 set 56 { 57 if (Set(ref _conflictsData, value)) 58 { 59 UpdateConflictItems(); 60 } 61 } 62 } 63 64 public ObservableCollection<HotkeyConflictGroupData> ConflictItems 65 { 66 get => _conflictItems; 67 private set => Set(ref _conflictItems, value); 68 } 69 70 protected override string ModuleName => "ShortcutConflictsWindow"; 71 72 /// <summary> 73 /// Ignore a specific HotkeySettings 74 /// </summary> 75 /// <param name="hotkeySettings">The HotkeySettings to ignore</param> 76 public void IgnoreShortcut(HotkeySettings hotkeySettings) 77 { 78 if (hotkeySettings == null) 79 { 80 return; 81 } 82 83 HotkeyConflictIgnoreHelper.AddToIgnoredList(hotkeySettings); 84 GlobalHotkeyConflictManager.Instance?.RequestAllConflicts(); 85 } 86 87 /// <summary> 88 /// Remove a HotkeySettings from the ignored list 89 /// </summary> 90 /// <param name="hotkeySettings">The HotkeySettings to unignore</param> 91 public void UnignoreShortcut(HotkeySettings hotkeySettings) 92 { 93 if (hotkeySettings == null) 94 { 95 return; 96 } 97 98 HotkeyConflictIgnoreHelper.RemoveFromIgnoredList(hotkeySettings); 99 GlobalHotkeyConflictManager.Instance?.RequestAllConflicts(); 100 } 101 102 private IHotkeyConfig GetModuleSettings(string moduleKey) 103 { 104 try 105 { 106 // MouseWithoutBorders and Peek settings may be changed by the logic in the utility as machines connect. 107 // We need to get a fresh version every time instead of using a repository. 108 if (string.Equals(moduleKey, MouseWithoutBordersSettings.ModuleName, StringComparison.OrdinalIgnoreCase) || 109 string.Equals(moduleKey, PeekSettings.ModuleName, StringComparison.OrdinalIgnoreCase)) 110 { 111 return _settingsFactory.GetFreshSettings(moduleKey); 112 } 113 114 // For other modules, get the settings from SettingsRepository 115 return _settingsFactory.GetSettings(moduleKey); 116 } 117 catch (Exception ex) 118 { 119 System.Diagnostics.Debug.WriteLine($"Error loading settings for {moduleKey}: {ex.Message}"); 120 return null; 121 } 122 } 123 124 protected override void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e) 125 { 126 _dispatcher.BeginInvoke(() => 127 { 128 ConflictsData = e.Conflicts ?? new AllHotkeyConflictsData(); 129 }); 130 } 131 132 private void UpdateConflictItems() 133 { 134 var items = new ObservableCollection<HotkeyConflictGroupData>(); 135 136 ProcessConflicts(ConflictsData?.InAppConflicts, false, items); 137 ProcessConflicts(ConflictsData?.SystemConflicts, true, items); 138 139 ConflictItems = items; 140 OnPropertyChanged(nameof(ConflictItems)); 141 } 142 143 private void ProcessConflicts(IEnumerable<HotkeyConflictGroupData> conflicts, bool isSystemConflict, ObservableCollection<HotkeyConflictGroupData> items) 144 { 145 if (conflicts == null) 146 { 147 return; 148 } 149 150 foreach (var conflict in conflicts) 151 { 152 HotkeySettings hotkey = new(conflict.Hotkey.Win, conflict.Hotkey.Ctrl, conflict.Hotkey.Alt, conflict.Hotkey.Shift, conflict.Hotkey.Key); 153 var isIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkey); 154 conflict.ConflictIgnored = isIgnored; 155 156 ProcessConflictGroup(conflict, isSystemConflict, isIgnored); 157 items.Add(conflict); 158 } 159 } 160 161 private void ProcessConflictGroup(HotkeyConflictGroupData conflict, bool isSystemConflict, bool isIgnored) 162 { 163 foreach (var module in conflict.Modules) 164 { 165 SetupModuleData(module, isSystemConflict, isIgnored); 166 } 167 } 168 169 private void SetupModuleData(ModuleHotkeyData module, bool isSystemConflict, bool isIgnored) 170 { 171 try 172 { 173 var settings = GetModuleSettings(module.ModuleName); 174 var allHotkeyAccessors = settings.GetAllHotkeyAccessors(); 175 var hotkeyAccessor = allHotkeyAccessors[module.HotkeyID]; 176 177 if (hotkeyAccessor != null) 178 { 179 // Get current hotkey settings (fresh from file) using the accessor's getter 180 module.HotkeySettings = hotkeyAccessor.Value; 181 module.HotkeySettings.ConflictDescription = isSystemConflict 182 ? ResourceLoaderInstance.ResourceLoader.GetString("SysHotkeyConflictTooltipText") 183 : ResourceLoaderInstance.ResourceLoader.GetString("InAppHotkeyConflictTooltipText"); 184 185 // Set header using localization key 186 module.Header = GetHotkeyLocalizationHeader(module.ModuleName, module.HotkeyID, hotkeyAccessor.LocalizationHeaderKey); 187 module.IsSystemConflict = isSystemConflict; 188 189 // Set module display info 190 var moduleType = settings.GetModuleType(); 191 module.ModuleType = moduleType; 192 var displayName = resourceLoader.GetString(ModuleHelper.GetModuleLabelResourceName(moduleType)); 193 module.DisplayName = displayName; 194 module.IconPath = ModuleHelper.GetModuleTypeFluentIconName(moduleType); 195 196 if (module.HotkeySettings != null) 197 { 198 SetConflictProperties(module.HotkeySettings, isSystemConflict); 199 } 200 201 module.PropertyChanged -= OnModuleHotkeyDataPropertyChanged; 202 module.PropertyChanged += OnModuleHotkeyDataPropertyChanged; 203 } 204 else 205 { 206 System.Diagnostics.Debug.WriteLine($"Could not find hotkey accessor for {module.ModuleName}.{module.HotkeyID}"); 207 } 208 } 209 catch (Exception ex) 210 { 211 System.Diagnostics.Debug.WriteLine($"Error setting up module data for {module.ModuleName}: {ex.Message}"); 212 } 213 } 214 215 private void SetConflictProperties(HotkeySettings settings, bool isSystemConflict) 216 { 217 settings.HasConflict = true; 218 settings.IsSystemConflict = isSystemConflict; 219 } 220 221 private void OnModuleHotkeyDataPropertyChanged(object sender, PropertyChangedEventArgs e) 222 { 223 if (sender is ModuleHotkeyData moduleData && e.PropertyName == nameof(ModuleHotkeyData.HotkeySettings)) 224 { 225 UpdateModuleHotkeySettings(moduleData.ModuleName, moduleData.HotkeyID, moduleData.HotkeySettings); 226 } 227 } 228 229 private void UpdateModuleHotkeySettings(string moduleName, int hotkeyID, HotkeySettings newHotkeySettings) 230 { 231 try 232 { 233 var settings = GetModuleSettings(moduleName); 234 var accessors = settings.GetAllHotkeyAccessors(); 235 236 var hotkeyAccessor = accessors[hotkeyID]; 237 238 // Use the accessor's setter to update the hotkey settings 239 hotkeyAccessor.Value = newHotkeySettings; 240 241 if (settings is ISettingsConfig settingsConfig) 242 { 243 // No need to save settings here, the runner will call module interface to save it 244 // SaveSettingsToFile(settings); 245 246 // For PowerToys Run, we should set the 'HotkeyChanged' property here to avoid issue #41468 247 if (string.Equals(moduleName, PowerLauncherSettings.ModuleName, StringComparison.OrdinalIgnoreCase)) 248 { 249 if (settings is PowerLauncherSettings powerLauncherSettings) 250 { 251 powerLauncherSettings.Properties.HotkeyChanged = true; 252 } 253 } 254 255 // Send IPC notification using the same format as other ViewModels 256 SendConfigMSG(settingsConfig, moduleName); 257 258 // Request updated conflicts after changing a hotkey 259 GlobalHotkeyConflictManager.Instance?.RequestAllConflicts(); 260 } 261 } 262 catch (Exception ex) 263 { 264 System.Diagnostics.Debug.WriteLine($"Error updating hotkey settings for {moduleName}.{hotkeyID}: {ex.Message}"); 265 } 266 } 267 268 /// <summary> 269 /// Sends IPC notification using the same format as other ViewModels 270 /// </summary> 271 private void SendConfigMSG(ISettingsConfig settingsConfig, string moduleName) 272 { 273 try 274 { 275 var jsonTypeInfo = GetJsonTypeInfo(settingsConfig.GetType()); 276 var serializedSettings = jsonTypeInfo != null 277 ? JsonSerializer.Serialize(settingsConfig, jsonTypeInfo) 278 : JsonSerializer.Serialize(settingsConfig); 279 280 string ipcMessage; 281 if (string.Equals(moduleName, "GeneralSettings", StringComparison.OrdinalIgnoreCase)) 282 { 283 ipcMessage = string.Format( 284 CultureInfo.InvariantCulture, 285 "{{ \"general\": {0} }}", 286 serializedSettings); 287 } 288 else 289 { 290 ipcMessage = string.Format( 291 CultureInfo.InvariantCulture, 292 "{{ \"powertoys\": {{ \"{0}\": {1} }} }}", 293 moduleName, 294 serializedSettings); 295 } 296 297 var result = _ipcMSGCallBackFunc(ipcMessage); 298 System.Diagnostics.Debug.WriteLine($"Sent IPC notification for {moduleName}, result: {result}"); 299 } 300 catch (Exception ex) 301 { 302 System.Diagnostics.Debug.WriteLine($"Error sending IPC notification for {moduleName}: {ex.Message}"); 303 } 304 } 305 306 private JsonTypeInfo GetJsonTypeInfo(Type settingsType) 307 { 308 try 309 { 310 var contextType = typeof(SourceGenerationContextContext); 311 var defaultProperty = contextType.GetProperty("Default", BindingFlags.Public | BindingFlags.Static); 312 var defaultContext = defaultProperty?.GetValue(null) as JsonSerializerContext; 313 314 if (defaultContext != null) 315 { 316 var typeInfoProperty = contextType.GetProperties(BindingFlags.Public | BindingFlags.Instance) 317 .FirstOrDefault(p => p.PropertyType.IsGenericType && 318 p.PropertyType.GetGenericTypeDefinition() == typeof(JsonTypeInfo<>) && 319 p.PropertyType.GetGenericArguments()[0] == settingsType); 320 321 return typeInfoProperty?.GetValue(defaultContext) as JsonTypeInfo; 322 } 323 } 324 catch (Exception ex) 325 { 326 System.Diagnostics.Debug.WriteLine($"Error getting JsonTypeInfo for {settingsType.Name}: {ex.Message}"); 327 } 328 329 return null; 330 } 331 332 private string GetHotkeyLocalizationHeader(string moduleName, int hotkeyID, string headerKey) 333 { 334 // Handle AdvancedPaste custom actions 335 if (string.Equals(moduleName, AdvancedPasteSettings.ModuleName, StringComparison.OrdinalIgnoreCase) 336 && hotkeyID > 9) 337 { 338 return headerKey; 339 } 340 341 try 342 { 343 return resourceLoader.GetString($"{headerKey}/Header"); 344 } 345 catch (Exception ex) 346 { 347 System.Diagnostics.Debug.WriteLine($"Error getting hotkey header for {moduleName}.{hotkeyID}: {ex.Message}"); 348 return headerKey; // Return the key itself as fallback 349 } 350 } 351 352 protected override void Dispose(bool disposing) 353 { 354 if (!_disposed) 355 { 356 if (disposing) 357 { 358 UnsubscribeFromEvents(); 359 } 360 361 _disposed = true; 362 } 363 364 base.Dispose(disposing); 365 } 366 367 private void UnsubscribeFromEvents() 368 { 369 try 370 { 371 if (ConflictItems != null) 372 { 373 foreach (var conflictGroup in ConflictItems) 374 { 375 if (conflictGroup?.Modules != null) 376 { 377 foreach (var module in conflictGroup.Modules) 378 { 379 if (module != null) 380 { 381 module.PropertyChanged -= OnModuleHotkeyDataPropertyChanged; 382 } 383 } 384 } 385 } 386 } 387 } 388 catch (Exception ex) 389 { 390 System.Diagnostics.Debug.WriteLine($"Error unsubscribing from events: {ex.Message}"); 391 } 392 } 393 } 394 }