diff --git a/src/core/Microsoft.PowerToys.Settings.UI/ViewModels/KeyboardManagerViewModel.cs b/src/core/Microsoft.PowerToys.Settings.UI.Lib/ViewModels/KeyboardManagerViewModel.cs similarity index 65% rename from src/core/Microsoft.PowerToys.Settings.UI/ViewModels/KeyboardManagerViewModel.cs rename to src/core/Microsoft.PowerToys.Settings.UI.Lib/ViewModels/KeyboardManagerViewModel.cs index 9df0ad629b..c0a795534d 100644 --- a/src/core/Microsoft.PowerToys.Settings.UI/ViewModels/KeyboardManagerViewModel.cs +++ b/src/core/Microsoft.PowerToys.Settings.UI.Lib/ViewModels/KeyboardManagerViewModel.cs @@ -9,15 +9,11 @@ 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.Helpers; using Microsoft.PowerToys.Settings.UI.Lib.Utilities; -using Microsoft.PowerToys.Settings.UI.Views; -using Windows.System; -using Windows.UI.Core; -using Windows.UI.Xaml; +using Microsoft.PowerToys.Settings.UI.Lib.ViewModels.Commands; -namespace Microsoft.PowerToys.Settings.UI.ViewModels +namespace Microsoft.PowerToys.Settings.UI.Lib.ViewModels { public class KeyboardManagerViewModel : Observable { @@ -30,18 +26,24 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels 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; + public KeyboardManagerSettings settings; private KeyboardManagerProfile profile; private GeneralSettings generalSettings; - public KeyboardManagerViewModel() + private Func SendConfigMSG { get; } + + private Func, int> FilterRemapKeysList { get; } + + public KeyboardManagerViewModel(Func ipcMSGCallBackFunc, Func, int> filterRemapKeysList) { - dispatcher = Window.Current.Dispatcher; + // set the callback functions value to hangle outgoing IPC message. + SendConfigMSG = ipcMSGCallBackFunc; + FilterRemapKeysList = filterRemapKeysList; + if (SettingsUtils.SettingsExists(PowerToyName)) { // Todo: Be more resilient while reading and saving settings. @@ -68,11 +70,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels generalSettings = new GeneralSettings(); SettingsUtils.SaveSettings(generalSettings.ToJsonString(), string.Empty); } - - watcher = Helper.GetFileWatcher( - PowerToyName, - settings.Properties.ActiveConfiguration.Value + JsonFileType, - OnConfigFileUpdate); } public bool Enabled @@ -90,7 +87,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels OnPropertyChanged(nameof(Enabled)); OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(generalSettings); - ShellPage.DefaultSndMSGCallback(outgoing.ToString()); + SendConfigMSG(outgoing.ToString()); } } } @@ -148,32 +145,24 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private async Task OnRemapKeyboardBackground() { Helper.AllowRunnerToForeground(); - ShellPage.DefaultSndMSGCallback(Helper.GetSerializedCustomAction(PowerToyName, RemapKeyboardActionName, RemapKeyboardActionValue)); + SendConfigMSG(Helper.GetSerializedCustomAction(PowerToyName, RemapKeyboardActionName, RemapKeyboardActionValue)); await Task.CompletedTask; } private async Task OnEditShortcutBackground() { Helper.AllowRunnerToForeground(); - ShellPage.DefaultSndMSGCallback(Helper.GetSerializedCustomAction(PowerToyName, EditShortcutActionName, EditShortcutActionValue)); + SendConfigMSG(Helper.GetSerializedCustomAction(PowerToyName, EditShortcutActionName, EditShortcutActionValue)); await Task.CompletedTask; } - private async void OnConfigFileUpdate() + public void NotifyFileChanged() { - // 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)); - }); - } + OnPropertyChanged(nameof(RemapKeys)); + OnPropertyChanged(nameof(RemapShortcuts)); } - private bool LoadProfile() + public bool LoadProfile() { var success = true; @@ -210,32 +199,5 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels return success; } - private void FilterRemapKeysList(List 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 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); - } - } - } } } diff --git a/src/core/Microsoft.PowerToys.Settings.UI.UnitTests/ViewModelTests/KeyboardManager.cs b/src/core/Microsoft.PowerToys.Settings.UI.UnitTests/ViewModelTests/KeyboardManager.cs new file mode 100644 index 0000000000..970fe95512 --- /dev/null +++ b/src/core/Microsoft.PowerToys.Settings.UI.UnitTests/ViewModelTests/KeyboardManager.cs @@ -0,0 +1,134 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.PowerToys.Settings.UI.Lib; +using Microsoft.PowerToys.Settings.UI.Lib.ViewModels; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace ViewModelTests +{ + [TestClass] + public class KeyboardManager + { + public const string Module = "Keyboard Manager"; + + [TestInitialize] + public void Setup() + { + } + + [TestCleanup] + public void CleanUp() + { + } + + [TestMethod] + public void CombineShortcutLists_ShouldReturnEmptyList_WhenBothArgumentsAreEmptyLists() + { + // arrange + var firstList = new List(); + var secondList = new List(); + + // act + var result = KeyboardManagerViewModel.CombineShortcutLists(firstList, secondList); + + // Assert + var expectedResult = new List(); + + Assert.AreEqual(expectedResult.Count(), result.Count()); + } + + [TestMethod] + public void CombineShortcutLists_ShouldReturnListWithOneAllAppsEntry_WhenFirstArgumentHasOneEntryAndSecondArgumentIsEmpty() + { + // arrange + var firstList = new List(); + var entry = new KeysDataModel(); + entry.OriginalKeys = "17;65"; + entry.NewRemapKeys = "17;86"; + firstList.Add(entry); + var secondList = new List(); + + // act + var result = KeyboardManagerViewModel.CombineShortcutLists(firstList, secondList); + + // Assert + var expectedResult = new List(); + var expectedEntry = new AppSpecificKeysDataModel(); + expectedEntry.OriginalKeys = entry.OriginalKeys; + expectedEntry.NewRemapKeys = entry.NewRemapKeys; + expectedEntry.TargetApp = "All Apps"; + expectedResult.Add(expectedEntry); + var x = expectedResult[0].Equals(result[0]); + Assert.AreEqual(expectedResult.Count(), result.Count()); + Assert.IsTrue(expectedResult[0].Compare(result[0])); + } + + [TestMethod] + public void CombineShortcutLists_ShouldReturnListWithOneAppSpecificEntry_WhenFirstArgumentIsEmptyAndSecondArgumentHasOneEntry() + { + // arrange + var firstList = new List(); + var secondList = new List(); + var entry = new AppSpecificKeysDataModel(); + entry.OriginalKeys = "17;65"; + entry.NewRemapKeys = "17;86"; + entry.TargetApp = "msedge"; + secondList.Add(entry); + + // act + var result = KeyboardManagerViewModel.CombineShortcutLists(firstList, secondList); + + // Assert + var expectedResult = new List(); + var expectedEntry = new AppSpecificKeysDataModel(); + expectedEntry.OriginalKeys = entry.OriginalKeys; + expectedEntry.NewRemapKeys = entry.NewRemapKeys; + expectedEntry.TargetApp = entry.TargetApp; + expectedResult.Add(expectedEntry); + + Assert.AreEqual(expectedResult.Count(), result.Count()); + Assert.IsTrue(expectedResult[0].Compare(result[0])); + } + + [TestMethod] + public void CombineShortcutLists_ShouldReturnListWithOneAllAppsEntryAndOneAppSpecificEntry_WhenFirstArgumentHasOneEntryAndSecondArgumentHasOneEntry() + { + // arrange + var firstList = new List(); + var firstListEntry = new KeysDataModel(); + firstListEntry.OriginalKeys = "17;65"; + firstListEntry.NewRemapKeys = "17;86"; + firstList.Add(firstListEntry); + var secondList = new List(); + var secondListEntry = new AppSpecificKeysDataModel(); + secondListEntry.OriginalKeys = "17;66"; + secondListEntry.NewRemapKeys = "17;87"; + secondListEntry.TargetApp = "msedge"; + secondList.Add(secondListEntry); + + // act + var result = KeyboardManagerViewModel.CombineShortcutLists(firstList, secondList); + + // Assert + var expectedResult = new List(); + var expectedFirstEntry = new AppSpecificKeysDataModel(); + expectedFirstEntry.OriginalKeys = firstListEntry.OriginalKeys; + expectedFirstEntry.NewRemapKeys = firstListEntry.NewRemapKeys; + expectedFirstEntry.TargetApp = "All Apps"; + expectedResult.Add(expectedFirstEntry); + var expectedSecondEntry = new AppSpecificKeysDataModel(); + expectedSecondEntry.OriginalKeys = secondListEntry.OriginalKeys; + expectedSecondEntry.NewRemapKeys = secondListEntry.NewRemapKeys; + expectedSecondEntry.TargetApp = secondListEntry.TargetApp; + expectedResult.Add(expectedSecondEntry); + + Assert.AreEqual(expectedResult.Count(), result.Count()); + Assert.IsTrue(expectedResult[0].Compare(result[0])); + Assert.IsTrue(expectedResult[1].Compare(result[1])); + } + } +} diff --git a/src/core/Microsoft.PowerToys.Settings.UI/Microsoft.PowerToys.Settings.UI.csproj b/src/core/Microsoft.PowerToys.Settings.UI/Microsoft.PowerToys.Settings.UI.csproj index f2e4012bd7..987f7aca4b 100644 --- a/src/core/Microsoft.PowerToys.Settings.UI/Microsoft.PowerToys.Settings.UI.csproj +++ b/src/core/Microsoft.PowerToys.Settings.UI/Microsoft.PowerToys.Settings.UI.csproj @@ -111,7 +111,6 @@ - diff --git a/src/core/Microsoft.PowerToys.Settings.UI/Views/KeyboardManagerPage.xaml b/src/core/Microsoft.PowerToys.Settings.UI/Views/KeyboardManagerPage.xaml index bc6a1194c7..fc3ea6a10a 100644 --- a/src/core/Microsoft.PowerToys.Settings.UI/Views/KeyboardManagerPage.xaml +++ b/src/core/Microsoft.PowerToys.Settings.UI/Views/KeyboardManagerPage.xaml @@ -5,7 +5,7 @@ xmlns:local="using:Microsoft.PowerToys.Settings.UI.Views" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:viewModel="using:Microsoft.PowerToys.Settings.UI.ViewModels" + xmlns:viewModel="using:Microsoft.PowerToys.Settings.UI.Lib.ViewModels" xmlns:extensions="using:Microsoft.Toolkit.Uwp.UI.Extensions" xmlns:CustomControls="using:Microsoft.PowerToys.Settings.UI.Controls" xmlns:Lib="using:Microsoft.PowerToys.Settings.UI.Lib" @@ -13,7 +13,6 @@ Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> - public sealed partial class KeyboardManagerPage : Page { - public KeyboardManagerViewModel ViewModel { get; } = new KeyboardManagerViewModel(); + private const string PowerToyName = "Keyboard Manager"; + + private readonly CoreDispatcher dispatcher; + private readonly FileSystemWatcher watcher; + + public KeyboardManagerViewModel ViewModel { get; } public KeyboardManagerPage() { + dispatcher = Window.Current.Dispatcher; + + ViewModel = new KeyboardManagerViewModel(ShellPage.SendDefaultIPCMessage, FilterRemapKeysList); + + watcher = Helper.GetFileWatcher( + PowerToyName, + ViewModel.settings.Properties.ActiveConfiguration.Value + ".json", + OnConfigFileUpdate); + InitializeComponent(); DataContext = ViewModel; } + + 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 (ViewModel.LoadProfile()) + { + await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + ViewModel.NotifyFileChanged(); + }); + } + } + + private void CombineRemappings(List 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); + } + } + } + + private int FilterRemapKeysList(List 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); + + return 0; + } } }