Skip to content

Commit

Permalink
[Keyboard Manager] Add JSON support for App Specific shortcuts (#4840)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
arjunbalgovind committed Jul 11, 2020
1 parent 653ae77 commit bb20494
Show file tree
Hide file tree
Showing 13 changed files with 403 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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 System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Lib.Utilities;

namespace Microsoft.PowerToys.Settings.UI.Lib
{
public class AppSpecificKeysDataModel : KeysDataModel
{
[JsonPropertyName("targetApp")]
public string TargetApp { get; set; }

public new List<string> GetOriginalKeys()
{
return base.GetOriginalKeys();
}

public new List<string> GetNewRemapKeys()
{
return base.GetNewRemapKeys();
}

public bool Compare(AppSpecificKeysDataModel arg)
{
return OriginalKeys.Equals(arg.OriginalKeys) && NewRemapKeys.Equals(arg.NewRemapKeys) && TargetApp.Equals(arg.TargetApp);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.PowerToys.Settings.UI.Lib.Utilities;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Lib.Utilities;

namespace Microsoft.PowerToys.Settings.UI.Lib
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ public RemapKeysDataModel()
InProcessRemapKeys = new List<KeysDataModel>();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ public class ShortcutsKeyDataModel
[JsonPropertyName("global")]
public List<KeysDataModel> GlobalRemapShortcuts { get; set; }

[JsonPropertyName("appSpecific")]
public List<AppSpecificKeysDataModel> AppSpecificRemapShortcuts { get; set; }

public ShortcutsKeyDataModel()
{
GlobalRemapShortcuts = new List<KeysDataModel>();
AppSpecificRemapShortcuts = new List<AppSpecificKeysDataModel>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ public KeyboardManagerViewModel()
PowerToyName,
settings.Properties.ActiveConfiguration.Value + JsonFileType,
OnConfigFileUpdate);

}

public bool Enabled
Expand Down Expand Up @@ -113,17 +112,22 @@ public List<KeysDataModel> RemapKeys
}
}

public List<KeysDataModel> RemapShortcuts
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 profile.RemapShortcuts.GlobalRemapShortcuts;
return CombineShortcutLists(profile.RemapShortcuts.GlobalRemapShortcuts, profile.RemapShortcuts.AppSpecificRemapShortcuts);
}
else
{
return new List<KeysDataModel>();
return new List<AppSpecificKeysDataModel>();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,95 @@
</ItemsControl>
</StackPanel>
</DataTemplate>
<DataTemplate x:Name="ShortcutKeysListViewTemplate" x:DataType="Lib:AppSpecificKeysDataModel">
<StackPanel
Orientation="Horizontal"
Height="56">
<ItemsControl
ItemsSource="{x:Bind GetOriginalKeys()}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
Background="{ThemeResource SystemBaseLowColor}"
CornerRadius="4"
Padding="14,0,14,0"
Margin="5,0,5,0"
Height="36"
VerticalAlignment="Center"
HorizontalAlignment="Left">
<TextBlock
FontWeight="SemiBold"
VerticalAlignment="Center"
TextAlignment="Center"
FontSize="12"
Text="{Binding}" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<FontIcon Glyph="&#xE72A;"
Grid.Column="1"
FontSize="14"
VerticalAlignment="Center"
Margin="5,0,5,0"/>
<ItemsControl
ItemsSource="{x:Bind GetNewRemapKeys()}"
Grid.Column="2">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
Background="{ThemeResource SystemAccentColor}"
CornerRadius="4"
Padding="14,0,14,0"
Margin="5,0,5,0"
Height="36"
VerticalAlignment="Center"
HorizontalAlignment="Left">
<TextBlock
FontWeight="SemiBold"
VerticalAlignment="Center"
TextAlignment="Center"
Foreground="White"
FontSize="12"
Text="{Binding}" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<FontIcon Glyph="&#xE72A;"
Grid.Column="3"
FontSize="14"
VerticalAlignment="Center"
Margin="5,0,5,0"/>
<Border
Background="{ThemeResource SystemAccentColor}"
Grid.Column="4"
CornerRadius="4"
Padding="14,0,14,0"
Margin="5,0,5,0"
Height="36"
VerticalAlignment="Center"
HorizontalAlignment="Left">
<TextBlock
FontWeight="SemiBold"
VerticalAlignment="Center"
TextAlignment="Center"
Foreground="White"
FontSize="12"
Text="{x:Bind TargetApp}" />
</Border>
</StackPanel>
</DataTemplate>
</Page.Resources>

<Grid ColumnSpacing="{StaticResource DefaultColumnSpacing}" RowSpacing="{StaticResource DefaultRowSpacing}">
Expand Down Expand Up @@ -192,7 +281,7 @@
<ListView x:Name="RemapShortcutsList"
extensions:ListViewExtensions.AlternateColor="{ThemeResource SystemControlBackgroundListLowBrush}"
ItemsSource="{x:Bind Path=ViewModel.RemapShortcuts, Mode=OneWay}"
ItemTemplate="{StaticResource KeysListViewTemplate}"
ItemTemplate="{StaticResource ShortcutKeysListViewTemplate}"
BorderBrush="{ThemeResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1"
CornerRadius="4"
Expand All @@ -203,6 +292,9 @@
SelectionMode="None"
IsSwipeEnabled="False"
Visibility="{x:Bind Path=ViewModel.RemapShortcuts, Mode=OneWay, Converter={StaticResource visibleIfNotEmptyConverter}}"
ScrollViewer.HorizontalScrollMode="Enabled"
ScrollViewer.HorizontalScrollBarVisibility="Visible"
ScrollViewer.IsHorizontalRailEnabled="True"
/>

<!--<AppBarButton x:Uid="KeyboardManager_RemapShortcutsButton"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
<Compile Include="UnitTestApp.xaml.cs">
<DependentUpon>UnitTestApp.xaml</DependentUpon>
</Compile>
<Compile Include="ViewModelTests\KeyboardManager.cs" />
<Compile Include="ViewModelTests\PowerRename.cs" />
<Compile Include="ViewModelTests\PowerPreview.cs" />
<Compile Include="ViewModelTests\ShortcutGuide.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using Microsoft.PowerToys.Settings.UI.Lib;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Windows.System;

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<KeysDataModel>();
var secondList = new List<AppSpecificKeysDataModel>();

// act
var result = KeyboardManagerViewModel.CombineShortcutLists(firstList, secondList);

// Assert
var expectedResult = new List<AppSpecificKeysDataModel>();

Assert.AreEqual(expectedResult.Count(), result.Count());
}

[TestMethod]
public void CombineShortcutLists_ShouldReturnListWithOneAllAppsEntry_WhenFirstArgumentHasOneEntryAndSecondArgumentIsEmpty()
{
// arrange
var firstList = new List<KeysDataModel>();
var entry = new KeysDataModel();
entry.OriginalKeys = VirtualKey.Control + ";" + VirtualKey.A;
entry.NewRemapKeys = VirtualKey.Control + ";" + VirtualKey.V;
firstList.Add(entry);
var secondList = new List<AppSpecificKeysDataModel>();

// act
var result = KeyboardManagerViewModel.CombineShortcutLists(firstList, secondList);

// Assert
var expectedResult = new List<AppSpecificKeysDataModel>();
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<KeysDataModel>();
var secondList = new List<AppSpecificKeysDataModel>();
var entry = new AppSpecificKeysDataModel();
entry.OriginalKeys = VirtualKey.Control + ";" + VirtualKey.A;
entry.NewRemapKeys = VirtualKey.Control + ";" + VirtualKey.V;
entry.TargetApp = "msedge";
secondList.Add(entry);

// act
var result = KeyboardManagerViewModel.CombineShortcutLists(firstList, secondList);

// Assert
var expectedResult = new List<AppSpecificKeysDataModel>();
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<KeysDataModel>();
var firstListEntry = new KeysDataModel();
firstListEntry.OriginalKeys = VirtualKey.Control + ";" + VirtualKey.A;
firstListEntry.NewRemapKeys = VirtualKey.Control + ";" + VirtualKey.V;
firstList.Add(firstListEntry);
var secondList = new List<AppSpecificKeysDataModel>();
var secondListEntry = new AppSpecificKeysDataModel();
secondListEntry.OriginalKeys = VirtualKey.Control + ";" + VirtualKey.B;
secondListEntry.NewRemapKeys = VirtualKey.Control + ";" + VirtualKey.W;
secondListEntry.TargetApp = "msedge";
secondList.Add(secondListEntry);

// act
var result = KeyboardManagerViewModel.CombineShortcutLists(firstList, secondList);

// Assert
var expectedResult = new List<AppSpecificKeysDataModel>();
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]));
}
}
}
6 changes: 6 additions & 0 deletions src/modules/keyboardmanager/common/KeyboardManagerConstants.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,18 @@ namespace KeyboardManagerConstants
// Name of the property use to store global shortcut remaps array.
inline const std::wstring GlobalRemapShortcutsSettingName = L"global";

// Name of the property use to store app specific shortcut remaps array.
inline const std::wstring AppSpecificRemapShortcutsSettingName = L"appSpecific";

// Name of the property use to store original keys.
inline const std::wstring OriginalKeysSettingName = L"originalKeys";

// Name of the property use to store new remap keys.
inline const std::wstring NewRemapKeysSettingName = L"newRemapKeys";

// Name of the property use to store the target application.
inline const std::wstring TargetAppSettingName = L"targetApp";

// Name of the default configuration.
inline const std::wstring DefaultConfiguration = L"default";

Expand Down

0 comments on commit bb20494

Please sign in to comment.