PowerToys/src/core/Microsoft.PowerToys.Settings.UI/ViewModels/KeyboardManagerViewModel.cs
Arjun Balgovind bb2049411b
[Keyboard Manager] Add JSON support for App Specific shortcuts (#4840)
* Enable app specific shortcut remapping

* Fixed lowercase function call

* Add test file

* Moved GetForegroundProcess to II and added tests

* Fixed runtime error while testing due to heap allocation across dll boundary

* Renamed function

* Changed shortcutBuffer type

* Linked App specific UI to backend

* Added shortcut validation logic on TextBox LostFocus handler

* Moved Validate function and changed default text

* Changed to case insensitive warning check

* Changed to case insensitive warning check at OnClickAccept

* Fixed alignment and spacing issues

* Added app-specific JSON support in backend

* Updated landing page

* Make listview horizontally scrollable

* Added tests

* Consider all case variants of All Apps in textbox to be global shortcuts
2020-07-10 17:07:28 -07:00

243 lines
9.5 KiB
C#

// 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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Lib;
using Microsoft.PowerToys.Settings.UI.Lib.Utilities;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.Toolkit.Uwp.Helpers;
using Windows.System;
using Windows.UI.Core;
using Windows.UI.Xaml;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public class KeyboardManagerViewModel : Observable
{
private const string PowerToyName = "Keyboard Manager";
private const string RemapKeyboardActionName = "RemapKeyboard";
private const string RemapKeyboardActionValue = "Open Remap Keyboard Window";
private const string EditShortcutActionName = "EditShortcut";
private const string EditShortcutActionValue = "Open Edit Shortcut Window";
private const string JsonFileType = ".json";
private const string ProfileFileMutexName = "PowerToys.KeyboardManager.ConfigMutex";
private const int ProfileFileMutexWaitTimeoutMilliseconds = 1000;
private readonly CoreDispatcher dispatcher;
private readonly FileSystemWatcher watcher;
private ICommand remapKeyboardCommand;
private ICommand editShortcutCommand;
private KeyboardManagerSettings settings;
private KeyboardManagerProfile profile;
private GeneralSettings generalSettings;
public KeyboardManagerViewModel()
{
dispatcher = Window.Current.Dispatcher;
if (SettingsUtils.SettingsExists(PowerToyName))
{
// Todo: Be more resilient while reading and saving settings.
settings = SettingsUtils.GetSettings<KeyboardManagerSettings>(PowerToyName);
// Load profile.
if (!LoadProfile())
{
profile = new KeyboardManagerProfile();
}
}
else
{
settings = new KeyboardManagerSettings(PowerToyName);
SettingsUtils.SaveSettings(settings.ToJsonString(), PowerToyName);
}
if (SettingsUtils.SettingsExists())
{
generalSettings = SettingsUtils.GetSettings<GeneralSettings>(string.Empty);
}
else
{
generalSettings = new GeneralSettings();
SettingsUtils.SaveSettings(generalSettings.ToJsonString(), string.Empty);
}
watcher = Helper.GetFileWatcher(
PowerToyName,
settings.Properties.ActiveConfiguration.Value + JsonFileType,
OnConfigFileUpdate);
}
public bool Enabled
{
get
{
return generalSettings.Enabled.KeyboardManager;
}
set
{
if (generalSettings.Enabled.KeyboardManager != value)
{
generalSettings.Enabled.KeyboardManager = value;
OnPropertyChanged(nameof(Enabled));
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(generalSettings);
ShellPage.DefaultSndMSGCallback(outgoing.ToString());
}
}
}
// store remappings
public List<KeysDataModel> RemapKeys
{
get
{
if (profile != null)
{
return profile.RemapKeys.InProcessRemapKeys;
}
else
{
return new List<KeysDataModel>();
}
}
}
public static List<AppSpecificKeysDataModel> CombineShortcutLists(List<KeysDataModel> globalShortcutList, List<AppSpecificKeysDataModel> appSpecificShortcutList)
{
return globalShortcutList.ConvertAll(x => new AppSpecificKeysDataModel { OriginalKeys = x.OriginalKeys, NewRemapKeys = x.NewRemapKeys, TargetApp = "All Apps" }).Concat(appSpecificShortcutList).ToList();
}
public List<AppSpecificKeysDataModel> RemapShortcuts
{
get
{
if (profile != null)
{
return CombineShortcutLists(profile.RemapShortcuts.GlobalRemapShortcuts, profile.RemapShortcuts.AppSpecificRemapShortcuts);
}
else
{
return new List<AppSpecificKeysDataModel>();
}
}
}
public ICommand RemapKeyboardCommand => remapKeyboardCommand ?? (remapKeyboardCommand = new RelayCommand(OnRemapKeyboard));
public ICommand EditShortcutCommand => editShortcutCommand ?? (editShortcutCommand = new RelayCommand(OnEditShortcut));
private async void OnRemapKeyboard()
{
await Task.Run(() => OnRemapKeyboardBackground());
}
private async void OnEditShortcut()
{
await Task.Run(() => OnEditShortcutBackground());
}
private async Task OnRemapKeyboardBackground()
{
Helper.AllowRunnerToForeground();
ShellPage.DefaultSndMSGCallback(Helper.GetSerializedCustomAction(PowerToyName, RemapKeyboardActionName, RemapKeyboardActionValue));
await Task.CompletedTask;
}
private async Task OnEditShortcutBackground()
{
Helper.AllowRunnerToForeground();
ShellPage.DefaultSndMSGCallback(Helper.GetSerializedCustomAction(PowerToyName, EditShortcutActionName, EditShortcutActionValue));
await Task.CompletedTask;
}
private async void OnConfigFileUpdate()
{
// Note: FileSystemWatcher raise notification multiple times for single update operation.
// Todo: Handle duplicate events either by somehow suppress them or re-read the configuration everytime since we will be updating the UI only if something is changed.
if (LoadProfile())
{
await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
OnPropertyChanged(nameof(RemapKeys));
OnPropertyChanged(nameof(RemapShortcuts));
});
}
}
private bool LoadProfile()
{
var success = true;
try
{
using (var profileFileMutex = Mutex.OpenExisting(ProfileFileMutexName))
{
if (profileFileMutex.WaitOne(ProfileFileMutexWaitTimeoutMilliseconds))
{
// update the UI element here.
try
{
profile = SettingsUtils.GetSettings<KeyboardManagerProfile>(PowerToyName, settings.Properties.ActiveConfiguration.Value + JsonFileType);
FilterRemapKeysList(profile.RemapKeys.InProcessRemapKeys);
}
finally
{
// Make sure to release the mutex.
profileFileMutex.ReleaseMutex();
}
}
else
{
success = false;
}
}
}
catch (Exception)
{
// Failed to load the configuration.
success = false;
}
return success;
}
private void FilterRemapKeysList(List<KeysDataModel> remapKeysList)
{
CombineRemappings(remapKeysList, (uint)VirtualKey.LeftControl, (uint)VirtualKey.RightControl, (uint)VirtualKey.Control);
CombineRemappings(remapKeysList, (uint)VirtualKey.LeftMenu, (uint)VirtualKey.RightMenu, (uint)VirtualKey.Menu);
CombineRemappings(remapKeysList, (uint)VirtualKey.LeftShift, (uint)VirtualKey.RightShift, (uint)VirtualKey.Shift);
CombineRemappings(remapKeysList, (uint)VirtualKey.LeftWindows, (uint)VirtualKey.RightWindows, Helper.VirtualKeyWindows);
}
private void CombineRemappings(List<KeysDataModel> remapKeysList, uint leftKey, uint rightKey, uint combinedKey)
{
KeysDataModel firstRemap = remapKeysList.Find(x => uint.Parse(x.OriginalKeys) == leftKey);
KeysDataModel secondRemap = remapKeysList.Find(x => uint.Parse(x.OriginalKeys) == rightKey);
if (firstRemap != null && secondRemap != null)
{
if (firstRemap.NewRemapKeys == secondRemap.NewRemapKeys)
{
KeysDataModel combinedRemap = new KeysDataModel
{
OriginalKeys = combinedKey.ToString(),
NewRemapKeys = firstRemap.NewRemapKeys,
};
remapKeysList.Insert(remapKeysList.IndexOf(firstRemap), combinedRemap);
remapKeysList.Remove(firstRemap);
remapKeysList.Remove(secondRemap);
}
}
}
}
}