/ src / settings-ui / Settings.UI.Library / KeysDataModel.cs
KeysDataModel.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.ComponentModel;
  8  using System.Diagnostics;
  9  using System.Globalization;
 10  using System.IO;
 11  using System.Linq;
 12  using System.Management;
 13  using System.Text.Json;
 14  using System.Text.Json.Serialization;
 15  using System.Threading;
 16  using System.Windows.Input;
 17  
 18  using ManagedCommon;
 19  using Microsoft.PowerToys.Settings.UI.Library.Utilities;
 20  using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
 21  
 22  namespace Microsoft.PowerToys.Settings.UI.Library
 23  {
 24      public class KeysDataModel : INotifyPropertyChanged
 25      {
 26          [JsonPropertyName("originalKeys")]
 27          public string OriginalKeys { get; set; }
 28  
 29          [JsonPropertyName("secondKeyOfChord")]
 30          public uint SecondKeyOfChord { get; set; }
 31  
 32          [JsonPropertyName("newRemapKeys")]
 33          public string NewRemapKeys { get; set; }
 34  
 35          [JsonPropertyName("unicodeText")]
 36          public string NewRemapString { get; set; }
 37  
 38          [JsonPropertyName("runProgramFilePath")]
 39          public string RunProgramFilePath { get; set; }
 40  
 41          [JsonPropertyName("runProgramArgs")]
 42          public string RunProgramArgs { get; set; }
 43  
 44          [JsonPropertyName("openUri")]
 45          public string OpenUri { get; set; }
 46  
 47          [JsonPropertyName("operationType")]
 48          public int OperationType { get; set; }
 49  
 50          private enum KeyboardManagerEditorType
 51          {
 52              KeyEditor = 0,
 53              ShortcutEditor,
 54          }
 55  
 56          public const string CommaSeparator = "<comma>";
 57  
 58          private static Process editor;
 59          private ICommand _editShortcutItemCommand;
 60  
 61          public ICommand EditShortcutItem => _editShortcutItemCommand ?? (_editShortcutItemCommand = new RelayCommand<object>(OnEditShortcutItem));
 62  
 63          public event PropertyChangedEventHandler PropertyChanged;
 64  
 65          protected virtual void OnPropertyChanged(string propertyName)
 66          {
 67              PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
 68          }
 69  
 70          public void OnEditShortcutItem(object parameter)
 71          {
 72              OpenEditor((int)KeyboardManagerEditorType.ShortcutEditor);
 73          }
 74  
 75          private async void OpenEditor(int type)
 76          {
 77              if (editor != null)
 78              {
 79                  BringProcessToFront(editor);
 80                  return;
 81              }
 82  
 83              const string PowerToyName = KeyboardManagerSettings.ModuleName;
 84              const string KeyboardManagerEditorPath = "KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe";
 85              try
 86              {
 87                  if (editor != null && editor.HasExited)
 88                  {
 89                      Logger.LogInfo($"Previous instance of {PowerToyName} editor exited");
 90                      editor = null;
 91                  }
 92  
 93                  if (editor != null)
 94                  {
 95                      Logger.LogInfo($"The {PowerToyName} editor instance {editor.Id} exists. Bringing the process to the front");
 96                      BringProcessToFront(editor);
 97                      return;
 98                  }
 99  
100                  string path = Path.Combine(Environment.CurrentDirectory, KeyboardManagerEditorPath);
101                  Logger.LogInfo($"Starting {PowerToyName} editor from {path}");
102  
103                  // InvariantCulture: type represents the KeyboardManagerEditorType enum value
104                  editor = Process.Start(path, $"{type.ToString(CultureInfo.InvariantCulture)} {Environment.ProcessId}");
105  
106                  await editor.WaitForExitAsync();
107  
108                  editor = null;
109              }
110              catch (Exception e)
111              {
112                  editor = null;
113                  Logger.LogError($"Exception encountered when opening an {PowerToyName} editor", e);
114              }
115          }
116  
117          private static void BringProcessToFront(Process process)
118          {
119              if (process == null)
120              {
121                  return;
122              }
123  
124              IntPtr handle = process.MainWindowHandle;
125              if (NativeMethods.IsIconic(handle))
126              {
127                  NativeMethods.ShowWindow(handle, NativeMethods.SWRESTORE);
128              }
129  
130              NativeMethods.SetForegroundWindow(handle);
131          }
132  
133          private static List<string> MapKeysOnlyChord(uint secondKeyOfChord)
134          {
135              var result = new List<string>();
136              if (secondKeyOfChord <= 0)
137              {
138                  return result;
139              }
140  
141              result.Add(Helper.GetKeyName(secondKeyOfChord));
142  
143              return result;
144          }
145  
146          private static List<string> MapKeys(string stringOfKeys, uint secondKeyOfChord, bool splitChordsWithComma = false)
147          {
148              if (stringOfKeys == null)
149              {
150                  return new List<string>();
151              }
152  
153              if (secondKeyOfChord > 0)
154              {
155                  var keys = stringOfKeys.Split(';');
156                  return keys.Take(keys.Length - 1)
157                      .Select(uint.Parse)
158                      .Select(Helper.GetKeyName)
159                      .ToList();
160              }
161              else
162              {
163                  if (splitChordsWithComma)
164                  {
165                      var keys = stringOfKeys.Split(';')
166                          .Select(uint.Parse)
167                          .Select(Helper.GetKeyName)
168                          .ToList();
169                      keys.Insert(keys.Count - 1, CommaSeparator);
170                      return keys;
171                  }
172                  else
173                  {
174                      return stringOfKeys
175                      .Split(';')
176                      .Select(uint.Parse)
177                      .Select(Helper.GetKeyName)
178                      .ToList();
179                  }
180              }
181          }
182  
183          private static List<string> MapKeys(string stringOfKeys)
184          {
185              return MapKeys(stringOfKeys, 0);
186          }
187  
188          public List<string> GetMappedOriginalKeys(bool ignoreSecondKeyInChord, bool splitChordsWithComma = false)
189          {
190              if (ignoreSecondKeyInChord && SecondKeyOfChord > 0)
191              {
192                  return MapKeys(OriginalKeys, SecondKeyOfChord);
193              }
194              else
195              {
196                  return MapKeys(OriginalKeys, 0, splitChordsWithComma);
197              }
198          }
199  
200          public List<string> GetMappedOriginalKeysOnlyChord()
201          {
202              return MapKeysOnlyChord(SecondKeyOfChord);
203          }
204  
205          public List<string> GetMappedOriginalKeys()
206          {
207              return GetMappedOriginalKeys(false);
208          }
209  
210          public List<string> GetMappedOriginalKeysWithSplitChord()
211          {
212              return GetMappedOriginalKeys(false, true);
213          }
214  
215          public bool IsRunProgram
216          {
217              get
218              {
219                  return OperationType == 1;
220              }
221          }
222  
223          public bool IsOpenURI
224          {
225              get
226              {
227                  return OperationType == 2;
228              }
229          }
230  
231          public bool IsOpenUriOrIsRunProgram
232          {
233              get
234              {
235                  return IsOpenURI || IsRunProgram;
236              }
237          }
238  
239          public bool HasChord
240          {
241              get
242              {
243                  return SecondKeyOfChord > 0;
244              }
245          }
246  
247          public List<string> GetMappedNewRemapKeys(int runProgramMaxLength)
248          {
249              if (IsRunProgram)
250              {
251                  // we're going to just pretend this is a "key" if we have a RunProgramFilePath
252                  if (string.IsNullOrEmpty(RunProgramFilePath))
253                  {
254                      return new List<string>();
255                  }
256                  else
257                  {
258                      return new List<string> { FormatFakeKeyForDisplay(runProgramMaxLength) };
259                  }
260              }
261              else if (IsOpenURI)
262              {
263                  // we're going to just pretend this is a "key" if we have a RunProgramFilePath
264                  if (string.IsNullOrEmpty(OpenUri))
265                  {
266                      return new List<string>();
267                  }
268                  else
269                  {
270                      if (OpenUri.Length > runProgramMaxLength)
271                      {
272                          return new List<string> { $"{OpenUri.Substring(0, runProgramMaxLength - 3)}..." };
273                      }
274                      else
275                      {
276                          return new List<string> { OpenUri };
277                      }
278                  }
279              }
280  
281              return (string.IsNullOrEmpty(NewRemapString) || NewRemapString == "*Unsupported*") ? MapKeys(NewRemapKeys) : new List<string> { NewRemapString };
282          }
283  
284          // Instead of doing something fancy pants, we'll just display the RunProgramFilePath data when it's IsRunProgram
285          // It truncates the start of the program to run, if it's long and truncates the end of the args if it's long
286          // e.g.: c:\MyCool\PathIs\Long\software.exe myArg1 myArg2 myArg3 -> (something like) "...ng\software.exe myArg1..."
287          // the idea is you get the most important part of the program to run and some of the args in case that the only thing thats different,
288          // e.g: "...path\software.exe cool1.txt" and "...path\software.exe cool3.txt"
289          private string FormatFakeKeyForDisplay(int runProgramMaxLength)
290          {
291              // was going to use this:
292              var fakeKey = Environment.ExpandEnvironmentVariables(RunProgramFilePath);
293              try
294              {
295                  if (File.Exists(fakeKey))
296                  {
297                      fakeKey = Path.GetFileName(Environment.ExpandEnvironmentVariables(RunProgramFilePath));
298                  }
299              }
300              catch
301              {
302              }
303  
304              fakeKey = $"{fakeKey} {RunProgramArgs}".Trim();
305  
306              if (fakeKey.Length > runProgramMaxLength)
307              {
308                  fakeKey = $"{fakeKey.Substring(0, runProgramMaxLength - 3)}...";
309              }
310  
311              return fakeKey;
312          }
313  
314          public string ToJsonString()
315          {
316              return JsonSerializer.Serialize(this);
317          }
318      }
319  }