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 }