/ src / settings-ui / Settings.UI / ViewModels / PageViewModelBase.cs
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  }