/ src / settings-ui / Settings.UI / ViewModels / KeyboardManagerViewModel.cs
KeyboardManagerViewModel.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.Globalization;
  9  using System.IO;
 10  using System.Linq;
 11  using System.Threading;
 12  using System.Threading.Tasks;
 13  using System.Windows.Input;
 14  
 15  using global::PowerToys.GPOWrapper;
 16  using ManagedCommon;
 17  using Microsoft.PowerToys.Settings.UI.Library;
 18  using Microsoft.PowerToys.Settings.UI.Library.Helpers;
 19  using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
 20  using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
 21  using Microsoft.PowerToys.Settings.Utilities;
 22  using Microsoft.Win32;
 23  
 24  namespace Microsoft.PowerToys.Settings.UI.ViewModels
 25  {
 26      public partial class KeyboardManagerViewModel : Observable
 27      {
 28          private GeneralSettings GeneralSettingsConfig { get; set; }
 29  
 30          private readonly SettingsUtils _settingsUtils;
 31  
 32          private const string PowerToyName = KeyboardManagerSettings.ModuleName;
 33          private const string JsonFileType = ".json";
 34  
 35          // Default editor path. Can be removed once the new WinUI3 editor is released.
 36          private const string KeyboardManagerEditorPath = "KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe";
 37  
 38          // New WinUI3 editor path. Still in development and do NOT use it in production.
 39          private const string KeyboardManagerEditorUIPath = "KeyboardManagerEditorUI\\PowerToys.KeyboardManagerEditorUI.exe";
 40  
 41          private Process editor;
 42  
 43          private enum KeyboardManagerEditorType
 44          {
 45              KeyEditor = 0,
 46              ShortcutEditor,
 47          }
 48  
 49          private GpoRuleConfigured _enabledGpoRuleConfiguration;
 50          private bool _enabledStateIsGPOConfigured;
 51          private bool _isEnabled;
 52  
 53          public KeyboardManagerSettings Settings { get; set; }
 54  
 55          private ICommand _remapKeyboardCommand;
 56          private ICommand _editShortcutCommand;
 57          private KeyboardManagerProfile _profile;
 58  
 59          private Func<string, int> SendConfigMSG { get; }
 60  
 61          private Func<List<KeysDataModel>, int> FilterRemapKeysList { get; }
 62  
 63          public KeyboardManagerViewModel(SettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, Func<string, int> ipcMSGCallBackFunc, Func<List<KeysDataModel>, int> filterRemapKeysList)
 64          {
 65              ArgumentNullException.ThrowIfNull(settingsRepository);
 66  
 67              GeneralSettingsConfig = settingsRepository.SettingsConfig;
 68  
 69              InitializeEnabledValue();
 70  
 71              // set the callback functions value to handle outgoing IPC message.
 72              SendConfigMSG = ipcMSGCallBackFunc;
 73              FilterRemapKeysList = filterRemapKeysList;
 74  
 75              _settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
 76  
 77              if (_settingsUtils.SettingsExists(PowerToyName))
 78              {
 79                  try
 80                  {
 81                      Settings = _settingsUtils.GetSettingsOrDefault<KeyboardManagerSettings>(PowerToyName);
 82                  }
 83                  catch (Exception e)
 84                  {
 85                      Logger.LogError($"Exception encountered while reading {PowerToyName} settings.", e);
 86  #if DEBUG
 87                      if (e is ArgumentException || e is ArgumentNullException || e is PathTooLongException)
 88                      {
 89                          throw;
 90                      }
 91  #endif
 92                  }
 93  
 94                  // Load profile.
 95                  if (!LoadProfile())
 96                  {
 97                      _profile = new KeyboardManagerProfile();
 98                  }
 99              }
100              else
101              {
102                  Settings = new KeyboardManagerSettings();
103                  _settingsUtils.SaveSettings(Settings.ToJsonString(), PowerToyName);
104              }
105          }
106  
107          private void InitializeEnabledValue()
108          {
109              _enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredKeyboardManagerEnabledValue();
110              if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
111              {
112                  // Get the enabled state from GPO.
113                  _enabledStateIsGPOConfigured = true;
114                  _isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
115              }
116              else
117              {
118                  _isEnabled = GeneralSettingsConfig.Enabled.KeyboardManager;
119              }
120          }
121  
122          public bool Enabled
123          {
124              get
125              {
126                  return _isEnabled;
127              }
128  
129              set
130              {
131                  if (_enabledStateIsGPOConfigured)
132                  {
133                      // If it's GPO configured, shouldn't be able to change this state.
134                      return;
135                  }
136  
137                  if (_isEnabled != value)
138                  {
139                      _isEnabled = value;
140  
141                      GeneralSettingsConfig.Enabled.KeyboardManager = value;
142                      OnPropertyChanged(nameof(Enabled));
143  
144                      if (!Enabled && editor != null)
145                      {
146                          editor.CloseMainWindow();
147                      }
148  
149                      OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
150  
151                      SendConfigMSG(outgoing.ToString());
152                  }
153              }
154          }
155  
156          public bool IsEnabledGpoConfigured
157          {
158              get => _enabledStateIsGPOConfigured;
159          }
160  
161          // store remappings
162          public List<KeysDataModel> RemapKeys
163          {
164              get
165              {
166                  if (_profile != null)
167                  {
168                      return _profile.RemapKeys.InProcessRemapKeys.Concat(_profile.RemapKeysToText.InProcessRemapKeys).ToList();
169                  }
170                  else
171                  {
172                      return new List<KeysDataModel>();
173                  }
174              }
175          }
176  
177          public static List<AppSpecificKeysDataModel> CombineShortcutLists(List<KeysDataModel> globalShortcutList, List<AppSpecificKeysDataModel> appSpecificShortcutList)
178          {
179              string allAppsDescription = "All Apps";
180              try
181              {
182                  var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
183                  allAppsDescription = resourceLoader.GetString("KeyboardManager_All_Apps_Description");
184              }
185              catch (Exception ex)
186              {
187                  Logger.LogError("Couldn't get translation for All Apps mention in KBM page.", ex);
188              }
189  
190              if (globalShortcutList == null && appSpecificShortcutList == null)
191              {
192                  return new List<AppSpecificKeysDataModel>();
193              }
194              else if (globalShortcutList == null)
195              {
196                  return appSpecificShortcutList;
197              }
198              else if (appSpecificShortcutList == null)
199              {
200                  return globalShortcutList.ConvertAll(x => new AppSpecificKeysDataModel { OriginalKeys = x.OriginalKeys, NewRemapKeys = x.NewRemapKeys, NewRemapString = x.NewRemapString, RunProgramFilePath = x.RunProgramFilePath, OperationType = x.OperationType, OpenUri = x.OpenUri, SecondKeyOfChord = x.SecondKeyOfChord, RunProgramArgs = x.RunProgramArgs, TargetApp = allAppsDescription }).ToList();
201              }
202              else
203              {
204                  return globalShortcutList.ConvertAll(x => new AppSpecificKeysDataModel { OriginalKeys = x.OriginalKeys, NewRemapKeys = x.NewRemapKeys, NewRemapString = x.NewRemapString, RunProgramFilePath = x.RunProgramFilePath, OperationType = x.OperationType, OpenUri = x.OpenUri, SecondKeyOfChord = x.SecondKeyOfChord, RunProgramArgs = x.RunProgramArgs, TargetApp = allAppsDescription }).Concat(appSpecificShortcutList).ToList();
205              }
206          }
207  
208          public List<AppSpecificKeysDataModel> RemapShortcuts
209          {
210              get
211              {
212                  if (_profile != null)
213                  {
214                      return CombineShortcutLists(_profile.RemapShortcuts.GlobalRemapShortcuts, _profile.RemapShortcuts.AppSpecificRemapShortcuts).Concat(CombineShortcutLists(_profile.RemapShortcutsToText.GlobalRemapShortcuts, _profile.RemapShortcutsToText.AppSpecificRemapShortcuts)).ToList();
215                  }
216                  else
217                  {
218                      return new List<AppSpecificKeysDataModel>();
219                  }
220              }
221          }
222  
223          public ICommand RemapKeyboardCommand => _remapKeyboardCommand ?? (_remapKeyboardCommand = new RelayCommand(OnRemapKeyboard));
224  
225          public ICommand EditShortcutCommand => _editShortcutCommand ?? (_editShortcutCommand = new RelayCommand(OnEditShortcut));
226  
227          public void OnRemapKeyboard()
228          {
229              OpenEditor((int)KeyboardManagerEditorType.KeyEditor);
230          }
231  
232          public void OnEditShortcut()
233          {
234              OpenEditor((int)KeyboardManagerEditorType.ShortcutEditor);
235          }
236  
237          private static void BringProcessToFront(Process process)
238          {
239              if (process == null)
240              {
241                  return;
242              }
243  
244              IntPtr handle = process.MainWindowHandle;
245              if (NativeMethods.IsIconic(handle))
246              {
247                  NativeMethods.ShowWindow(handle, NativeMethods.SWRESTORE);
248              }
249  
250              NativeMethods.SetForegroundWindow(handle);
251          }
252  
253          private void OpenEditor(int type)
254          {
255              try
256              {
257                  if (editor != null && editor.HasExited)
258                  {
259                      Logger.LogInfo($"Previous instance of {PowerToyName} editor exited");
260                      editor = null;
261                  }
262  
263                  if (editor != null)
264                  {
265                      Logger.LogInfo($"The {PowerToyName} editor instance {editor.Id} exists. Bringing the process to the front");
266                      BringProcessToFront(editor);
267                      return;
268                  }
269  
270                  // Launch the new editor if:
271                  // 1. the experimentation toggle is enabled in the settings
272                  // 2. the new WinUI3 editor is enabled in the registry. The registry value does not exist by default and is only used for development purposes
273                  string editorPath = KeyboardManagerEditorPath;
274                  try
275                  {
276                      // Check if the experimentation toggle is enabled in the settings
277                      var settingsUtils = SettingsUtils.Default;
278                      bool isExperimentationEnabled = SettingsRepository<GeneralSettings>.GetInstance(settingsUtils).SettingsConfig.EnableExperimentation;
279  
280                      // Only read the registry value if the experimentation toggle is enabled
281                      if (isExperimentationEnabled)
282                      {
283                          // Read the registry value to determine which editor to launch
284                          var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\PowerToys\Keyboard Manager");
285                          if (key != null && (int?)key.GetValue("UseNewEditor") == 1)
286                          {
287                              editorPath = KeyboardManagerEditorUIPath;
288                          }
289  
290                          // Close the registry key
291                          key?.Close();
292                      }
293                  }
294                  catch (Exception e)
295                  {
296                      // Fall back to the default editor path if any exception occurs
297                      Logger.LogError("Failed to launch the new WinUI3 Editor", e);
298                  }
299  
300                  string path = Path.Combine(Environment.CurrentDirectory, editorPath);
301                  Logger.LogInfo($"Starting {PowerToyName} editor from {path}");
302  
303                  // InvariantCulture: type represents the KeyboardManagerEditorType enum value
304                  editor = Process.Start(path, $"{type.ToString(CultureInfo.InvariantCulture)} {Environment.ProcessId}");
305              }
306              catch (Exception e)
307              {
308                  Logger.LogError($"Exception encountered when opening an {PowerToyName} editor", e);
309              }
310          }
311  
312          public void NotifyFileChanged()
313          {
314              OnPropertyChanged(nameof(RemapKeys));
315              OnPropertyChanged(nameof(RemapShortcuts));
316          }
317  
318          public void RefreshEnabledState()
319          {
320              InitializeEnabledValue();
321              OnPropertyChanged(nameof(Enabled));
322          }
323  
324          public bool LoadProfile()
325          {
326              var success = true;
327              var readSuccessfully = false;
328  
329              // The KBM process out of runner doesn't create the default.json file if it does not exist.
330              string fileName = Settings.Properties.ActiveConfiguration.Value + JsonFileType;
331              var profileExists = false;
332  
333              try
334              {
335                  // retry loop for reading
336                  CancellationTokenSource ts = new CancellationTokenSource();
337                  Task t = Task.Run(() =>
338                  {
339                      while (!readSuccessfully && !ts.IsCancellationRequested)
340                      {
341                          profileExists = _settingsUtils.SettingsExists(PowerToyName, fileName);
342                          if (!profileExists)
343                          {
344                              break;
345                          }
346                          else
347                          {
348                              try
349                              {
350                                  _profile = _settingsUtils.GetSettingsOrDefault<KeyboardManagerProfile>(PowerToyName, fileName);
351                                  readSuccessfully = true;
352                              }
353                              catch (Exception e)
354                              {
355                                  Logger.LogError($"Exception encountered when reading {PowerToyName} settings", e);
356                              }
357                          }
358  
359                          if (!readSuccessfully)
360                          {
361                              Task.Delay(500, ts.Token).Wait(ts.Token);
362                          }
363                      }
364                  });
365  
366                  var completedInTime = t.Wait(3000, ts.Token);
367                  ts.Cancel();
368                  ts.Dispose();
369  
370                  if (readSuccessfully)
371                  {
372                      FilterRemapKeysList(_profile?.RemapKeys?.InProcessRemapKeys);
373                      FilterRemapKeysList(_profile?.RemapKeysToText?.InProcessRemapKeys);
374                  }
375                  else
376                  {
377                      success = false;
378                  }
379  
380                  if (!completedInTime)
381                  {
382                      Logger.LogError($"Timeout encountered when loading {PowerToyName} profile");
383                  }
384              }
385              catch (Exception e)
386              {
387                  // Failed to load the configuration.
388                  Logger.LogError($"Exception encountered when loading {PowerToyName} profile", e);
389                  success = false;
390              }
391  
392              if (!profileExists)
393              {
394                  Logger.LogInfo($"Couldn't load {PowerToyName} profile because it doesn't exist");
395              }
396              else if (!success)
397              {
398                  Logger.LogError($"Couldn't load {PowerToyName} profile");
399              }
400  
401              return success;
402          }
403      }
404  }