// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Utilities; using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands; namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels { public class KeyboardManagerViewModel : Observable { private GeneralSettings GeneralSettingsConfig { get; set; } private readonly ISettingsUtils _settingsUtils; private const string PowerToyName = KeyboardManagerSettings.ModuleName; private const string JsonFileType = ".json"; private const string KeyboardManagerEditorPath = "modules\\KeyboardManager\\KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe"; private Process editor; private enum KeyboardManagerEditorType { KeyEditor = 0, ShortcutEditor, } public KeyboardManagerSettings Settings { get; set; } private ICommand _remapKeyboardCommand; private ICommand _editShortcutCommand; private KeyboardManagerProfile _profile; private Func SendConfigMSG { get; } private Func, int> FilterRemapKeysList { get; } [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exceptions should not crash the program but will be logged until we can understand common exception scenarios")] public KeyboardManagerViewModel(ISettingsUtils settingsUtils, ISettingsRepository settingsRepository, Func ipcMSGCallBackFunc, Func, int> filterRemapKeysList) { if (settingsRepository == null) { throw new ArgumentNullException(nameof(settingsRepository)); } GeneralSettingsConfig = settingsRepository.SettingsConfig; // set the callback functions value to hangle outgoing IPC message. SendConfigMSG = ipcMSGCallBackFunc; FilterRemapKeysList = filterRemapKeysList; _settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils)); if (_settingsUtils.SettingsExists(PowerToyName)) { try { Settings = _settingsUtils.GetSettingsOrDefault(PowerToyName); } catch (Exception e) { Logger.LogError($"Exception encountered while reading {PowerToyName} settings.", e); #if DEBUG if (e is ArgumentException || e is ArgumentNullException || e is PathTooLongException) { throw; } #endif } // Load profile. if (!LoadProfile()) { _profile = new KeyboardManagerProfile(); } } else { Settings = new KeyboardManagerSettings(); _settingsUtils.SaveSettings(Settings.ToJsonString(), PowerToyName); } } public bool Enabled { get { return GeneralSettingsConfig.Enabled.KeyboardManager; } set { if (GeneralSettingsConfig.Enabled.KeyboardManager != value) { GeneralSettingsConfig.Enabled.KeyboardManager = value; OnPropertyChanged(nameof(Enabled)); if (!Enabled && editor != null) { editor.CloseMainWindow(); } OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); SendConfigMSG(outgoing.ToString()); } } } // store remappings public List RemapKeys { get { if (_profile != null) { return _profile.RemapKeys.InProcessRemapKeys; } else { return new List(); } } } public static List CombineShortcutLists(List globalShortcutList, List appSpecificShortcutList) { if (globalShortcutList == null && appSpecificShortcutList == null) { return new List(); } else if (globalShortcutList == null) { return appSpecificShortcutList; } else if (appSpecificShortcutList == null) { return globalShortcutList.ConvertAll(x => new AppSpecificKeysDataModel { OriginalKeys = x.OriginalKeys, NewRemapKeys = x.NewRemapKeys, TargetApp = "All Apps" }).ToList(); } else { return globalShortcutList.ConvertAll(x => new AppSpecificKeysDataModel { OriginalKeys = x.OriginalKeys, NewRemapKeys = x.NewRemapKeys, TargetApp = "All Apps" }).Concat(appSpecificShortcutList).ToList(); } } public List RemapShortcuts { get { if (_profile != null) { return CombineShortcutLists(_profile.RemapShortcuts.GlobalRemapShortcuts, _profile.RemapShortcuts.AppSpecificRemapShortcuts); } else { return new List(); } } } public ICommand RemapKeyboardCommand => _remapKeyboardCommand ?? (_remapKeyboardCommand = new RelayCommand(OnRemapKeyboard)); public ICommand EditShortcutCommand => _editShortcutCommand ?? (_editShortcutCommand = new RelayCommand(OnEditShortcut)); private void OnRemapKeyboard() { OpenEditor((int)KeyboardManagerEditorType.KeyEditor); } private void OnEditShortcut() { OpenEditor((int)KeyboardManagerEditorType.ShortcutEditor); } private static void BringProcessToFront(Process process) { if (process == null) { return; } IntPtr handle = process.MainWindowHandle; if (IsIconic(handle)) { ShowWindow(handle, SWRESTORE); } SetForegroundWindow(handle); } private const int SWRESTORE = 9; [System.Runtime.InteropServices.DllImport("User32.dll")] private static extern bool SetForegroundWindow(IntPtr handle); [System.Runtime.InteropServices.DllImport("User32.dll")] private static extern bool ShowWindow(IntPtr handle, int nCmdShow); [System.Runtime.InteropServices.DllImport("User32.dll")] private static extern bool IsIconic(IntPtr handle); [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exceptions here (especially mutex errors) should not halt app execution, but they will be logged.")] private void OpenEditor(int type) { try { if (editor != null && editor.HasExited) { Logger.LogInfo($"Previous instance of {PowerToyName} editor exited"); editor = null; } if (editor != null) { Logger.LogInfo($"The {PowerToyName} editor instance {editor.Id} exists. Bringing the process to the front"); BringProcessToFront(editor); return; } string path = Path.Combine(Environment.CurrentDirectory, KeyboardManagerEditorPath); Logger.LogInfo($"Starting {PowerToyName} editor from {path}"); // InvariantCulture: type represents the KeyboardManagerEditorType enum value editor = Process.Start(path, $"{type.ToString(CultureInfo.InvariantCulture)} {Process.GetCurrentProcess().Id}"); } catch (Exception e) { Logger.LogError($"Exception encountered when opening an {PowerToyName} editor", e); } } public void NotifyFileChanged() { OnPropertyChanged(nameof(RemapKeys)); OnPropertyChanged(nameof(RemapShortcuts)); } [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exceptions here (especially mutex errors) should not halt app execution, but they will be logged.")] public bool LoadProfile() { // The KBM process out of runner creates the default.json file if it does not exist. var success = true; var readSuccessfully = false; string fileName = Settings.Properties.ActiveConfiguration.Value + JsonFileType; try { // retry loop for reading CancellationTokenSource ts = new CancellationTokenSource(); Task t = Task.Run(() => { while (!readSuccessfully && !ts.IsCancellationRequested) { if (_settingsUtils.SettingsExists(PowerToyName, fileName)) { try { _profile = _settingsUtils.GetSettingsOrDefault(PowerToyName, fileName); readSuccessfully = true; } catch (Exception e) { Logger.LogError($"Exception encountered when reading {PowerToyName} settings", e); } } if (!readSuccessfully) { Task.Delay(500).Wait(); } } }); t.Wait(1000, ts.Token); ts.Cancel(); ts.Dispose(); if (!readSuccessfully) { success = false; } FilterRemapKeysList(_profile?.RemapKeys?.InProcessRemapKeys); } catch (Exception e) { // Failed to load the configuration. Logger.LogError($"Exception encountered when loading {PowerToyName} profile", e); success = false; } return success; } } }