[KBM] Migrate Engine and Editor into separate processes (#10774)

* Move KBM engine into separate process (#10672)

* [KBM] Migrate KBM UI out of the runner (#10709)

* Clean up keyboard hook handles (#10817)

* [C++ common] Unhandled exception handler (#10821)

* [KBM] Use icon in the KeyboardManagerEditor (#10845)

* [KBM] Move resources from the Common project to the Editor. (#10844)

* KBM Editor tests (#10858)

* Rename engine executable (#10868)

* clean up (#10870)

* [KBM] Changed Editor and libraries output folders (#10871)

* [KBM] New logs structure (#10872)

* Add unhandled exception handling to the editor (#10874)

* [KBM] Trace for edit keyboard window

* Logging for XamlBridge message loop

* [KBM] Added Editor and Engine to the installer (#10876)

* Fix spelling

* Interprocess communication logs, remove unnecessary windows message logs

* [KBM] Separated telemetry for the engine and editor. (#10889)

* [KBM] Editor test project (#10891)

* Versions for the engine and the editor (#10897)

* Add the editor's and the engine's executables to signing process (#10900)

* [KBM editor] Run only one instance, exit when parent process exits (#10890)

* [KBM] Force kill editor process to avoid XAML crash (#10907)

* [KBM] Force kill editor process to avoid XAML crash

* Fix event releasing

Co-authored-by: mykhailopylyp <17161067+mykhailopylyp@users.noreply.github.com>

* Make the editor dpi aware (#10908)

* [KBM] KeyboardManagerCommon refactoring (#10909)

* Do not start the process if it is already started (#10910)

* logs

* Update src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditKeyboardWindow.cpp

* Update src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditKeyboardWindow.cpp

* [KBM] Rename InitUnhandledExceptionHandler
to make it explicit that is for x64 only.
We will fix it properly when adding support for ARM64 and add a header with
the proper conditional building.

* [KBM] rename file/class/variables using camel case

* [KBM] Rename "event_locker" -> "EventLocker"

* [KBM] rename process_waiter
Add a TODO comment

* [KBM] rename methods
Add TODO comment

* [KBM] use uppercase for function names

* [KBM] use uppercase for methos, lowercase for properties

* [KBM] rename method, make methods private, formatting

* [KBM] rename private variables

* [KBM] use uppercase for function names

* [KBM] Added support to run the editor stand-alone when built in debug mode

* Update src/modules/keyboardmanager/KeyboardManagerEditor/KeyboardManagerEditor.cpp

* Check success of event creation, comment (#10947)

* [KBM] code formatting (#10951)

* [KBM] code formatting

* Update src/modules/keyboardmanager/KeyboardManagerEditorLibrary/BufferValidationHelpers.cpp

* [KBM] tracing

* [KBM] Remappings not showing fix. (#10954)

* removed mutex

* retry loop for reading

* retry on reading config once

* log error

Co-authored-by: Enrico Giordani <enricogior@users.noreply.github.com>

Co-authored-by: Enrico Giordani <enricogior@users.noreply.github.com>

Co-authored-by: Seraphima Zykova <zykovas91@gmail.com>
Co-authored-by: Enrico Giordani <enricogior@users.noreply.github.com>
Co-authored-by: Enrico Giordani <enrico.giordani@gmail.com>
This commit is contained in:
Mykhailo Pylyp
2021-04-26 22:01:38 +03:00
committed by GitHub
parent e9a0b58796
commit a8c99e9513
141 changed files with 5124 additions and 2374 deletions

View File

@@ -0,0 +1,309 @@
#include "pch.h"
#include "BufferValidationHelpers.h"
#include <common/interop/shared_constants.h>
#include <KeyboardManagerEditorStrings.h>
#include <KeyboardManagerConstants.h>
#include <KeyDropDownControl.h>
namespace BufferValidationHelpers
{
// Function to validate and update an element of the key remap buffer when the selection has changed
KeyboardManagerHelper::ErrorType ValidateAndUpdateKeyBufferElement(int rowIndex, int colIndex, int selectedKeyCode, RemapBuffer& remapBuffer)
{
KeyboardManagerHelper::ErrorType errorType = KeyboardManagerHelper::ErrorType::NoError;
// Check if the element was not found or the index exceeds the known keys
if (selectedKeyCode != -1)
{
// Check if the value being set is the same as the other column
if (remapBuffer[rowIndex].first[std::abs(int(colIndex) - 1)].index() == 0)
{
if (std::get<DWORD>(remapBuffer[rowIndex].first[std::abs(int(colIndex) - 1)]) == selectedKeyCode)
{
errorType = KeyboardManagerHelper::ErrorType::MapToSameKey;
}
}
// If one column is shortcut and other is key no warning required
if (errorType == KeyboardManagerHelper::ErrorType::NoError && colIndex == 0)
{
// Check if the key is already remapped to something else
for (int i = 0; i < remapBuffer.size(); i++)
{
if (i != rowIndex)
{
if (remapBuffer[i].first[colIndex].index() == 0)
{
KeyboardManagerHelper::ErrorType result = KeyboardManagerHelper::DoKeysOverlap(std::get<DWORD>(remapBuffer[i].first[colIndex]), selectedKeyCode);
if (result != KeyboardManagerHelper::ErrorType::NoError)
{
errorType = result;
break;
}
}
// If one column is shortcut and other is key no warning required
}
}
}
// If there is no error, set the buffer
if (errorType == KeyboardManagerHelper::ErrorType::NoError)
{
remapBuffer[rowIndex].first[colIndex] = selectedKeyCode;
}
else
{
remapBuffer[rowIndex].first[colIndex] = NULL;
}
}
else
{
// Reset to null if the key is not found
remapBuffer[rowIndex].first[colIndex] = NULL;
}
return errorType;
}
// Function to validate an element of the shortcut remap buffer when the selection has changed
std::pair<KeyboardManagerHelper::ErrorType, DropDownAction> ValidateShortcutBufferElement(int rowIndex, int colIndex, uint32_t dropDownIndex, const std::vector<int32_t>& selectedCodes, std::wstring appName, bool isHybridControl, const RemapBuffer& remapBuffer, bool dropDownFound)
{
BufferValidationHelpers::DropDownAction dropDownAction = BufferValidationHelpers::DropDownAction::NoAction;
KeyboardManagerHelper::ErrorType errorType = KeyboardManagerHelper::ErrorType::NoError;
size_t dropDownCount = selectedCodes.size();
DWORD selectedKeyCode = dropDownFound ? selectedCodes[dropDownIndex] : -1;
if (selectedKeyCode != -1 && dropDownFound)
{
// If only 1 drop down and action key is chosen: Warn that a modifier must be chosen (if the drop down is not for a hybrid scenario)
if (dropDownCount == 1 && !KeyboardManagerHelper::IsModifierKey(selectedKeyCode) && !isHybridControl)
{
// warn and reset the drop down
errorType = KeyboardManagerHelper::ErrorType::ShortcutStartWithModifier;
}
else if (dropDownIndex == dropDownCount - 1)
{
// If it is the last drop down
// If last drop down and a modifier is selected: add a new drop down (max drop down count should be enforced)
if (KeyboardManagerHelper::IsModifierKey(selectedKeyCode) && dropDownCount < KeyboardManagerConstants::MaxShortcutSize)
{
// If it matched any of the previous modifiers then reset that drop down
if (KeyboardManagerHelper::CheckRepeatedModifier(selectedCodes, selectedKeyCode))
{
// warn and reset the drop down
errorType = KeyboardManagerHelper::ErrorType::ShortcutCannotHaveRepeatedModifier;
}
else
{
// If not, add a new drop down
dropDownAction = BufferValidationHelpers::DropDownAction::AddDropDown;
}
}
else if (KeyboardManagerHelper::IsModifierKey(selectedKeyCode) && dropDownCount >= KeyboardManagerConstants::MaxShortcutSize)
{
// If last drop down and a modifier is selected but there are already max drop downs: warn the user
// warn and reset the drop down
errorType = KeyboardManagerHelper::ErrorType::ShortcutMaxShortcutSizeOneActionKey;
}
else if (selectedKeyCode == 0)
{
// If None is selected but it's the last index: warn
// If it is a hybrid control and there are 2 drop downs then deletion is allowed
if (isHybridControl && dropDownCount == KeyboardManagerConstants::MinShortcutSize)
{
// set delete drop down flag
dropDownAction = BufferValidationHelpers::DropDownAction::DeleteDropDown;
// do not delete the drop down now since there may be some other error which would cause the drop down to be invalid after removal
}
else
{
// warn and reset the drop down
errorType = KeyboardManagerHelper::ErrorType::ShortcutOneActionKey;
}
}
else if (selectedKeyCode == CommonSharedConstants::VK_DISABLED && dropDownIndex)
{
// Disable can not be selected if one modifier key has already been selected
errorType = KeyboardManagerHelper::ErrorType::ShortcutDisableAsActionKey;
}
// If none of the above, then the action key will be set
}
else
{
// If it is not the last drop down
if (KeyboardManagerHelper::IsModifierKey(selectedKeyCode))
{
// If it matched any of the previous modifiers then reset that drop down
if (KeyboardManagerHelper::CheckRepeatedModifier(selectedCodes, selectedKeyCode))
{
// warn and reset the drop down
errorType = KeyboardManagerHelper::ErrorType::ShortcutCannotHaveRepeatedModifier;
}
// If not, the modifier key will be set
}
else if (selectedKeyCode == 0 && dropDownCount > KeyboardManagerConstants::MinShortcutSize)
{
// If None is selected and there are more than 2 drop downs
// set delete drop down flag
dropDownAction = BufferValidationHelpers::DropDownAction::DeleteDropDown;
// do not delete the drop down now since there may be some other error which would cause the drop down to be invalid after removal
}
else if (selectedKeyCode == 0 && dropDownCount <= KeyboardManagerConstants::MinShortcutSize)
{
// If it is a hybrid control and there are 2 drop downs then deletion is allowed
if (isHybridControl && dropDownCount == KeyboardManagerConstants::MinShortcutSize)
{
// set delete drop down flag
dropDownAction = BufferValidationHelpers::DropDownAction::DeleteDropDown;
// do not delete the drop down now since there may be some other error which would cause the drop down to be invalid after removal
}
else
{
// warn and reset the drop down
errorType = KeyboardManagerHelper::ErrorType::ShortcutAtleast2Keys;
}
}
else if (selectedKeyCode == CommonSharedConstants::VK_DISABLED && dropDownIndex)
{
// Allow selection of VK_DISABLE only in first dropdown
errorType = KeyboardManagerHelper::ErrorType::ShortcutDisableAsActionKey;
}
else if (dropDownIndex != 0 || isHybridControl)
{
// If the user tries to set an action key check if all drop down menus after this are empty if it is not the first key.
// If it is a hybrid control, this can be done even on the first key
bool isClear = true;
for (int i = dropDownIndex + 1; i < (int)dropDownCount; i++)
{
if (selectedCodes[i] != -1)
{
isClear = false;
break;
}
}
if (isClear)
{
dropDownAction = BufferValidationHelpers::DropDownAction::ClearUnusedDropDowns;
}
else
{
// warn and reset the drop down
errorType = KeyboardManagerHelper::ErrorType::ShortcutNotMoreThanOneActionKey;
}
}
else
{
// If there an action key is chosen on the first drop down and there are more than one drop down menus
// warn and reset the drop down
errorType = KeyboardManagerHelper::ErrorType::ShortcutStartWithModifier;
}
}
}
// After validating the shortcut, now for errors like remap to same shortcut, remap shortcut more than once, Win L and Ctrl Alt Del
if (errorType == KeyboardManagerHelper::ErrorType::NoError)
{
KeyShortcutUnion tempShortcut;
if (isHybridControl && KeyDropDownControl::GetNumberOfSelectedKeys(selectedCodes) == 1)
{
tempShortcut = *std::find_if(selectedCodes.begin(), selectedCodes.end(), [](int32_t a) { return a != -1 && a != 0; });
}
else
{
tempShortcut = Shortcut();
std::get<Shortcut>(tempShortcut).SetKeyCodes(selectedCodes);
}
// Convert app name to lower case
std::transform(appName.begin(), appName.end(), appName.begin(), towlower);
std::wstring lowercaseDefAppName = KeyboardManagerEditorStrings::DefaultAppName;
std::transform(lowercaseDefAppName.begin(), lowercaseDefAppName.end(), lowercaseDefAppName.begin(), towlower);
if (appName == lowercaseDefAppName)
{
appName = L"";
}
// Check if the value being set is the same as the other column - index of other column does not have to be checked since only one column is hybrid
if (tempShortcut.index() == 1)
{
// If shortcut to shortcut
if (remapBuffer[rowIndex].first[std::abs(int(colIndex) - 1)].index() == 1)
{
if (std::get<Shortcut>(remapBuffer[rowIndex].first[std::abs(int(colIndex) - 1)]) == std::get<Shortcut>(tempShortcut) && std::get<Shortcut>(remapBuffer[rowIndex].first[std::abs(int(colIndex) - 1)]).IsValidShortcut() && std::get<Shortcut>(tempShortcut).IsValidShortcut())
{
errorType = KeyboardManagerHelper::ErrorType::MapToSameShortcut;
}
}
// If one column is shortcut and other is key no warning required
}
else
{
// If key to key
if (remapBuffer[rowIndex].first[std::abs(int(colIndex) - 1)].index() == 0)
{
if (std::get<DWORD>(remapBuffer[rowIndex].first[std::abs(int(colIndex) - 1)]) == std::get<DWORD>(tempShortcut) && std::get<DWORD>(remapBuffer[rowIndex].first[std::abs(int(colIndex) - 1)]) != NULL && std::get<DWORD>(tempShortcut) != NULL)
{
errorType = KeyboardManagerHelper::ErrorType::MapToSameKey;
}
}
// If one column is shortcut and other is key no warning required
}
if (errorType == KeyboardManagerHelper::ErrorType::NoError && colIndex == 0)
{
// Check if the key is already remapped to something else for the same target app
for (int i = 0; i < remapBuffer.size(); i++)
{
std::wstring currAppName = remapBuffer[i].second;
std::transform(currAppName.begin(), currAppName.end(), currAppName.begin(), towlower);
if (i != rowIndex && currAppName == appName)
{
KeyboardManagerHelper::ErrorType result = KeyboardManagerHelper::ErrorType::NoError;
if (!isHybridControl)
{
result = Shortcut::DoKeysOverlap(std::get<Shortcut>(remapBuffer[i].first[colIndex]), std::get<Shortcut>(tempShortcut));
}
else
{
if (tempShortcut.index() == 0 && remapBuffer[i].first[colIndex].index() == 0)
{
if (std::get<DWORD>(tempShortcut) != NULL && std::get<DWORD>(remapBuffer[i].first[colIndex]) != NULL)
{
result = KeyboardManagerHelper::DoKeysOverlap(std::get<DWORD>(remapBuffer[i].first[colIndex]), std::get<DWORD>(tempShortcut));
}
}
else if (tempShortcut.index() == 1 && remapBuffer[i].first[colIndex].index() == 1)
{
if (std::get<Shortcut>(tempShortcut).IsValidShortcut() && std::get<Shortcut>(remapBuffer[i].first[colIndex]).IsValidShortcut())
{
result = Shortcut::DoKeysOverlap(std::get<Shortcut>(remapBuffer[i].first[colIndex]), std::get<Shortcut>(tempShortcut));
}
}
// Other scenarios not possible since key to shortcut is with key to key, and shortcut to key is with shortcut to shortcut
}
if (result != KeyboardManagerHelper::ErrorType::NoError)
{
errorType = result;
break;
}
}
}
}
if (errorType == KeyboardManagerHelper::ErrorType::NoError && tempShortcut.index() == 1)
{
errorType = std::get<Shortcut>(tempShortcut).IsShortcutIllegal();
}
}
return std::make_pair(errorType, dropDownAction);
}
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include <keyboardmanager/common/Helpers.h>
namespace BufferValidationHelpers
{
enum class DropDownAction
{
NoAction,
AddDropDown,
DeleteDropDown,
ClearUnusedDropDowns
};
// Function to validate and update an element of the key remap buffer when the selection has changed
KeyboardManagerHelper::ErrorType ValidateAndUpdateKeyBufferElement(int rowIndex, int colIndex, int selectedKeyCode, RemapBuffer& remapBuffer);
// Function to validate an element of the shortcut remap buffer when the selection has changed
std::pair<KeyboardManagerHelper::ErrorType, DropDownAction> ValidateShortcutBufferElement(int rowIndex, int colIndex, uint32_t dropDownIndex, const std::vector<int32_t>& selectedCodes, std::wstring appName, bool isHybridControl, const RemapBuffer& remapBuffer, bool dropDownFound);
}

View File

@@ -0,0 +1,19 @@
#include "pch.h"
#include "Dialog.h"
using namespace winrt::Windows::Foundation;
IAsyncOperation<bool> Dialog::PartialRemappingConfirmationDialog(XamlRoot root, std::wstring dialogTitle)
{
ContentDialog confirmationDialog;
confirmationDialog.XamlRoot(root);
confirmationDialog.Title(box_value(dialogTitle));
confirmationDialog.IsPrimaryButtonEnabled(true);
confirmationDialog.DefaultButton(ContentDialogButton::Primary);
confirmationDialog.PrimaryButtonText(winrt::hstring(GET_RESOURCE_STRING(IDS_CONTINUE_BUTTON)));
confirmationDialog.IsSecondaryButtonEnabled(true);
confirmationDialog.SecondaryButtonText(winrt::hstring(GET_RESOURCE_STRING(IDS_CANCEL_BUTTON)));
ContentDialogResult res = co_await confirmationDialog.ShowAsync();
co_return res == ContentDialogResult::Primary;
}

View File

@@ -0,0 +1,19 @@
#pragma once
namespace winrt::Windows::UI::Xaml
{
namespace Foundation
{
template<typename T>
struct IAsyncOperation;
}
namespace UI::Xaml
{
struct XamlRoot;
}
}
namespace Dialog
{
winrt::Windows::Foundation::IAsyncOperation<bool> PartialRemappingConfirmationDialog(winrt::Windows::UI::Xaml::XamlRoot root, std::wstring dialogTitle);
};

View File

@@ -0,0 +1,479 @@
#include "pch.h"
#include <set>
#include <common/Display/dpi_aware.h>
#include <common/interop/shared_constants.h>
#include <common/themes/windows_colors.h>
#include <common/utils/EventLocker.h>
#include <KeyboardManagerConstants.h>
#include <KeyboardManagerState.h>
#include "EditKeyboardWindow.h"
#include "ErrorTypes.h"
#include "SingleKeyRemapControl.h"
#include "KeyDropDownControl.h"
#include "XamlBridge.h"
#include "Styles.h"
#include "Dialog.h"
#include "LoadingAndSavingRemappingHelper.h"
#include "UIHelpers.h"
#include <common/utils/winapi_error.h>
using namespace winrt::Windows::Foundation;
LRESULT CALLBACK EditKeyboardWindowProc(HWND, UINT, WPARAM, LPARAM);
// This Hwnd will be the window handler for the Xaml Island: A child window that contains Xaml.
HWND hWndXamlIslandEditKeyboardWindow = nullptr;
// This variable is used to check if window registration has been done to avoid repeated registration leading to an error.
bool isEditKeyboardWindowRegistrationCompleted = false;
// Holds the native window handle of EditKeyboard Window.
HWND hwndEditKeyboardNativeWindow = nullptr;
std::mutex editKeyboardWindowMutex;
// Stores a pointer to the Xaml Bridge object so that it can be accessed from the window procedure
static XamlBridge* xamlBridgePtr = nullptr;
static IAsyncOperation<bool> OrphanKeysConfirmationDialog(
KeyboardManagerState& state,
const std::vector<DWORD>& keys,
XamlRoot root)
{
ContentDialog confirmationDialog;
confirmationDialog.XamlRoot(root);
confirmationDialog.Title(box_value(GET_RESOURCE_STRING(IDS_EDITKEYBOARD_ORPHANEDDIALOGTITLE)));
confirmationDialog.Content(nullptr);
confirmationDialog.IsPrimaryButtonEnabled(true);
confirmationDialog.DefaultButton(ContentDialogButton::Primary);
confirmationDialog.PrimaryButtonText(winrt::hstring(GET_RESOURCE_STRING(IDS_CONTINUE_BUTTON)));
confirmationDialog.IsSecondaryButtonEnabled(true);
confirmationDialog.SecondaryButtonText(winrt::hstring(GET_RESOURCE_STRING(IDS_CANCEL_BUTTON)));
TextBlock orphanKeysBlock;
std::wstring orphanKeyString;
for (auto k : keys)
{
orphanKeyString.append(state.keyboardMap.GetKeyName(k));
orphanKeyString.append(L", ");
}
orphanKeyString = orphanKeyString.substr(0, max(0, orphanKeyString.length() - 2));
orphanKeysBlock.Text(winrt::hstring(orphanKeyString));
orphanKeysBlock.TextWrapping(TextWrapping::Wrap);
confirmationDialog.Content(orphanKeysBlock);
ContentDialogResult res = co_await confirmationDialog.ShowAsync();
co_return res == ContentDialogResult::Primary;
}
static IAsyncAction OnClickAccept(KeyboardManagerState& keyboardManagerState, XamlRoot root, std::function<void()> ApplyRemappings)
{
KeyboardManagerHelper::ErrorType isSuccess = LoadingAndSavingRemappingHelper::CheckIfRemappingsAreValid(SingleKeyRemapControl::singleKeyRemapBuffer);
if (isSuccess != KeyboardManagerHelper::ErrorType::NoError)
{
if (!co_await Dialog::PartialRemappingConfirmationDialog(root, GET_RESOURCE_STRING(IDS_EDITKEYBOARD_PARTIALCONFIRMATIONDIALOGTITLE)))
{
co_return;
}
}
// Check for orphaned keys
// Draw content Dialog
std::vector<DWORD> orphanedKeys = LoadingAndSavingRemappingHelper::GetOrphanedKeys(SingleKeyRemapControl::singleKeyRemapBuffer);
if (orphanedKeys.size() > 0)
{
if (!co_await OrphanKeysConfirmationDialog(keyboardManagerState, orphanedKeys, root))
{
co_return;
}
}
ApplyRemappings();
}
// Function to create the Edit Keyboard Window
inline void CreateEditKeyboardWindowImpl(HINSTANCE hInst, KeyboardManagerState& keyboardManagerState)
{
Logger::trace("CreateEditKeyboardWindowImpl()");
auto locker = EventLocker::Get(KeyboardManagerConstants::EditorWindowEventName.c_str());
if (!locker.has_value())
{
Logger::error(L"Failed to lock event {}. {}", KeyboardManagerConstants::EditorWindowEventName, get_last_error_or_default(GetLastError()));
}
Logger::trace(L"Signaled {} event to suspend the KBM engine", KeyboardManagerConstants::EditorWindowEventName);
// Window Registration
const wchar_t szWindowClass[] = L"EditKeyboardWindow";
if (!isEditKeyboardWindowRegistrationCompleted)
{
WNDCLASSEX windowClass = {};
windowClass.cbSize = sizeof(WNDCLASSEX);
windowClass.lpfnWndProc = EditKeyboardWindowProc;
windowClass.hInstance = hInst;
windowClass.lpszClassName = szWindowClass;
windowClass.hbrBackground = (HBRUSH)(COLOR_WINDOW);
windowClass.hIcon = (HICON)LoadImageW(
windowClass.hInstance,
MAKEINTRESOURCE(IDS_KEYBOARDMANAGER_ICON),
IMAGE_ICON,
48,
48,
LR_DEFAULTCOLOR);
if (RegisterClassEx(&windowClass) == NULL)
{
MessageBox(NULL, GET_RESOURCE_STRING(IDS_REGISTERCLASSFAILED_ERRORMESSAGE).c_str(), GET_RESOURCE_STRING(IDS_REGISTERCLASSFAILED_ERRORTITLE).c_str(), NULL);
return;
}
isEditKeyboardWindowRegistrationCompleted = true;
}
// Find coordinates of the screen where the settings window is placed.
RECT desktopRect = UIHelpers::GetForegroundWindowDesktopRect();
// Calculate DPI dependent window size
int windowWidth = KeyboardManagerConstants::DefaultEditKeyboardWindowWidth;
int windowHeight = KeyboardManagerConstants::DefaultEditKeyboardWindowHeight;
DPIAware::Convert(nullptr, windowWidth, windowHeight);
// Window Creation
HWND _hWndEditKeyboardWindow = CreateWindow(
szWindowClass,
GET_RESOURCE_STRING(IDS_EDITKEYBOARD_WINDOWNAME).c_str(),
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MAXIMIZEBOX,
((desktopRect.right + desktopRect.left) / 2) - (windowWidth / 2),
((desktopRect.bottom + desktopRect.top) / 2) - (windowHeight / 2),
windowWidth,
windowHeight,
NULL,
NULL,
hInst,
NULL);
if (_hWndEditKeyboardWindow == NULL)
{
MessageBox(NULL, GET_RESOURCE_STRING(IDS_CREATEWINDOWFAILED_ERRORMESSAGE).c_str(), GET_RESOURCE_STRING(IDS_CREATEWINDOWFAILED_ERRORTITLE).c_str(), NULL);
return;
}
// Ensures the window is in foreground on first startup. If this is not done, the window appears behind because the thread is not on the foreground.
if (_hWndEditKeyboardWindow)
{
SetForegroundWindow(_hWndEditKeyboardWindow);
}
// Store the newly created Edit Keyboard window's handle.
std::unique_lock<std::mutex> hwndLock(editKeyboardWindowMutex);
hwndEditKeyboardNativeWindow = _hWndEditKeyboardWindow;
hwndLock.unlock();
// Create the xaml bridge object
XamlBridge xamlBridge(_hWndEditKeyboardWindow);
// DesktopSource needs to be declared before the RelativePanel xamlContainer object to avoid errors
winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource desktopSource;
// Create the desktop window xaml source object and set its content
hWndXamlIslandEditKeyboardWindow = xamlBridge.InitDesktopWindowsXamlSource(desktopSource);
// Set the pointer to the xaml bridge object
xamlBridgePtr = &xamlBridge;
// Header for the window
Windows::UI::Xaml::Controls::RelativePanel header;
header.Margin({ 10, 10, 10, 30 });
// Header text
TextBlock headerText;
headerText.Text(GET_RESOURCE_STRING(IDS_EDITKEYBOARD_WINDOWNAME));
headerText.FontSize(30);
headerText.Margin({ 0, 0, 0, 0 });
header.SetAlignLeftWithPanel(headerText, true);
// Header Cancel button
Button cancelButton;
cancelButton.Content(winrt::box_value(GET_RESOURCE_STRING(IDS_CANCEL_BUTTON)));
cancelButton.Margin({ 10, 0, 0, 0 });
cancelButton.Click([&](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
// Close the window since settings do not need to be saved
PostMessage(_hWndEditKeyboardWindow, WM_CLOSE, 0, 0);
});
// Text block for information about remap key section.
TextBlock keyRemapInfoHeader;
keyRemapInfoHeader.Text(GET_RESOURCE_STRING(IDS_EDITKEYBOARD_INFO));
keyRemapInfoHeader.Margin({ 10, 0, 0, 10 });
keyRemapInfoHeader.FontWeight(Text::FontWeights::SemiBold());
keyRemapInfoHeader.TextWrapping(TextWrapping::Wrap);
TextBlock keyRemapInfoExample;
keyRemapInfoExample.Text(GET_RESOURCE_STRING(IDS_EDITKEYBOARD_INFOEXAMPLE));
keyRemapInfoExample.Margin({ 10, 0, 0, 20 });
keyRemapInfoExample.FontStyle(Text::FontStyle::Italic);
keyRemapInfoExample.TextWrapping(TextWrapping::Wrap);
// Table to display the key remaps
StackPanel keyRemapTable;
// First header textblock in the header row of the keys remap table
TextBlock originalKeyRemapHeader;
originalKeyRemapHeader.Text(GET_RESOURCE_STRING(IDS_EDITKEYBOARD_SOURCEHEADER));
originalKeyRemapHeader.FontWeight(Text::FontWeights::Bold());
StackPanel originalKeyHeaderContainer = UIHelpers::GetWrapped(originalKeyRemapHeader, KeyboardManagerConstants::RemapTableDropDownWidth + KeyboardManagerConstants::TableArrowColWidth).as<StackPanel>();
// Second header textblock in the header row of the keys remap table
TextBlock newKeyRemapHeader;
newKeyRemapHeader.Text(GET_RESOURCE_STRING(IDS_EDITKEYBOARD_TARGETHEADER));
newKeyRemapHeader.FontWeight(Text::FontWeights::Bold());
StackPanel tableHeader = StackPanel();
tableHeader.Orientation(Orientation::Horizontal);
tableHeader.Margin({ 10, 0, 0, 10 });
tableHeader.Children().Append(originalKeyHeaderContainer);
tableHeader.Children().Append(newKeyRemapHeader);
// Store handle of edit keyboard window
SingleKeyRemapControl::EditKeyboardWindowHandle = _hWndEditKeyboardWindow;
// Store keyboard manager state
SingleKeyRemapControl::keyboardManagerState = &keyboardManagerState;
KeyDropDownControl::keyboardManagerState = &keyboardManagerState;
// Clear the single key remap buffer
SingleKeyRemapControl::singleKeyRemapBuffer.clear();
// Vector to store dynamically allocated control objects to avoid early destruction
std::vector<std::vector<std::unique_ptr<SingleKeyRemapControl>>> keyboardRemapControlObjects;
// Set keyboard manager UI state so that remaps are not applied while on this window
keyboardManagerState.SetUIState(KeyboardManagerUIState::EditKeyboardWindowActivated, _hWndEditKeyboardWindow);
// Load existing remaps into UI
SingleKeyRemapTable singleKeyRemapCopy = keyboardManagerState.singleKeyReMap;
LoadingAndSavingRemappingHelper::PreProcessRemapTable(singleKeyRemapCopy);
for (const auto& it : singleKeyRemapCopy)
{
SingleKeyRemapControl::AddNewControlKeyRemapRow(keyRemapTable, keyboardRemapControlObjects, it.first, it.second);
}
// Main Header Apply button
Button applyButton;
applyButton.Content(winrt::box_value(GET_RESOURCE_STRING(IDS_OK_BUTTON)));
applyButton.Style(AccentButtonStyle());
applyButton.MinWidth(KeyboardManagerConstants::HeaderButtonWidth);
cancelButton.MinWidth(KeyboardManagerConstants::HeaderButtonWidth);
header.SetAlignRightWithPanel(cancelButton, true);
header.SetLeftOf(applyButton, cancelButton);
auto ApplyRemappings = [&keyboardManagerState, _hWndEditKeyboardWindow]() {
LoadingAndSavingRemappingHelper::ApplySingleKeyRemappings(keyboardManagerState, SingleKeyRemapControl::singleKeyRemapBuffer, true);
bool saveResult = keyboardManagerState.SaveConfigToFile();
PostMessage(_hWndEditKeyboardWindow, WM_CLOSE, 0, 0);
};
applyButton.Click([&keyboardManagerState, ApplyRemappings, applyButton](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
OnClickAccept(keyboardManagerState, applyButton.XamlRoot(), ApplyRemappings);
});
header.Children().Append(headerText);
header.Children().Append(applyButton);
header.Children().Append(cancelButton);
ScrollViewer scrollViewer;
scrollViewer.VerticalScrollMode(ScrollMode::Enabled);
scrollViewer.HorizontalScrollMode(ScrollMode::Enabled);
scrollViewer.VerticalScrollBarVisibility(ScrollBarVisibility::Auto);
scrollViewer.HorizontalScrollBarVisibility(ScrollBarVisibility::Auto);
// Add remap key button
Windows::UI::Xaml::Controls::Button addRemapKey;
FontIcon plusSymbol;
plusSymbol.FontFamily(Media::FontFamily(L"Segoe MDL2 Assets"));
plusSymbol.Glyph(L"\xE109");
addRemapKey.Content(plusSymbol);
addRemapKey.Margin({ 10, 10, 0, 25 });
addRemapKey.Click([&](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
SingleKeyRemapControl::AddNewControlKeyRemapRow(keyRemapTable, keyboardRemapControlObjects);
// Whenever a remap is added move to the bottom of the screen
scrollViewer.ChangeView(nullptr, scrollViewer.ScrollableHeight(), nullptr);
// Set focus to the first Type Button in the newly added row
UIHelpers::SetFocusOnTypeButtonInLastRow(keyRemapTable, KeyboardManagerConstants::RemapTableColCount);
});
// Set accessible name for the addRemapKey button
addRemapKey.SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_ADD_KEY_REMAP_BUTTON)));
// Add tooltip for add button which would appear on hover
ToolTip addRemapKeytoolTip;
addRemapKeytoolTip.Content(box_value(GET_RESOURCE_STRING(IDS_ADD_KEY_REMAP_BUTTON)));
ToolTipService::SetToolTip(addRemapKey, addRemapKeytoolTip);
// Header and example text at the top of the window
StackPanel helperText;
helperText.Children().Append(keyRemapInfoHeader);
helperText.Children().Append(keyRemapInfoExample);
// Remapping table
StackPanel mappingsPanel;
mappingsPanel.Children().Append(tableHeader);
mappingsPanel.Children().Append(keyRemapTable);
mappingsPanel.Children().Append(addRemapKey);
// Remapping table should be scrollable
scrollViewer.Content(mappingsPanel);
// Creating the Xaml content. xamlContainer is the parent UI element
RelativePanel xamlContainer;
xamlContainer.SetBelow(helperText, header);
xamlContainer.SetBelow(scrollViewer, helperText);
xamlContainer.SetAlignLeftWithPanel(header, true);
xamlContainer.SetAlignRightWithPanel(header, true);
xamlContainer.SetAlignLeftWithPanel(helperText, true);
xamlContainer.SetAlignRightWithPanel(helperText, true);
xamlContainer.SetAlignLeftWithPanel(scrollViewer, true);
xamlContainer.SetAlignRightWithPanel(scrollViewer, true);
xamlContainer.Children().Append(header);
xamlContainer.Children().Append(helperText);
xamlContainer.Children().Append(scrollViewer);
xamlContainer.UpdateLayout();
desktopSource.Content(xamlContainer);
////End XAML Island section
if (_hWndEditKeyboardWindow)
{
ShowWindow(_hWndEditKeyboardWindow, SW_SHOW);
UpdateWindow(_hWndEditKeyboardWindow);
}
// Message loop:
xamlBridge.MessageLoop();
// Reset pointers to nullptr
xamlBridgePtr = nullptr;
hWndXamlIslandEditKeyboardWindow = nullptr;
hwndLock.lock();
hwndEditKeyboardNativeWindow = nullptr;
keyboardManagerState.ResetUIState();
keyboardManagerState.ClearRegisteredKeyDelays();
// Cannot be done in WM_DESTROY because that causes crashes due to fatal app exit
xamlBridge.ClearXamlIslands();
}
void CreateEditKeyboardWindow(HINSTANCE hInst, KeyboardManagerState& keyboardManagerState)
{
// Move implementation into the separate method so resources get destroyed correctly
CreateEditKeyboardWindowImpl(hInst, keyboardManagerState);
// Calling ClearXamlIslands() outside of the message loop is not enough to prevent
// Microsoft.UI.XAML.dll from crashing during deinitialization, see https://github.com/microsoft/PowerToys/issues/10906
Logger::trace("Terminating process {}", GetCurrentProcessId());
Logger::flush();
TerminateProcess(GetCurrentProcess(), 0);
}
inline std::wstring getMessage(UINT messageCode)
{
switch (messageCode)
{
case WM_SIZE:
return L"WM_SIZE";
case WM_NCDESTROY:
return L"WM_NCDESTROY";
default:
return L"";
}
}
LRESULT CALLBACK EditKeyboardWindowProc(HWND hWnd, UINT messageCode, WPARAM wParam, LPARAM lParam)
{
RECT rcClient;
auto message = getMessage(messageCode);
if (message != L"")
{
Logger::trace(L"EditKeyboardWindowProc() messageCode={}", getMessage(messageCode));
}
switch (messageCode)
{
// Resize the XAML window whenever the parent window is painted or resized
case WM_PAINT:
case WM_SIZE:
{
GetClientRect(hWnd, &rcClient);
SetWindowPos(hWndXamlIslandEditKeyboardWindow, 0, rcClient.left, rcClient.top, rcClient.right, rcClient.bottom, SWP_SHOWWINDOW);
}
break;
// To avoid UI elements overlapping on making the window smaller enforce a minimum window size
case WM_GETMINMAXINFO:
{
LPMINMAXINFO lpMMI = (LPMINMAXINFO)lParam;
int minWidth = KeyboardManagerConstants::MinimumEditKeyboardWindowWidth;
int minHeight = KeyboardManagerConstants::MinimumEditKeyboardWindowHeight;
DPIAware::Convert(nullptr, minWidth, minHeight);
lpMMI->ptMinTrackSize.x = minWidth;
lpMMI->ptMinTrackSize.y = minHeight;
}
break;
default:
// If the Xaml Bridge object exists, then use it's message handler to handle keyboard focus operations
if (xamlBridgePtr != nullptr)
{
return xamlBridgePtr->MessageHandler(messageCode, wParam, lParam);
}
else if (messageCode == WM_NCDESTROY)
{
PostQuitMessage(0);
break;
}
return DefWindowProc(hWnd, messageCode, wParam, lParam);
break;
}
return 0;
}
// Function to check if there is already a window active if yes bring to foreground
bool CheckEditKeyboardWindowActive()
{
bool result = false;
std::unique_lock<std::mutex> hwndLock(editKeyboardWindowMutex);
if (hwndEditKeyboardNativeWindow != nullptr)
{
// Check if the window is minimized if yes then restore the window.
if (IsIconic(hwndEditKeyboardNativeWindow))
{
ShowWindow(hwndEditKeyboardNativeWindow, SW_RESTORE);
}
// If there is an already existing window no need to create a new open bring it on foreground.
SetForegroundWindow(hwndEditKeyboardNativeWindow);
result = true;
}
return result;
}
// Function to close any active Edit Keyboard window
void CloseActiveEditKeyboardWindow()
{
std::unique_lock<std::mutex> hwndLock(editKeyboardWindowMutex);
if (hwndEditKeyboardNativeWindow != nullptr)
{
Logger::trace("CloseActiveEditKeyboardWindow()");
PostMessage(hwndEditKeyboardNativeWindow, WM_CLOSE, 0, 0);
}
}

View File

@@ -0,0 +1,11 @@
#pragma once
class KeyboardManagerState;
// Function to create the Edit Keyboard Window
void CreateEditKeyboardWindow(HINSTANCE hInst, KeyboardManagerState& keyboardManagerState);
// Function to check if there is already a window active if yes bring to foreground
bool CheckEditKeyboardWindowActive();
// Function to close any active Edit Keyboard window
void CloseActiveEditKeyboardWindow();

View File

@@ -0,0 +1,432 @@
#include "pch.h"
#include "EditShortcutsWindow.h"
#include <common/Display/dpi_aware.h>
#include <common/utils/EventLocker.h>
#include <KeyboardManagerState.h>
#include <Dialog.h>
#include <ErrorTypes.h>
#include <KeyDropDownControl.h>
#include <LoadingAndSavingRemappingHelper.h>
#include <ShortcutControl.h>
#include <Styles.h>
#include <UIHelpers.h>
#include <XamlBridge.h>
#include <common/utils/winapi_error.h>
using namespace winrt::Windows::Foundation;
LRESULT CALLBACK EditShortcutsWindowProc(HWND, UINT, WPARAM, LPARAM);
// This Hwnd will be the window handler for the Xaml Island: A child window that contains Xaml.
HWND hWndXamlIslandEditShortcutsWindow = nullptr;
// This variable is used to check if window registration has been done to avoid repeated registration leading to an error.
bool isEditShortcutsWindowRegistrationCompleted = false;
// Holds the native window handle of EditShortcuts Window.
HWND hwndEditShortcutsNativeWindow = nullptr;
std::mutex editShortcutsWindowMutex;
// Stores a pointer to the Xaml Bridge object so that it can be accessed from the window procedure
static XamlBridge* xamlBridgePtr = nullptr;
static IAsyncAction OnClickAccept(
KeyboardManagerState& keyboardManagerState,
XamlRoot root,
std::function<void()> ApplyRemappings)
{
KeyboardManagerHelper::ErrorType isSuccess = LoadingAndSavingRemappingHelper::CheckIfRemappingsAreValid(ShortcutControl::shortcutRemapBuffer);
if (isSuccess != KeyboardManagerHelper::ErrorType::NoError)
{
if (!co_await Dialog::PartialRemappingConfirmationDialog(root, GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_PARTIALCONFIRMATIONDIALOGTITLE)))
{
co_return;
}
}
ApplyRemappings();
}
// Function to create the Edit Shortcuts Window
inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KeyboardManagerState& keyboardManagerState)
{
Logger::trace("CreateEditShortcutsWindowImpl()");
auto locker = EventLocker::Get(KeyboardManagerConstants::EditorWindowEventName.c_str());
if (!locker.has_value())
{
Logger::error(L"Failed to lock event {}. {}", KeyboardManagerConstants::EditorWindowEventName, get_last_error_or_default(GetLastError()));
}
Logger::trace(L"Signaled {} event to suspend the KBM engine", KeyboardManagerConstants::EditorWindowEventName);
// Window Registration
const wchar_t szWindowClass[] = L"EditShortcutsWindow";
if (!isEditShortcutsWindowRegistrationCompleted)
{
WNDCLASSEX windowClass = {};
windowClass.cbSize = sizeof(WNDCLASSEX);
windowClass.lpfnWndProc = EditShortcutsWindowProc;
windowClass.hInstance = hInst;
windowClass.lpszClassName = szWindowClass;
windowClass.hbrBackground = (HBRUSH)(COLOR_WINDOW);
windowClass.hIcon = (HICON)LoadImageW(
windowClass.hInstance,
MAKEINTRESOURCE(IDS_KEYBOARDMANAGER_ICON),
IMAGE_ICON,
48,
48,
LR_DEFAULTCOLOR);
if (RegisterClassEx(&windowClass) == NULL)
{
MessageBox(NULL, GET_RESOURCE_STRING(IDS_REGISTERCLASSFAILED_ERRORMESSAGE).c_str(), GET_RESOURCE_STRING(IDS_REGISTERCLASSFAILED_ERRORTITLE).c_str(), NULL);
return;
}
isEditShortcutsWindowRegistrationCompleted = true;
}
// Find coordinates of the screen where the settings window is placed.
RECT desktopRect = UIHelpers::GetForegroundWindowDesktopRect();
// Calculate DPI dependent window size
int windowWidth = KeyboardManagerConstants::DefaultEditShortcutsWindowWidth;
int windowHeight = KeyboardManagerConstants::DefaultEditShortcutsWindowHeight;
DPIAware::Convert(nullptr, windowWidth, windowHeight);
// Window Creation
HWND _hWndEditShortcutsWindow = CreateWindow(
szWindowClass,
GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_WINDOWNAME).c_str(),
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MAXIMIZEBOX,
((desktopRect.right + desktopRect.left) / 2) - (windowWidth / 2),
((desktopRect.bottom + desktopRect.top) / 2) - (windowHeight / 2),
windowWidth,
windowHeight,
NULL,
NULL,
hInst,
NULL);
if (_hWndEditShortcutsWindow == NULL)
{
MessageBox(NULL, GET_RESOURCE_STRING(IDS_CREATEWINDOWFAILED_ERRORMESSAGE).c_str(), GET_RESOURCE_STRING(IDS_CREATEWINDOWFAILED_ERRORTITLE).c_str(), NULL);
return;
}
// Ensures the window is in foreground on first startup. If this is not done, the window appears behind because the thread is not on the foreground.
if (_hWndEditShortcutsWindow)
{
SetForegroundWindow(_hWndEditShortcutsWindow);
}
// Store the newly created Edit Shortcuts window's handle.
std::unique_lock<std::mutex> hwndLock(editShortcutsWindowMutex);
hwndEditShortcutsNativeWindow = _hWndEditShortcutsWindow;
hwndLock.unlock();
// Create the xaml bridge object
XamlBridge xamlBridge(_hWndEditShortcutsWindow);
// DesktopSource needs to be declared before the RelativePanel xamlContainer object to avoid errors
winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource desktopSource;
// Create the desktop window xaml source object and set its content
hWndXamlIslandEditShortcutsWindow = xamlBridge.InitDesktopWindowsXamlSource(desktopSource);
// Set the pointer to the xaml bridge object
xamlBridgePtr = &xamlBridge;
// Header for the window
Windows::UI::Xaml::Controls::RelativePanel header;
header.Margin({ 10, 10, 10, 30 });
// Header text
TextBlock headerText;
headerText.Text(GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_WINDOWNAME));
headerText.FontSize(30);
headerText.Margin({ 0, 0, 0, 0 });
header.SetAlignLeftWithPanel(headerText, true);
// Cancel button
Button cancelButton;
cancelButton.Content(winrt::box_value(GET_RESOURCE_STRING(IDS_CANCEL_BUTTON)));
cancelButton.Margin({ 10, 0, 0, 0 });
cancelButton.Click([&](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
// Close the window since settings do not need to be saved
PostMessage(_hWndEditShortcutsWindow, WM_CLOSE, 0, 0);
});
// Text block for information about remap key section.
TextBlock shortcutRemapInfoHeader;
shortcutRemapInfoHeader.Text(GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_INFO));
shortcutRemapInfoHeader.Margin({ 10, 0, 0, 10 });
shortcutRemapInfoHeader.FontWeight(Text::FontWeights::SemiBold());
shortcutRemapInfoHeader.TextWrapping(TextWrapping::Wrap);
TextBlock shortcutRemapInfoExample;
shortcutRemapInfoExample.Text(GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_INFOEXAMPLE));
shortcutRemapInfoExample.Margin({ 10, 0, 0, 20 });
shortcutRemapInfoExample.FontStyle(Text::FontStyle::Italic);
shortcutRemapInfoExample.TextWrapping(TextWrapping::Wrap);
// Table to display the shortcuts
StackPanel shortcutTable;
// First header textblock in the header row of the shortcut table
TextBlock originalShortcutHeader;
originalShortcutHeader.Text(GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_SOURCEHEADER));
originalShortcutHeader.FontWeight(Text::FontWeights::Bold());
// Second header textblock in the header row of the shortcut table
TextBlock newShortcutHeader;
newShortcutHeader.Text(GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_TARGETHEADER));
newShortcutHeader.FontWeight(Text::FontWeights::Bold());
// Third header textblock in the header row of the shortcut table
TextBlock targetAppHeader;
targetAppHeader.Text(GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_TARGETAPPHEADER));
targetAppHeader.FontWeight(Text::FontWeights::Bold());
targetAppHeader.HorizontalAlignment(HorizontalAlignment::Center);
StackPanel tableHeader = StackPanel();
tableHeader.Orientation(Orientation::Horizontal);
tableHeader.Margin({ 10, 0, 0, 10 });
auto originalShortcutContainer = UIHelpers::GetWrapped(originalShortcutHeader, KeyboardManagerConstants::ShortcutOriginColumnWidth + (double)KeyboardManagerConstants::ShortcutArrowColumnWidth);
tableHeader.Children().Append(originalShortcutContainer.as<FrameworkElement>());
auto newShortcutHeaderContainer = UIHelpers::GetWrapped(newShortcutHeader, KeyboardManagerConstants::ShortcutTargetColumnWidth);
tableHeader.Children().Append(newShortcutHeaderContainer.as<FrameworkElement>());
tableHeader.Children().Append(targetAppHeader);
// Store handle of edit shortcuts window
ShortcutControl::editShortcutsWindowHandle = _hWndEditShortcutsWindow;
// Store keyboard manager state
ShortcutControl::keyboardManagerState = &keyboardManagerState;
KeyDropDownControl::keyboardManagerState = &keyboardManagerState;
// Clear the shortcut remap buffer
ShortcutControl::shortcutRemapBuffer.clear();
// Vector to store dynamically allocated control objects to avoid early destruction
std::vector<std::vector<std::unique_ptr<ShortcutControl>>> keyboardRemapControlObjects;
// Set keyboard manager UI state so that shortcut remaps are not applied while on this window
keyboardManagerState.SetUIState(KeyboardManagerUIState::EditShortcutsWindowActivated, _hWndEditShortcutsWindow);
// Load existing os level shortcuts into UI
// Create copy of the remaps to avoid concurrent access
ShortcutRemapTable osLevelShortcutReMapCopy = keyboardManagerState.osLevelShortcutReMap;
for (const auto& it : osLevelShortcutReMapCopy)
{
ShortcutControl::AddNewShortcutControlRow(shortcutTable, keyboardRemapControlObjects, it.first, it.second.targetShortcut);
}
// Load existing app-specific shortcuts into UI
// Create copy of the remaps to avoid concurrent access
AppSpecificShortcutRemapTable appSpecificShortcutReMapCopy = keyboardManagerState.appSpecificShortcutReMap;
// Iterate through all the apps
for (const auto& itApp : appSpecificShortcutReMapCopy)
{
// Iterate through shortcuts for each app
for (const auto& itShortcut : itApp.second)
{
ShortcutControl::AddNewShortcutControlRow(shortcutTable, keyboardRemapControlObjects, itShortcut.first, itShortcut.second.targetShortcut, itApp.first);
}
}
// Apply button
Button applyButton;
applyButton.Content(winrt::box_value(GET_RESOURCE_STRING(IDS_OK_BUTTON)));
applyButton.Style(AccentButtonStyle());
applyButton.MinWidth(KeyboardManagerConstants::HeaderButtonWidth);
cancelButton.MinWidth(KeyboardManagerConstants::HeaderButtonWidth);
header.SetAlignRightWithPanel(cancelButton, true);
header.SetLeftOf(applyButton, cancelButton);
auto ApplyRemappings = [&keyboardManagerState, _hWndEditShortcutsWindow]() {
LoadingAndSavingRemappingHelper::ApplyShortcutRemappings(keyboardManagerState, ShortcutControl::shortcutRemapBuffer, true);
bool saveResult = keyboardManagerState.SaveConfigToFile();
PostMessage(_hWndEditShortcutsWindow, WM_CLOSE, 0, 0);
};
applyButton.Click([&keyboardManagerState, applyButton, ApplyRemappings](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
OnClickAccept(keyboardManagerState, applyButton.XamlRoot(), ApplyRemappings);
});
header.Children().Append(headerText);
header.Children().Append(applyButton);
header.Children().Append(cancelButton);
ScrollViewer scrollViewer;
scrollViewer.VerticalScrollMode(ScrollMode::Enabled);
scrollViewer.HorizontalScrollMode(ScrollMode::Enabled);
scrollViewer.VerticalScrollBarVisibility(ScrollBarVisibility::Auto);
scrollViewer.HorizontalScrollBarVisibility(ScrollBarVisibility::Auto);
// Add shortcut button
Windows::UI::Xaml::Controls::Button addShortcut;
FontIcon plusSymbol;
plusSymbol.FontFamily(Xaml::Media::FontFamily(L"Segoe MDL2 Assets"));
plusSymbol.Glyph(L"\xE109");
addShortcut.Content(plusSymbol);
addShortcut.Margin({ 10, 10, 0, 25 });
addShortcut.Click([&](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
ShortcutControl::AddNewShortcutControlRow(shortcutTable, keyboardRemapControlObjects);
// Whenever a remap is added move to the bottom of the screen
scrollViewer.ChangeView(nullptr, scrollViewer.ScrollableHeight(), nullptr);
// Set focus to the first Type Button in the newly added row
UIHelpers::SetFocusOnTypeButtonInLastRow(shortcutTable, KeyboardManagerConstants::ShortcutTableColCount);
});
// Set accessible name for the add shortcut button
addShortcut.SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_ADD_SHORTCUT_BUTTON)));
// Add tooltip for add button which would appear on hover
ToolTip addShortcuttoolTip;
addShortcuttoolTip.Content(box_value(GET_RESOURCE_STRING(IDS_ADD_SHORTCUT_BUTTON)));
ToolTipService::SetToolTip(addShortcut, addShortcuttoolTip);
// Header and example text at the top of the window
StackPanel helperText;
helperText.Children().Append(shortcutRemapInfoHeader);
helperText.Children().Append(shortcutRemapInfoExample);
// Remapping table
StackPanel mappingsPanel;
mappingsPanel.Children().Append(tableHeader);
mappingsPanel.Children().Append(shortcutTable);
mappingsPanel.Children().Append(addShortcut);
// Remapping table should be scrollable
scrollViewer.Content(mappingsPanel);
RelativePanel xamlContainer;
xamlContainer.SetBelow(helperText, header);
xamlContainer.SetBelow(scrollViewer, helperText);
xamlContainer.SetAlignLeftWithPanel(header, true);
xamlContainer.SetAlignRightWithPanel(header, true);
xamlContainer.SetAlignLeftWithPanel(helperText, true);
xamlContainer.SetAlignRightWithPanel(helperText, true);
xamlContainer.SetAlignLeftWithPanel(scrollViewer, true);
xamlContainer.SetAlignRightWithPanel(scrollViewer, true);
xamlContainer.Children().Append(header);
xamlContainer.Children().Append(helperText);
xamlContainer.Children().Append(scrollViewer);
xamlContainer.UpdateLayout();
desktopSource.Content(xamlContainer);
////End XAML Island section
if (_hWndEditShortcutsWindow)
{
ShowWindow(_hWndEditShortcutsWindow, SW_SHOW);
UpdateWindow(_hWndEditShortcutsWindow);
}
// Message loop:
xamlBridge.MessageLoop();
// Reset pointers to nullptr
xamlBridgePtr = nullptr;
hWndXamlIslandEditShortcutsWindow = nullptr;
hwndLock.lock();
hwndEditShortcutsNativeWindow = nullptr;
keyboardManagerState.ResetUIState();
keyboardManagerState.ClearRegisteredKeyDelays();
// Cannot be done in WM_DESTROY because that causes crashes due to fatal app exit
xamlBridge.ClearXamlIslands();
}
void CreateEditShortcutsWindow(HINSTANCE hInst, KeyboardManagerState& keyboardManagerState)
{
// Move implementation into the separate method so resources get destroyed correctly
CreateEditShortcutsWindowImpl(hInst, keyboardManagerState);
// Calling ClearXamlIslands() outside of the message loop is not enough to prevent
// Microsoft.UI.XAML.dll from crashing during deinitialization, see https://github.com/microsoft/PowerToys/issues/10906
Logger::trace("Terminating process {}", GetCurrentProcessId());
Logger::flush();
TerminateProcess(GetCurrentProcess(), 0);
}
LRESULT CALLBACK EditShortcutsWindowProc(HWND hWnd, UINT messageCode, WPARAM wParam, LPARAM lParam)
{
RECT rcClient;
switch (messageCode)
{
// Resize the XAML window whenever the parent window is painted or resized
case WM_PAINT:
case WM_SIZE:
{
GetClientRect(hWnd, &rcClient);
SetWindowPos(hWndXamlIslandEditShortcutsWindow, 0, rcClient.left, rcClient.top, rcClient.right, rcClient.bottom, SWP_SHOWWINDOW);
}
break;
// To avoid UI elements overlapping on making the window smaller enforce a minimum window size
case WM_GETMINMAXINFO:
{
LPMINMAXINFO lpMMI = (LPMINMAXINFO)lParam;
int minWidth = KeyboardManagerConstants::MinimumEditShortcutsWindowWidth;
int minHeight = KeyboardManagerConstants::MinimumEditShortcutsWindowHeight;
DPIAware::Convert(nullptr, minWidth, minHeight);
lpMMI->ptMinTrackSize.x = minWidth;
lpMMI->ptMinTrackSize.y = minHeight;
}
break;
default:
// If the Xaml Bridge object exists, then use it's message handler to handle keyboard focus operations
if (xamlBridgePtr != nullptr)
{
return xamlBridgePtr->MessageHandler(messageCode, wParam, lParam);
}
else if (messageCode == WM_NCDESTROY)
{
PostQuitMessage(0);
break;
}
return DefWindowProc(hWnd, messageCode, wParam, lParam);
break;
}
return 0;
}
// Function to check if there is already a window active if yes bring to foreground
bool CheckEditShortcutsWindowActive()
{
bool result = false;
std::unique_lock<std::mutex> hwndLock(editShortcutsWindowMutex);
if (hwndEditShortcutsNativeWindow != nullptr)
{
// Check if the window is minimized if yes then restore the window.
if (IsIconic(hwndEditShortcutsNativeWindow))
{
ShowWindow(hwndEditShortcutsNativeWindow, SW_RESTORE);
}
// If there is an already existing window no need to create a new open bring it on foreground.
SetForegroundWindow(hwndEditShortcutsNativeWindow);
result = true;
}
return result;
}
// Function to close any active Edit Shortcuts window
void CloseActiveEditShortcutsWindow()
{
std::unique_lock<std::mutex> hwndLock(editShortcutsWindowMutex);
if (hwndEditShortcutsNativeWindow != nullptr)
{
PostMessage(hwndEditShortcutsNativeWindow, WM_CLOSE, 0, 0);
}
}

View File

@@ -0,0 +1,11 @@
#pragma once
class KeyboardManagerState;
// Function to create the Edit Shortcuts Window
void CreateEditShortcutsWindow(HINSTANCE hInst, KeyboardManagerState& keyboardManagerState);
// Function to check if there is already a window active if yes bring to foreground
bool CheckEditShortcutsWindowActive();
// Function to close any active Edit Shortcuts window
void CloseActiveEditShortcutsWindow();

View File

@@ -0,0 +1,419 @@
#include "pch.h"
#include "KeyDropDownControl.h"
#include <common/interop/shared_constants.h>
#include <KeyboardManagerState.h>
#include <BufferValidationHelpers.h>
#include <KeyboardManagerEditorStrings.h>
#include <ErrorTypes.h>
#include <UIHelpers.h>
// Initialized to null
KeyboardManagerState* KeyDropDownControl::keyboardManagerState = nullptr;
// Get selected value of dropdown or -1 if nothing is selected
DWORD KeyDropDownControl::GetSelectedValue(ComboBox comboBox)
{
auto dataContext = comboBox.SelectedValue();
if (!dataContext)
{
return -1;
}
auto value = winrt::unbox_value<hstring>(dataContext);
return stoi(std::wstring(value));
}
void KeyDropDownControl::SetSelectedValue(std::wstring value)
{
this->dropDown.as<ComboBox>().SelectedValue(winrt::box_value(value));
}
// Get keys name list depending if Disable is in dropdown
std::vector<std::pair<DWORD, std::wstring>> KeyDropDownControl::GetKeyList(bool isShortcut, bool renderDisable)
{
auto list = keyboardManagerState->keyboardMap.GetKeyNameList(isShortcut);
if (renderDisable)
{
list.insert(list.begin(), { CommonSharedConstants::VK_DISABLED, keyboardManagerState->keyboardMap.GetKeyName(CommonSharedConstants::VK_DISABLED) });
}
return list;
}
// Function to set properties apart from the SelectionChanged event handler
void KeyDropDownControl::SetDefaultProperties(bool isShortcut, bool renderDisable)
{
dropDown = ComboBox();
warningFlyout = Flyout();
warningMessage = TextBlock();
if (!isShortcut)
{
dropDown.as<ComboBox>().Width(KeyboardManagerConstants::RemapTableDropDownWidth);
}
else
{
dropDown.as<ComboBox>().Width(KeyboardManagerConstants::ShortcutTableDropDownWidth);
}
dropDown.as<ComboBox>().MaxDropDownHeight(KeyboardManagerConstants::TableDropDownHeight);
// Initialise layout attribute
previousLayout = GetKeyboardLayout(0);
dropDown.as<ComboBox>().SelectedValuePath(L"DataContext");
dropDown.as<ComboBox>().ItemsSource(UIHelpers::ToBoxValue(GetKeyList(isShortcut, renderDisable)));
// drop down open handler - to reload the items with the latest layout
dropDown.as<ComboBox>().DropDownOpened([&, isShortcut](winrt::Windows::Foundation::IInspectable const& sender, auto args) {
ComboBox currentDropDown = sender.as<ComboBox>();
CheckAndUpdateKeyboardLayout(currentDropDown, isShortcut, renderDisable);
});
// Attach flyout to the drop down
warningFlyout.as<Flyout>().Content(warningMessage.as<TextBlock>());
// Enable narrator for Content of FlyoutPresenter. For details https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.flyout?view=winrt-19041#accessibility
Style style = Style(winrt::xaml_typename<FlyoutPresenter>());
style.Setters().Append(Setter(Windows::UI::Xaml::Controls::Control::IsTabStopProperty(), winrt::box_value(true)));
style.Setters().Append(Setter(Windows::UI::Xaml::Controls::Control::TabNavigationProperty(), winrt::box_value(Windows::UI::Xaml::Input::KeyboardNavigationMode::Cycle)));
warningFlyout.as<Flyout>().FlyoutPresenterStyle(style);
dropDown.as<ComboBox>().ContextFlyout().SetAttachedFlyout((FrameworkElement)dropDown.as<ComboBox>(), warningFlyout.as<Flyout>());
// To set the accessible name of the combo-box (by default index 1)
SetAccessibleNameForComboBox(dropDown.as<ComboBox>(), 1);
}
// Function to set accessible name for combobox
void KeyDropDownControl::SetAccessibleNameForComboBox(ComboBox dropDown, int index)
{
// Display name with drop down index (where this indexing will start from 1) - Used by narrator
dropDown.SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_KEY_DROPDOWN_COMBOBOX) + L" " + std::to_wstring(index)));
}
// Function to check if the layout has changed and accordingly update the drop down list
void KeyDropDownControl::CheckAndUpdateKeyboardLayout(ComboBox currentDropDown, bool isShortcut, bool renderDisable)
{
// Get keyboard layout for current thread
HKL layout = GetKeyboardLayout(0);
// Check if the layout has changed
if (previousLayout != layout)
{
currentDropDown.ItemsSource(UIHelpers::ToBoxValue(GetKeyList(isShortcut, renderDisable)));
previousLayout = layout;
}
}
// Function to set selection handler for single key remap drop down. Needs to be called after the constructor since the singleKeyControl StackPanel is null if called in the constructor
void KeyDropDownControl::SetSelectionHandler(StackPanel& table, StackPanel row, int colIndex, RemapBuffer& singleKeyRemapBuffer)
{
// drop down selection handler
auto onSelectionChange = [&, table, row, colIndex](winrt::Windows::Foundation::IInspectable const& sender) {
uint32_t rowIndex = -1;
if (!table.Children().IndexOf(row, rowIndex))
{
return;
}
ComboBox currentDropDown = sender.as<ComboBox>();
int selectedKeyCode = GetSelectedValue(currentDropDown);
// Validate current remap selection
KeyboardManagerHelper::ErrorType errorType = BufferValidationHelpers::ValidateAndUpdateKeyBufferElement(rowIndex, colIndex, selectedKeyCode, singleKeyRemapBuffer);
// If there is an error set the warning flyout
if (errorType != KeyboardManagerHelper::ErrorType::NoError)
{
SetDropDownError(currentDropDown, KeyboardManagerEditorStrings::GetErrorMessage(errorType));
}
};
// Rather than on every selection change (which gets triggered on searching as well) we set the handler only when the drop down is closed
dropDown.as<ComboBox>().DropDownClosed([onSelectionChange](winrt::Windows::Foundation::IInspectable const& sender, auto const& args) {
onSelectionChange(sender);
});
// We check if the selection changed was triggered while the drop down was closed. This is required to handle Type key, initial loading of remaps and if the user just types in the combo box without opening it
dropDown.as<ComboBox>().SelectionChanged([onSelectionChange](winrt::Windows::Foundation::IInspectable const& sender, SelectionChangedEventArgs const& args) {
ComboBox currentDropDown = sender.as<ComboBox>();
if (!currentDropDown.IsDropDownOpen())
{
onSelectionChange(sender);
}
});
}
std::pair<KeyboardManagerHelper::ErrorType, int> KeyDropDownControl::ValidateShortcutSelection(StackPanel table, StackPanel row, StackPanel parent, int colIndex, RemapBuffer& shortcutRemapBuffer, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, TextBox targetApp, bool isHybridControl, bool isSingleKeyWindow)
{
ComboBox currentDropDown = dropDown.as<ComboBox>();
uint32_t dropDownIndex = -1;
bool dropDownFound = parent.Children().IndexOf(currentDropDown, dropDownIndex);
std::pair<KeyboardManagerHelper::ErrorType, BufferValidationHelpers::DropDownAction> validationResult = std::make_pair(KeyboardManagerHelper::ErrorType::NoError, BufferValidationHelpers::DropDownAction::NoAction);
uint32_t rowIndex;
bool controlIindexFound = table.Children().IndexOf(row, rowIndex);
if (controlIindexFound)
{
std::vector<int32_t> selectedCodes = GetSelectedCodesFromStackPanel(parent);
std::wstring appName;
if (targetApp != nullptr)
{
appName = targetApp.Text().c_str();
}
// Validate shortcut element
validationResult = BufferValidationHelpers::ValidateShortcutBufferElement(rowIndex, colIndex, dropDownIndex, selectedCodes, appName, isHybridControl, shortcutRemapBuffer, dropDownFound);
// Add or clear unused drop downs
if (validationResult.second == BufferValidationHelpers::DropDownAction::AddDropDown)
{
AddDropDown(table, row, parent, colIndex, shortcutRemapBuffer, keyDropDownControlObjects, targetApp, isHybridControl, isSingleKeyWindow);
}
else if (validationResult.second == BufferValidationHelpers::DropDownAction::ClearUnusedDropDowns)
{
// remove all the drop downs after the current index (accessible names do not have to be updated since drop downs at the end of the list are getting removed)
int elementsToBeRemoved = parent.Children().Size() - dropDownIndex - 1;
for (int i = 0; i < elementsToBeRemoved; i++)
{
parent.Children().RemoveAtEnd();
keyDropDownControlObjects.erase(keyDropDownControlObjects.end() - 1);
}
parent.UpdateLayout();
}
// If ignore key to shortcut warning flag is true and it is a hybrid control in SingleKeyRemapControl, then skip MapToSameKey error
if (isHybridControl && isSingleKeyWindow && ignoreKeyToShortcutWarning && (validationResult.first == KeyboardManagerHelper::ErrorType::MapToSameKey))
{
validationResult.first = KeyboardManagerHelper::ErrorType::NoError;
}
// If the remapping is invalid display an error message
if (validationResult.first != KeyboardManagerHelper::ErrorType::NoError)
{
SetDropDownError(currentDropDown, KeyboardManagerEditorStrings::GetErrorMessage(validationResult.first));
}
// Handle None case if there are no other errors
else if (validationResult.second == BufferValidationHelpers::DropDownAction::DeleteDropDown)
{
// Update accessible names for drop downs appearing after the deleted one
for (uint32_t i = dropDownIndex + 1; i < keyDropDownControlObjects.size(); i++)
{
// Update accessible name (row index will become i-1 for this element, so the display name would be i (display name indexing from 1)
SetAccessibleNameForComboBox(keyDropDownControlObjects[i]->GetComboBox(), i);
}
parent.Children().RemoveAt(dropDownIndex);
// delete drop down control object from the vector so that it can be destructed
keyDropDownControlObjects.erase(keyDropDownControlObjects.begin() + dropDownIndex);
parent.UpdateLayout();
}
}
// Reset ignoreKeyToShortcutWarning
if (ignoreKeyToShortcutWarning)
{
ignoreKeyToShortcutWarning = false;
}
return std::make_pair(validationResult.first, rowIndex);
}
// Function to set selection handler for shortcut drop down. Needs to be called after the constructor since the shortcutControl StackPanel is null if called in the constructor
void KeyDropDownControl::SetSelectionHandler(StackPanel& table, StackPanel row, StackPanel parent, int colIndex, RemapBuffer& shortcutRemapBuffer, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, TextBox& targetApp, bool isHybridControl, bool isSingleKeyWindow)
{
auto onSelectionChange = [&, table, row, colIndex, parent, targetApp, isHybridControl, isSingleKeyWindow](winrt::Windows::Foundation::IInspectable const& sender) {
std::pair<KeyboardManagerHelper::ErrorType, int> validationResult = ValidateShortcutSelection(table, row, parent, colIndex, shortcutRemapBuffer, keyDropDownControlObjects, targetApp, isHybridControl, isSingleKeyWindow);
// Check if the drop down row index was identified from the return value of validateSelection
if (validationResult.second != -1)
{
// If an error occurred
if (validationResult.first != KeyboardManagerHelper::ErrorType::NoError)
{
// Validate all the drop downs
ValidateShortcutFromDropDownList(table, row, parent, colIndex, shortcutRemapBuffer, keyDropDownControlObjects, targetApp, isHybridControl, isSingleKeyWindow);
}
// Reset the buffer based on the new selected drop down items. Use static key code list since the KeyDropDownControl object might be deleted
std::vector<int32_t> selectedKeyCodes = GetSelectedCodesFromStackPanel(parent);
if (!isHybridControl)
{
std::get<Shortcut>(shortcutRemapBuffer[validationResult.second].first[colIndex]).SetKeyCodes(selectedKeyCodes);
}
else
{
// If exactly one key is selected consider it to be a key remap
if (GetNumberOfSelectedKeys(selectedKeyCodes) == 1)
{
shortcutRemapBuffer[validationResult.second].first[colIndex] = selectedKeyCodes[0];
}
else
{
Shortcut tempShortcut;
tempShortcut.SetKeyCodes(selectedKeyCodes);
// Assign instead of setting the value in the buffer since the previous value may not be a Shortcut
shortcutRemapBuffer[validationResult.second].first[colIndex] = tempShortcut;
}
}
if (targetApp != nullptr)
{
std::wstring newText = targetApp.Text().c_str();
std::wstring lowercaseDefAppName = KeyboardManagerEditorStrings::DefaultAppName;
std::transform(newText.begin(), newText.end(), newText.begin(), towlower);
std::transform(lowercaseDefAppName.begin(), lowercaseDefAppName.end(), lowercaseDefAppName.begin(), towlower);
if (newText == lowercaseDefAppName)
{
shortcutRemapBuffer[validationResult.second].second = L"";
}
else
{
shortcutRemapBuffer[validationResult.second].second = targetApp.Text().c_str();
}
}
}
// If the user searches for a key the selection handler gets invoked however if they click away it reverts back to the previous state. This can result in dangling references to added drop downs which were then reset.
// We handle this by removing the drop down if it no longer a child of the parent
for (long long i = keyDropDownControlObjects.size() - 1; i >= 0; i--)
{
uint32_t index;
bool found = parent.Children().IndexOf(keyDropDownControlObjects[i]->GetComboBox(), index);
if (!found)
{
keyDropDownControlObjects.erase(keyDropDownControlObjects.begin() + i);
}
}
};
// Rather than on every selection change (which gets triggered on searching as well) we set the handler only when the drop down is closed
dropDown.as<ComboBox>().DropDownClosed([onSelectionChange](winrt::Windows::Foundation::IInspectable const& sender, auto const& args) {
onSelectionChange(sender);
});
// We check if the selection changed was triggered while the drop down was closed. This is required to handle Type key, initial loading of remaps and if the user just types in the combo box without opening it
dropDown.as<ComboBox>().SelectionChanged([onSelectionChange](winrt::Windows::Foundation::IInspectable const& sender, SelectionChangedEventArgs const& args) {
ComboBox currentDropDown = sender.as<ComboBox>();
if (!currentDropDown.IsDropDownOpen())
{
onSelectionChange(sender);
}
});
}
// Function to return the combo box element of the drop down
ComboBox KeyDropDownControl::GetComboBox()
{
return dropDown.as<ComboBox>();
}
// Function to add a drop down to the shortcut stack panel
void KeyDropDownControl::AddDropDown(StackPanel& table, StackPanel row, StackPanel parent, const int colIndex, RemapBuffer& shortcutRemapBuffer, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, TextBox targetApp, bool isHybridControl, bool isSingleKeyWindow, bool ignoreWarning)
{
keyDropDownControlObjects.emplace_back(std::make_unique<KeyDropDownControl>(true, ignoreWarning, colIndex == 1));
parent.Children().Append(keyDropDownControlObjects[keyDropDownControlObjects.size() - 1]->GetComboBox());
uint32_t index;
bool found = table.Children().IndexOf(row, index);
keyDropDownControlObjects[keyDropDownControlObjects.size() - 1]->SetSelectionHandler(table, row, parent, colIndex, shortcutRemapBuffer, keyDropDownControlObjects, targetApp, isHybridControl, isSingleKeyWindow);
parent.UpdateLayout();
// Update accessible name
SetAccessibleNameForComboBox(keyDropDownControlObjects[keyDropDownControlObjects.size() - 1]->GetComboBox(), (int)keyDropDownControlObjects.size());
}
// Function to get the list of key codes from the shortcut combo box stack panel
std::vector<int32_t> KeyDropDownControl::GetSelectedCodesFromStackPanel(StackPanel parent)
{
std::vector<int32_t> selectedKeyCodes;
// Get selected indices for each drop down
for (int i = 0; i < (int)parent.Children().Size(); i++)
{
ComboBox ItDropDown = parent.Children().GetAt(i).as<ComboBox>();
selectedKeyCodes.push_back(GetSelectedValue(ItDropDown));
}
return selectedKeyCodes;
}
// Function for validating the selection of shortcuts for all the associated drop downs
void KeyDropDownControl::ValidateShortcutFromDropDownList(StackPanel table, StackPanel row, StackPanel parent, int colIndex, RemapBuffer& shortcutRemapBuffer, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, TextBox targetApp, bool isHybridControl, bool isSingleKeyWindow)
{
// Iterate over all drop downs from left to right in that row/col and validate if there is an error in any of the drop downs. After this the state should be error-free (if it is a valid shortcut)
for (int i = 0; i < keyDropDownControlObjects.size(); i++)
{
// Check for errors only if the current selection is a valid shortcut
std::vector<int32_t> selectedKeyCodes = GetSelectedCodesFromStackPanel(parent);
KeyShortcutUnion currentShortcut;
if (GetNumberOfSelectedKeys(selectedKeyCodes) == 1 && isHybridControl)
{
currentShortcut = selectedKeyCodes[0];
}
else
{
Shortcut temp;
temp.SetKeyCodes(selectedKeyCodes);
currentShortcut = temp;
}
// If the key/shortcut is valid and that drop down is not empty
if (((currentShortcut.index() == 0 && std::get<DWORD>(currentShortcut) != NULL) || (currentShortcut.index() == 1 && std::get<Shortcut>(currentShortcut).IsValidShortcut())) && GetSelectedValue(keyDropDownControlObjects[i]->GetComboBox()) != -1)
{
keyDropDownControlObjects[i]->ValidateShortcutSelection(table, row, parent, colIndex, shortcutRemapBuffer, keyDropDownControlObjects, targetApp, isHybridControl, isSingleKeyWindow);
}
}
}
// Function to set the warning message
void KeyDropDownControl::SetDropDownError(ComboBox currentDropDown, hstring message)
{
currentDropDown.SelectedIndex(-1);
warningMessage.as<TextBlock>().Text(message);
currentDropDown.ContextFlyout().ShowAttachedFlyout((FrameworkElement)dropDown.as<ComboBox>());
}
// Function to add a shortcut to the UI control as combo boxes
void KeyDropDownControl::AddShortcutToControl(Shortcut shortcut, StackPanel table, StackPanel parent, KeyboardManagerState& keyboardManagerState, const int colIndex, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, RemapBuffer& remapBuffer, StackPanel row, TextBox targetApp, bool isHybridControl, bool isSingleKeyWindow)
{
// Delete the existing drop down menus
parent.Children().Clear();
// Remove references to the old drop down objects to destroy them
keyDropDownControlObjects.clear();
std::vector<DWORD> shortcutKeyCodes = shortcut.GetKeyCodes();
if (shortcutKeyCodes.size() != 0)
{
bool ignoreWarning = false;
// If more than one key is to be added, ignore a shortcut to key warning on partially entering the remapping
if (shortcutKeyCodes.size() > 1)
{
ignoreWarning = true;
}
KeyDropDownControl::AddDropDown(table, row, parent, colIndex, remapBuffer, keyDropDownControlObjects, targetApp, isHybridControl, isSingleKeyWindow, ignoreWarning);
for (int i = 0; i < shortcutKeyCodes.size(); i++)
{
// New drop down gets added automatically when the SelectedValue(key code) is set
if (i < (int)parent.Children().Size())
{
ComboBox currentDropDown = parent.Children().GetAt(i).as<ComboBox>();
currentDropDown.SelectedValue(winrt::box_value(std::to_wstring(shortcutKeyCodes[i])));
}
}
}
parent.UpdateLayout();
}
// Get number of selected keys. Do not count -1 and 0 values as they stand for Not selected and None
int KeyDropDownControl::GetNumberOfSelectedKeys(std::vector<int32_t> keyCodes)
{
return (int)std::count_if(keyCodes.begin(), keyCodes.end(), [](int32_t a) { return a != -1 && a != 0; });
}

View File

@@ -0,0 +1,105 @@
#pragma once
#include <Shortcut.h>
class KeyboardManagerState;
namespace winrt::Windows
{
namespace Foundation
{
struct hstring;
}
namespace UI::Xaml::Controls
{
struct StackPanel;
struct ComboBox;
struct Flyout;
struct TextBlock;
}
}
namespace KeyboardManagerHelper
{
enum class ErrorType;
}
// Wrapper class for the key drop down menu
class KeyDropDownControl
{
private:
// Stores the drop down combo box
winrt::Windows::Foundation::IInspectable dropDown;
// Stores the previous layout
HKL previousLayout = 0;
// Stores the flyout warning message
winrt::Windows::Foundation::IInspectable warningMessage;
// Stores the flyout attached to the current drop down
winrt::Windows::Foundation::IInspectable warningFlyout;
// Stores whether a key to shortcut warning has to be ignored
bool ignoreKeyToShortcutWarning;
// Function to set properties apart from the SelectionChanged event handler
void SetDefaultProperties(bool isShortcut, bool renderDisable);
// Function to check if the layout has changed and accordingly update the drop down list
void CheckAndUpdateKeyboardLayout(ComboBox currentDropDown, bool isShortcut, bool renderDisable);
// Get selected value of dropdown or -1 if nothing is selected
static DWORD GetSelectedValue(ComboBox comboBox);
// Function to set accessible name for combobox
static void SetAccessibleNameForComboBox(ComboBox dropDown, int index);
public:
// Pointer to the keyboard manager state
static KeyboardManagerState* keyboardManagerState;
// Constructor - the last default parameter should be passed as false only if it originates from Type shortcut or when an old shortcut is reloaded
KeyDropDownControl(bool isShortcut, bool fromAddShortcutToControl = false, bool renderDisable = false) :
ignoreKeyToShortcutWarning(fromAddShortcutToControl)
{
SetDefaultProperties(isShortcut, renderDisable);
}
// Function to set selection handler for single key remap drop down. Needs to be called after the constructor since the singleKeyControl StackPanel is null if called in the constructor
void SetSelectionHandler(StackPanel& table, StackPanel row, int colIndex, RemapBuffer& singleKeyRemapBuffer);
// Function for validating the selection of shortcuts for the drop down
std::pair<KeyboardManagerHelper::ErrorType, int> ValidateShortcutSelection(StackPanel table, StackPanel row, StackPanel parent, int colIndex, RemapBuffer& shortcutRemapBuffer, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, winrt::Windows::UI::Xaml::Controls::TextBox targetApp, bool isHybridControl, bool isSingleKeyWindow);
// Function to set selection handler for shortcut drop down.
void SetSelectionHandler(StackPanel& table, StackPanel row, winrt::Windows::UI::Xaml::Controls::StackPanel parent, int colIndex, RemapBuffer& shortcutRemapBuffer, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, winrt::Windows::UI::Xaml::Controls::TextBox& targetApp, bool isHybridControl, bool isSingleKeyWindow);
// Function to return the combo box element of the drop down
ComboBox GetComboBox();
// Function to add a drop down to the shortcut stack panel
static void AddDropDown(StackPanel& table, StackPanel row, winrt::Windows::UI::Xaml::Controls::StackPanel parent, const int colIndex, RemapBuffer& shortcutRemapBuffer, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, winrt::Windows::UI::Xaml::Controls::TextBox targetApp, bool isHybridControl, bool isSingleKeyWindow, bool ignoreWarning = false);
// Function to get the list of key codes from the shortcut combo box stack panel
static std::vector<int32_t> GetSelectedCodesFromStackPanel(StackPanel parent);
// Function for validating the selection of shortcuts for all the associated drop downs
static void ValidateShortcutFromDropDownList(StackPanel table, StackPanel row, StackPanel parent, int colIndex, RemapBuffer& shortcutRemapBuffer, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, TextBox targetApp, bool isHybridControl, bool isSingleKeyWindow);
// Function to set the warning message
void SetDropDownError(winrt::Windows::UI::Xaml::Controls::ComboBox currentDropDown, winrt::hstring message);
// Set selected Value
void SetSelectedValue(std::wstring value);
// Function to add a shortcut to the UI control as combo boxes
static void AddShortcutToControl(Shortcut shortcut, StackPanel table, StackPanel parent, KeyboardManagerState& keyboardManagerState, const int colIndex, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, RemapBuffer& remapBuffer, StackPanel row, TextBox targetApp, bool isHybridControl, bool isSingleKeyWindow);
// Get keys name list depending if Disable is in dropdown
static std::vector<std::pair<DWORD,std::wstring>> GetKeyList(bool isShortcut, bool renderDisable);
// Get number of selected keys. Do not count -1 and 0 values as they stand for Not selected and None
static int GetNumberOfSelectedKeys(std::vector<int32_t> keys);
};

View File

@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{23d2070d-e4ad-4add-85a7-083d9c76ad49}</ProjectGuid>
<RootNamespace>KeyboardManagerEditorLibrary</RootNamespace>
<OverrideWindowsTargetPlatformVersion>true</OverrideWindowsTargetPlatformVersion>
<TargetPlatformVersion>10.0.18362.0</TargetPlatformVersion>
<WindowsTargetPlatformVersion>10.0.18362.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\modules\KeyboardManager\</OutDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<AdditionalIncludeDirectories>./;$(SolutionDir)src\modules\;$(SolutionDir)src\common\Display;$(SolutionDir)src\common\inc;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src;./../common;./../;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="BufferValidationHelpers.h" />
<ClInclude Include="Dialog.h" />
<ClInclude Include="EditKeyboardWindow.h" />
<ClInclude Include="EditShortcutsWindow.h" />
<ClInclude Include="KeyboardManagerEditorStrings.h" />
<ClInclude Include="KeyDropDownControl.h" />
<ClInclude Include="LoadingAndSavingRemappingHelper.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="ShortcutControl.h" />
<ClInclude Include="SingleKeyRemapControl.h" />
<ClInclude Include="Styles.h" />
<ClInclude Include="targetver.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="UIHelpers.h" />
<ClInclude Include="XamlBridge.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="BufferValidationHelpers.cpp" />
<ClCompile Include="Dialog.cpp" />
<ClCompile Include="EditKeyboardWindow.cpp" />
<ClCompile Include="EditShortcutsWindow.cpp" />
<ClCompile Include="KeyboardManagerEditorStrings.cpp" />
<ClCompile Include="KeyDropDownControl.cpp" />
<ClCompile Include="LoadingAndSavingRemappingHelper.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="ShortcutControl.cpp" />
<ClCompile Include="SingleKeyRemapControl.cpp" />
<ClCompile Include="Styles.cpp" />
<ClCompile Include="trace.cpp" />
<ClCompile Include="UIHelpers.cpp" />
<ClCompile Include="XamlBridge.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Display\Display.vcxproj">
<Project>{caba8dfb-823b-4bf2-93ac-3f31984150d9}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\Themes\Themes.vcxproj">
<Project>{98537082-0fdb-40de-abd8-0dc5a4269bab}</Project>
</ProjectReference>
<ProjectReference Include="..\common\KeyboardManagerCommon.vcxproj">
<Project>{8affa899-0b73-49ec-8c50-0fadda57b2fc}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Import Project="..\..\..\..\deps\spdlog.props" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory)\..\KeyboardManagerEditor\ resource.base.h resource.h KeyboardManagerEditor.base.rc KeyboardManagerEditor.rc" />
</Target>
</Project>

View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="BufferValidationHelpers.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Dialog.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="EditKeyboardWindow.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="EditShortcutsWindow.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="KeyDropDownControl.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LoadingAndSavingRemappingHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ShortcutControl.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SingleKeyRemapControl.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Styles.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="targetver.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="UIHelpers.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="XamlBridge.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="KeyboardManagerEditorStrings.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="BufferValidationHelpers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Dialog.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="EditKeyboardWindow.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="EditShortcutsWindow.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="KeyDropDownControl.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LoadingAndSavingRemappingHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ShortcutControl.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SingleKeyRemapControl.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Styles.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="UIHelpers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="XamlBridge.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="KeyboardManagerEditorStrings.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,50 @@
#include "pch.h"
#include "KeyboardManagerEditorStrings.h"
// Function to return the error message
winrt::hstring KeyboardManagerEditorStrings::GetErrorMessage(KeyboardManagerHelper::ErrorType errorType)
{
using namespace KeyboardManagerHelper;
switch (errorType)
{
case ErrorType::NoError:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_REMAPSUCCESSFUL).c_str();
case ErrorType::SameKeyPreviouslyMapped:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_SAMEKEYPREVIOUSLYMAPPED).c_str();
case ErrorType::MapToSameKey:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_MAPPEDTOSAMEKEY).c_str();
case ErrorType::ConflictingModifierKey:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_CONFLICTINGMODIFIERKEY).c_str();
case ErrorType::SameShortcutPreviouslyMapped:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_SAMESHORTCUTPREVIOUSLYMAPPED).c_str();
case ErrorType::MapToSameShortcut:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_MAPTOSAMESHORTCUT).c_str();
case ErrorType::ConflictingModifierShortcut:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_CONFLICTINGMODIFIERSHORTCUT).c_str();
case ErrorType::WinL:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_WINL).c_str();
case ErrorType::CtrlAltDel:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_CTRLALTDEL).c_str();
case ErrorType::RemapUnsuccessful:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_REMAPUNSUCCESSFUL).c_str();
case ErrorType::SaveFailed:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_SAVEFAILED).c_str();
case ErrorType::ShortcutStartWithModifier:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_SHORTCUTSTARTWITHMODIFIER).c_str();
case ErrorType::ShortcutCannotHaveRepeatedModifier:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_SHORTCUTNOREPEATEDMODIFIER).c_str();
case ErrorType::ShortcutAtleast2Keys:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_SHORTCUTATLEAST2KEYS).c_str();
case ErrorType::ShortcutOneActionKey:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_SHORTCUTONEACTIONKEY).c_str();
case ErrorType::ShortcutNotMoreThanOneActionKey:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_SHORTCUTMAXONEACTIONKEY).c_str();
case ErrorType::ShortcutMaxShortcutSizeOneActionKey:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_MAXSHORTCUTSIZE).c_str();
case ErrorType::ShortcutDisableAsActionKey:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_DISABLEASACTIONKEY).c_str();
default:
return GET_RESOURCE_STRING(IDS_ERRORMESSAGE_DEFAULT).c_str();
}
}

View File

@@ -0,0 +1,14 @@
#pragma once
#include "pch.h"
#include <ErrorTypes.h>
namespace KeyboardManagerEditorStrings
{
// String constant for the default app name in Remap shortcuts
inline const std::wstring DefaultAppName = GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_ALLAPPS);
// Function to return the error message
winrt::hstring GetErrorMessage(KeyboardManagerHelper::ErrorType errorType);
}

View File

@@ -0,0 +1,230 @@
#include "pch.h"
#include "LoadingAndSavingRemappingHelper.h"
#include <set>
#include <common/interop/shared_constants.h>
#include <ErrorTypes.h>
#include <KeyboardManagerState.h>
#include <keyboardmanager/KeyboardManagerEditorLibrary/trace.h>
namespace LoadingAndSavingRemappingHelper
{
// Function to check if the set of remappings in the buffer are valid
KeyboardManagerHelper::ErrorType CheckIfRemappingsAreValid(const RemapBuffer& remappings)
{
KeyboardManagerHelper::ErrorType isSuccess = KeyboardManagerHelper::ErrorType::NoError;
std::map<std::wstring, std::set<KeyShortcutUnion>> ogKeys;
for (int i = 0; i < remappings.size(); i++)
{
KeyShortcutUnion ogKey = remappings[i].first[0];
KeyShortcutUnion newKey = remappings[i].first[1];
std::wstring appName = remappings[i].second;
bool ogKeyValidity = (ogKey.index() == 0 && std::get<DWORD>(ogKey) != NULL) || (ogKey.index() == 1 && std::get<Shortcut>(ogKey).IsValidShortcut());
bool newKeyValidity = (newKey.index() == 0 && std::get<DWORD>(newKey) != NULL) || (newKey.index() == 1 && std::get<Shortcut>(newKey).IsValidShortcut());
// Add new set for a new target app name
if (ogKeys.find(appName) == ogKeys.end())
{
ogKeys[appName] = std::set<KeyShortcutUnion>();
}
if (ogKeyValidity && newKeyValidity && ogKeys[appName].find(ogKey) == ogKeys[appName].end())
{
ogKeys[appName].insert(ogKey);
}
else if (ogKeyValidity && newKeyValidity && ogKeys[appName].find(ogKey) != ogKeys[appName].end())
{
isSuccess = KeyboardManagerHelper::ErrorType::RemapUnsuccessful;
}
else
{
isSuccess = KeyboardManagerHelper::ErrorType::RemapUnsuccessful;
}
}
return isSuccess;
}
// Function to return the set of keys that have been orphaned from the remap buffer
std::vector<DWORD> GetOrphanedKeys(const RemapBuffer& remappings)
{
std::set<DWORD> ogKeys;
std::set<DWORD> newKeys;
for (int i = 0; i < remappings.size(); i++)
{
DWORD ogKey = std::get<DWORD>(remappings[i].first[0]);
KeyShortcutUnion newKey = remappings[i].first[1];
if (ogKey != NULL && ((newKey.index() == 0 && std::get<DWORD>(newKey) != 0) || (newKey.index() == 1 && std::get<Shortcut>(newKey).IsValidShortcut())))
{
ogKeys.insert(ogKey);
// newKey should be added only if the target is a key
if (remappings[i].first[1].index() == 0)
{
newKeys.insert(std::get<DWORD>(newKey));
}
}
}
for (auto& k : newKeys)
{
ogKeys.erase(k);
}
return std::vector(ogKeys.begin(), ogKeys.end());
}
// Function to combine remappings if the L and R version of the modifier is mapped to the same key
void CombineRemappings(SingleKeyRemapTable& table, DWORD leftKey, DWORD rightKey, DWORD combinedKey)
{
if (table.find(leftKey) != table.end() && table.find(rightKey) != table.end())
{
// If they are mapped to the same key, delete those entries and set the common version
if (table[leftKey] == table[rightKey])
{
table[combinedKey] = table[leftKey];
table.erase(leftKey);
table.erase(rightKey);
}
}
}
// Function to pre process the remap table before loading it into the UI
void PreProcessRemapTable(SingleKeyRemapTable& table)
{
// Pre process the table to combine L and R versions of Ctrl/Alt/Shift/Win that are mapped to the same key
CombineRemappings(table, VK_LCONTROL, VK_RCONTROL, VK_CONTROL);
CombineRemappings(table, VK_LMENU, VK_RMENU, VK_MENU);
CombineRemappings(table, VK_LSHIFT, VK_RSHIFT, VK_SHIFT);
CombineRemappings(table, VK_LWIN, VK_RWIN, CommonSharedConstants::VK_WIN_BOTH);
}
// Function to apply the single key remappings from the buffer to the KeyboardManagerState variable
void ApplySingleKeyRemappings(KeyboardManagerState& keyboardManagerState, const RemapBuffer& remappings, bool isTelemetryRequired)
{
// Clear existing Key Remaps
keyboardManagerState.ClearSingleKeyRemaps();
DWORD successfulKeyToKeyRemapCount = 0;
DWORD successfulKeyToShortcutRemapCount = 0;
for (int i = 0; i < remappings.size(); i++)
{
DWORD originalKey = std::get<DWORD>(remappings[i].first[0]);
KeyShortcutUnion newKey = remappings[i].first[1];
if (originalKey != NULL && !(newKey.index() == 0 && std::get<DWORD>(newKey) == NULL) && !(newKey.index() == 1 && !std::get<Shortcut>(newKey).IsValidShortcut()))
{
// If Ctrl/Alt/Shift are added, add their L and R versions instead to the same key
bool result = false;
bool res1, res2;
switch (originalKey)
{
case VK_CONTROL:
res1 = keyboardManagerState.AddSingleKeyRemap(VK_LCONTROL, newKey);
res2 = keyboardManagerState.AddSingleKeyRemap(VK_RCONTROL, newKey);
result = res1 && res2;
break;
case VK_MENU:
res1 = keyboardManagerState.AddSingleKeyRemap(VK_LMENU, newKey);
res2 = keyboardManagerState.AddSingleKeyRemap(VK_RMENU, newKey);
result = res1 && res2;
break;
case VK_SHIFT:
res1 = keyboardManagerState.AddSingleKeyRemap(VK_LSHIFT, newKey);
res2 = keyboardManagerState.AddSingleKeyRemap(VK_RSHIFT, newKey);
result = res1 && res2;
break;
case CommonSharedConstants::VK_WIN_BOTH:
res1 = keyboardManagerState.AddSingleKeyRemap(VK_LWIN, newKey);
res2 = keyboardManagerState.AddSingleKeyRemap(VK_RWIN, newKey);
result = res1 && res2;
break;
default:
result = keyboardManagerState.AddSingleKeyRemap(originalKey, newKey);
}
if (result)
{
if (newKey.index() == 0)
{
successfulKeyToKeyRemapCount += 1;
}
else
{
successfulKeyToShortcutRemapCount += 1;
}
}
}
}
// If telemetry is to be logged, log the key remap counts
if (isTelemetryRequired)
{
Trace::KeyRemapCount(successfulKeyToKeyRemapCount, successfulKeyToShortcutRemapCount);
}
}
// Function to apply the shortcut remappings from the buffer to the KeyboardManagerState variable
void ApplyShortcutRemappings(KeyboardManagerState& keyboardManagerState, const RemapBuffer& remappings, bool isTelemetryRequired)
{
// Clear existing shortcuts
keyboardManagerState.ClearOSLevelShortcuts();
keyboardManagerState.ClearAppSpecificShortcuts();
DWORD successfulOSLevelShortcutToShortcutRemapCount = 0;
DWORD successfulOSLevelShortcutToKeyRemapCount = 0;
DWORD successfulAppSpecificShortcutToShortcutRemapCount = 0;
DWORD successfulAppSpecificShortcutToKeyRemapCount = 0;
// Save the shortcuts that are valid and report if any of them were invalid
for (int i = 0; i < remappings.size(); i++)
{
Shortcut originalShortcut = std::get<Shortcut>(remappings[i].first[0]);
KeyShortcutUnion newShortcut = remappings[i].first[1];
if (originalShortcut.IsValidShortcut() && ((newShortcut.index() == 0 && std::get<DWORD>(newShortcut) != NULL) || (newShortcut.index() == 1 && std::get<Shortcut>(newShortcut).IsValidShortcut())))
{
if (remappings[i].second == L"")
{
bool result = keyboardManagerState.AddOSLevelShortcut(originalShortcut, newShortcut);
if (result)
{
if (newShortcut.index() == 0)
{
successfulOSLevelShortcutToKeyRemapCount += 1;
}
else
{
successfulOSLevelShortcutToShortcutRemapCount += 1;
}
}
}
else
{
bool result = keyboardManagerState.AddAppSpecificShortcut(remappings[i].second, originalShortcut, newShortcut);
if (result)
{
if (newShortcut.index() == 0)
{
successfulAppSpecificShortcutToKeyRemapCount += 1;
}
else
{
successfulAppSpecificShortcutToShortcutRemapCount += 1;
}
}
}
}
}
// If telemetry is to be logged, log the shortcut remap counts
if (isTelemetryRequired)
{
Trace::OSLevelShortcutRemapCount(successfulOSLevelShortcutToShortcutRemapCount, successfulOSLevelShortcutToKeyRemapCount);
Trace::AppSpecificShortcutRemapCount(successfulAppSpecificShortcutToShortcutRemapCount, successfulAppSpecificShortcutToKeyRemapCount);
}
}
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include <keyboardmanager/common/Helpers.h>
class KeyboardManagerState;
namespace LoadingAndSavingRemappingHelper
{
// Function to check if the set of remappings in the buffer are valid
KeyboardManagerHelper::ErrorType CheckIfRemappingsAreValid(const RemapBuffer& remappings);
// Function to return the set of keys that have been orphaned from the remap buffer
std::vector<DWORD> GetOrphanedKeys(const RemapBuffer& remappings);
// Function to combine remappings if the L and R version of the modifier is mapped to the same key
void CombineRemappings(std::unordered_map<DWORD, KeyShortcutUnion>& table, DWORD leftKey, DWORD rightKey, DWORD combinedKey);
// Function to pre process the remap table before loading it into the UI
void PreProcessRemapTable(std::unordered_map<DWORD, KeyShortcutUnion>& table);
// Function to apply the single key remappings from the buffer to the KeyboardManagerState variable
void ApplySingleKeyRemappings(KeyboardManagerState& keyboardManagerState, const RemapBuffer& remappings, bool isTelemetryRequired);
// Function to apply the shortcut remappings from the buffer to the KeyboardManagerState variable
void ApplyShortcutRemappings(KeyboardManagerState& keyboardManagerState, const RemapBuffer& remappings, bool isTelemetryRequired);
}

View File

@@ -0,0 +1,482 @@
#include "pch.h"
#include "ShortcutControl.h"
#include <common/interop/shared_constants.h>
#include <KeyboardManagerState.h>
#include <KeyboardManagerEditorStrings.h>
#include <KeyDropDownControl.h>
#include <UIHelpers.h>
//Both static members are initialized to null
HWND ShortcutControl::editShortcutsWindowHandle = nullptr;
KeyboardManagerState* ShortcutControl::keyboardManagerState = nullptr;
// Initialized as new vector
RemapBuffer ShortcutControl::shortcutRemapBuffer;
ShortcutControl::ShortcutControl(StackPanel table, StackPanel row, const int colIndex, TextBox targetApp)
{
shortcutDropDownStackPanel = StackPanel();
typeShortcut = Button();
shortcutControlLayout = StackPanel();
bool isHybridControl = colIndex == 1 ? true : false;
shortcutDropDownStackPanel.as<StackPanel>().Spacing(KeyboardManagerConstants::ShortcutTableDropDownSpacing);
shortcutDropDownStackPanel.as<StackPanel>().Orientation(Windows::UI::Xaml::Controls::Orientation::Horizontal);
typeShortcut.as<Button>().Content(winrt::box_value(GET_RESOURCE_STRING(IDS_TYPE_BUTTON)));
typeShortcut.as<Button>().Width(KeyboardManagerConstants::ShortcutTableDropDownWidth);
typeShortcut.as<Button>().Click([&, table, row, colIndex, isHybridControl, targetApp](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
keyboardManagerState->SetUIState(KeyboardManagerUIState::DetectShortcutWindowActivated, editShortcutsWindowHandle);
// Using the XamlRoot of the typeShortcut to get the root of the XAML host
CreateDetectShortcutWindow(sender, sender.as<Button>().XamlRoot(), *keyboardManagerState, colIndex, table, keyDropDownControlObjects, row, targetApp, isHybridControl, false, editShortcutsWindowHandle, shortcutRemapBuffer);
});
// Set an accessible name for the type shortcut button
typeShortcut.as<Button>().SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_TYPE_BUTTON)));
shortcutControlLayout.as<StackPanel>().Spacing(KeyboardManagerConstants::ShortcutTableDropDownSpacing);
shortcutControlLayout.as<StackPanel>().Children().Append(typeShortcut.as<Button>());
shortcutControlLayout.as<StackPanel>().Children().Append(shortcutDropDownStackPanel.as<StackPanel>());
KeyDropDownControl::AddDropDown(table, row, shortcutDropDownStackPanel.as<StackPanel>(), colIndex, shortcutRemapBuffer, keyDropDownControlObjects, targetApp, isHybridControl, false);
shortcutControlLayout.as<StackPanel>().UpdateLayout();
}
// Function to set the accessible name of the target App text box
void ShortcutControl::SetAccessibleNameForTextBox(TextBox targetAppTextBox, int rowIndex)
{
// To set the accessible name of the target App text box by adding the string `All Apps` if the text box is empty, if not the application name is read by narrator.
std::wstring targetAppTextBoxAccessibleName = GET_RESOURCE_STRING(IDS_AUTOMATIONPROPERTIES_ROW) + std::to_wstring(rowIndex) + L", " + GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_TARGETAPPHEADER);
if (targetAppTextBox.Text() == L"")
{
targetAppTextBoxAccessibleName += GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_ALLAPPS);
}
targetAppTextBox.SetValue(Automation::AutomationProperties::NameProperty(), box_value(targetAppTextBoxAccessibleName));
}
// Function to set the accessible names for all the controls in a row
void ShortcutControl::UpdateAccessibleNames(StackPanel sourceColumn, StackPanel mappedToColumn, TextBox targetAppTextBox, Button deleteButton, int rowIndex)
{
sourceColumn.SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_AUTOMATIONPROPERTIES_ROW) + std::to_wstring(rowIndex) + L", " + GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_SOURCEHEADER)));
mappedToColumn.SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_AUTOMATIONPROPERTIES_ROW) + std::to_wstring(rowIndex) + L", " + GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_TARGETHEADER)));
ShortcutControl::SetAccessibleNameForTextBox(targetAppTextBox, rowIndex);
deleteButton.SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_AUTOMATIONPROPERTIES_ROW) + std::to_wstring(rowIndex) + L", " + GET_RESOURCE_STRING(IDS_DELETE_REMAPPING_BUTTON)));
}
// Function to add a new row to the shortcut table. If the originalKeys and newKeys args are provided, then the displayed shortcuts are set to those values.
void ShortcutControl::AddNewShortcutControlRow(StackPanel& parent, std::vector<std::vector<std::unique_ptr<ShortcutControl>>>& keyboardRemapControlObjects, const Shortcut& originalKeys, const KeyShortcutUnion& newKeys, const std::wstring& targetAppName)
{
// Textbox for target application
TextBox targetAppTextBox;
// Create new ShortcutControl objects dynamically so that we does not get destructed
std::vector<std::unique_ptr<ShortcutControl>> newrow;
StackPanel row = StackPanel();
parent.Children().Append(row);
newrow.emplace_back(std::make_unique<ShortcutControl>(parent, row, 0, targetAppTextBox));
newrow.emplace_back(std::make_unique<ShortcutControl>(parent, row, 1, targetAppTextBox));
keyboardRemapControlObjects.push_back(std::move(newrow));
row.Padding({ 10, 10, 10, 10 });
row.Orientation(Orientation::Horizontal);
auto brush = Windows::UI::Xaml::Application::Current().Resources().Lookup(box_value(L"SystemControlBackgroundListLowBrush")).as<Windows::UI::Xaml::Media::SolidColorBrush>();
if (keyboardRemapControlObjects.size() % 2)
{
row.Background(brush);
}
// ShortcutControl for the original shortcut
auto origin = keyboardRemapControlObjects.back()[0]->GetShortcutControl();
origin.Width(KeyboardManagerConstants::ShortcutOriginColumnWidth);
row.Children().Append(origin);
// Arrow icon
FontIcon arrowIcon;
arrowIcon.FontFamily(Xaml::Media::FontFamily(L"Segoe MDL2 Assets"));
arrowIcon.Glyph(L"\xE72A");
arrowIcon.VerticalAlignment(VerticalAlignment::Center);
arrowIcon.HorizontalAlignment(HorizontalAlignment::Center);
auto arrowIconContainer = UIHelpers::GetWrapped(arrowIcon, KeyboardManagerConstants::ShortcutArrowColumnWidth).as<StackPanel>();
arrowIconContainer.Orientation(Orientation::Vertical);
arrowIconContainer.VerticalAlignment(VerticalAlignment::Center);
row.Children().Append(arrowIconContainer);
// ShortcutControl for the new shortcut
auto target = keyboardRemapControlObjects.back()[1]->GetShortcutControl();
target.Width(KeyboardManagerConstants::ShortcutTargetColumnWidth);
row.Children().Append(target);
targetAppTextBox.Width(KeyboardManagerConstants::ShortcutTableDropDownWidth);
targetAppTextBox.PlaceholderText(KeyboardManagerEditorStrings::DefaultAppName);
targetAppTextBox.Text(targetAppName);
// GotFocus handler will be called whenever the user tabs into or clicks on the textbox
targetAppTextBox.GotFocus([targetAppTextBox](auto const& sender, auto const& e) {
// Select all text for accessible purpose
targetAppTextBox.SelectAll();
});
// LostFocus handler will be called whenever text is updated by a user and then they click something else or tab to another control. Does not get called if Text is updated while the TextBox isn't in focus (i.e. from code)
targetAppTextBox.LostFocus([&keyboardRemapControlObjects, parent, row, targetAppTextBox](auto const& sender, auto const& e) {
// Get index of targetAppTextBox button
uint32_t rowIndex;
if (!parent.Children().IndexOf(row, rowIndex))
{
return;
}
// rowIndex could be out of bounds if the the row got deleted after LostFocus handler was invoked. In this case it should return
if (rowIndex >= keyboardRemapControlObjects.size())
{
return;
}
// Validate both set of drop downs
KeyDropDownControl::ValidateShortcutFromDropDownList(parent, row, keyboardRemapControlObjects[rowIndex][0]->shortcutDropDownStackPanel.as<StackPanel>(), 0, ShortcutControl::shortcutRemapBuffer, keyboardRemapControlObjects[rowIndex][0]->keyDropDownControlObjects, targetAppTextBox, false, false);
KeyDropDownControl::ValidateShortcutFromDropDownList(parent, row, keyboardRemapControlObjects[rowIndex][1]->shortcutDropDownStackPanel.as<StackPanel>(), 1, ShortcutControl::shortcutRemapBuffer, keyboardRemapControlObjects[rowIndex][1]->keyDropDownControlObjects, targetAppTextBox, true, false);
// Reset the buffer based on the selected drop down items
std::get<Shortcut>(shortcutRemapBuffer[rowIndex].first[0]).SetKeyCodes(KeyDropDownControl::GetSelectedCodesFromStackPanel(keyboardRemapControlObjects[rowIndex][0]->shortcutDropDownStackPanel.as<StackPanel>()));
// second column is a hybrid column
std::vector<int32_t> selectedKeyCodes = KeyDropDownControl::GetSelectedCodesFromStackPanel(keyboardRemapControlObjects[rowIndex][1]->shortcutDropDownStackPanel.as<StackPanel>());
// If exactly one key is selected consider it to be a key remap
if (selectedKeyCodes.size() == 1)
{
shortcutRemapBuffer[rowIndex].first[1] = selectedKeyCodes[0];
}
else
{
Shortcut tempShortcut;
tempShortcut.SetKeyCodes(selectedKeyCodes);
// Assign instead of setting the value in the buffer since the previous value may not be a Shortcut
shortcutRemapBuffer[rowIndex].first[1] = tempShortcut;
}
std::wstring newText = targetAppTextBox.Text().c_str();
std::wstring lowercaseDefAppName = KeyboardManagerEditorStrings::DefaultAppName;
std::transform(newText.begin(), newText.end(), newText.begin(), towlower);
std::transform(lowercaseDefAppName.begin(), lowercaseDefAppName.end(), lowercaseDefAppName.begin(), towlower);
if (newText == lowercaseDefAppName)
{
shortcutRemapBuffer[rowIndex].second = L"";
}
else
{
shortcutRemapBuffer[rowIndex].second = targetAppTextBox.Text().c_str();
}
// To set the accessibile name of the target app text box when focus is lost
ShortcutControl::SetAccessibleNameForTextBox(targetAppTextBox, rowIndex + 1);
});
// We need two containers in order to align it horizontally and vertically
StackPanel targetAppHorizontal = UIHelpers::GetWrapped(targetAppTextBox, KeyboardManagerConstants::TableTargetAppColWidth).as<StackPanel>();
targetAppHorizontal.Orientation(Orientation::Horizontal);
targetAppHorizontal.HorizontalAlignment(HorizontalAlignment::Left);
StackPanel targetAppContainer = UIHelpers::GetWrapped(targetAppHorizontal, KeyboardManagerConstants::TableTargetAppColWidth).as<StackPanel>();
targetAppContainer.Orientation(Orientation::Vertical);
targetAppContainer.VerticalAlignment(VerticalAlignment::Bottom);
row.Children().Append(targetAppContainer);
// Delete row button
Windows::UI::Xaml::Controls::Button deleteShortcut;
FontIcon deleteSymbol;
deleteSymbol.FontFamily(Xaml::Media::FontFamily(L"Segoe MDL2 Assets"));
deleteSymbol.Glyph(L"\xE74D");
deleteShortcut.Content(deleteSymbol);
deleteShortcut.Background(Media::SolidColorBrush(Colors::Transparent()));
deleteShortcut.HorizontalAlignment(HorizontalAlignment::Center);
deleteShortcut.Click([&, parent, row, brush](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
Button currentButton = sender.as<Button>();
uint32_t rowIndex;
// Get index of delete button
UIElementCollection children = parent.Children();
bool indexFound = children.IndexOf(row, rowIndex);
// IndexOf could fail if the the row got deleted and the button handler was invoked twice. In this case it should return
if (!indexFound)
{
return;
}
for (uint32_t i = rowIndex + 1; i < children.Size(); i++)
{
StackPanel row = children.GetAt(i).as<StackPanel>();
row.Background(i % 2 ? brush : Media::SolidColorBrush(Colors::Transparent()));
StackPanel sourceCol = row.Children().GetAt(0).as<StackPanel>();
StackPanel targetCol = row.Children().GetAt(2).as<StackPanel>();
TextBox targetApp = row.Children().GetAt(3).as<StackPanel>().Children().GetAt(0).as<StackPanel>().Children().GetAt(0).as<TextBox>();
Button delButton = row.Children().GetAt(4).as<StackPanel>().Children().GetAt(0).as<Button>();
UpdateAccessibleNames(sourceCol, targetCol, targetApp, delButton, i);
}
children.RemoveAt(rowIndex);
parent.UpdateLayout();
shortcutRemapBuffer.erase(shortcutRemapBuffer.begin() + rowIndex);
// delete the SingleKeyRemapControl objects so that they get destructed
keyboardRemapControlObjects.erase(keyboardRemapControlObjects.begin() + rowIndex);
});
// To set the accessible name of the delete button
deleteShortcut.SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_DELETE_REMAPPING_BUTTON)));
// Add tooltip for delete button which would appear on hover
ToolTip deleteShortcuttoolTip;
deleteShortcuttoolTip.Content(box_value(GET_RESOURCE_STRING(IDS_DELETE_REMAPPING_BUTTON)));
ToolTipService::SetToolTip(deleteShortcut, deleteShortcuttoolTip);
StackPanel deleteShortcutContainer = StackPanel();
deleteShortcutContainer.Children().Append(deleteShortcut);
deleteShortcutContainer.Orientation(Orientation::Vertical);
deleteShortcutContainer.VerticalAlignment(VerticalAlignment::Center);
row.Children().Append(deleteShortcutContainer);
parent.UpdateLayout();
// Set accessible names
UpdateAccessibleNames(keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][0]->GetShortcutControl(), keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][1]->GetShortcutControl(), targetAppTextBox, deleteShortcut, (int)keyboardRemapControlObjects.size());
// Set the shortcut text if the two vectors are not empty (i.e. default args)
if (originalKeys.IsValidShortcut() && !(newKeys.index() == 0 && std::get<DWORD>(newKeys) == NULL) && !(newKeys.index() == 1 && !std::get<Shortcut>(newKeys).IsValidShortcut()))
{
// change to load app name
shortcutRemapBuffer.push_back(std::make_pair<RemapBufferItem, std::wstring>(RemapBufferItem{ Shortcut(), Shortcut() }, std::wstring(targetAppName)));
KeyDropDownControl::AddShortcutToControl(originalKeys, parent, keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][0]->shortcutDropDownStackPanel.as<StackPanel>(), *keyboardManagerState, 0, keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][0]->keyDropDownControlObjects, shortcutRemapBuffer, row, targetAppTextBox, false, false);
if (newKeys.index() == 0)
{
keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][1]->keyDropDownControlObjects[0]->SetSelectedValue(std::to_wstring(std::get<DWORD>(newKeys)));
}
else
{
KeyDropDownControl::AddShortcutToControl(std::get<Shortcut>(newKeys), parent, keyboardRemapControlObjects.back()[1]->shortcutDropDownStackPanel.as<StackPanel>(), *keyboardManagerState, 1, keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][1]->keyDropDownControlObjects, shortcutRemapBuffer, row, targetAppTextBox, true, false);
}
}
else
{
// Initialize both shortcuts as empty shortcuts
shortcutRemapBuffer.push_back(std::make_pair<RemapBufferItem, std::wstring>(RemapBufferItem{ Shortcut(), Shortcut() }, std::wstring(targetAppName)));
}
}
// Function to return the stack panel element of the ShortcutControl. This is the externally visible UI element which can be used to add it to other layouts
StackPanel ShortcutControl::GetShortcutControl()
{
return shortcutControlLayout.as<StackPanel>();
}
// Function to create the detect shortcut UI window
void ShortcutControl::CreateDetectShortcutWindow(winrt::Windows::Foundation::IInspectable const& sender, XamlRoot xamlRoot, KeyboardManagerState& keyboardManagerState, const int colIndex, StackPanel table, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, StackPanel row, TextBox targetApp, bool isHybridControl, bool isSingleKeyWindow, HWND parentWindow, RemapBuffer& remapBuffer)
{
// ContentDialog for detecting shortcuts. This is the parent UI element.
ContentDialog detectShortcutBox;
// ContentDialog requires manually setting the XamlRoot (https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.contentdialog#contentdialog-in-appwindow-or-xaml-islands)
detectShortcutBox.XamlRoot(xamlRoot);
detectShortcutBox.Title(box_value(GET_RESOURCE_STRING(IDS_TYPESHORTCUT_TITLE)));
detectShortcutBox.IsPrimaryButtonEnabled(false);
detectShortcutBox.IsSecondaryButtonEnabled(false);
// Get the linked stack panel for the "Type shortcut" button that was clicked
StackPanel linkedShortcutStackPanel = UIHelpers::GetSiblingElement(sender).as<StackPanel>();
auto unregisterKeys = [&keyboardManagerState]() {
keyboardManagerState.ClearRegisteredKeyDelays();
};
auto selectDetectedShortcutAndResetKeys = [&keyboardManagerState](DWORD key) {
keyboardManagerState.SelectDetectedShortcut(key);
keyboardManagerState.ResetDetectedShortcutKey(key);
};
auto onPressEnter = [linkedShortcutStackPanel,
detectShortcutBox,
&keyboardManagerState,
unregisterKeys,
colIndex,
table,
targetApp,
&keyDropDownControlObjects,
row,
isHybridControl,
isSingleKeyWindow,
&remapBuffer] {
// Save the detected shortcut in the linked text block
Shortcut detectedShortcutKeys = keyboardManagerState.GetDetectedShortcut();
if (!detectedShortcutKeys.IsEmpty())
{
// The shortcut buffer gets set in this function
KeyDropDownControl::AddShortcutToControl(detectedShortcutKeys, table, linkedShortcutStackPanel, keyboardManagerState, colIndex, keyDropDownControlObjects, remapBuffer, row, targetApp, isHybridControl, isSingleKeyWindow);
}
// Hide the type shortcut UI
detectShortcutBox.Hide();
};
auto onReleaseEnter = [&keyboardManagerState,
unregisterKeys,
isSingleKeyWindow,
parentWindow] {
// Reset the keyboard manager UI state
keyboardManagerState.ResetUIState();
if (isSingleKeyWindow)
{
// Revert UI state back to Edit Keyboard window
keyboardManagerState.SetUIState(KeyboardManagerUIState::EditKeyboardWindowActivated, parentWindow);
}
else
{
// Revert UI state back to Edit Shortcut window
keyboardManagerState.SetUIState(KeyboardManagerUIState::EditShortcutsWindowActivated, parentWindow);
}
unregisterKeys();
};
auto onAccept = [onPressEnter,
onReleaseEnter] {
onPressEnter();
onReleaseEnter();
};
TextBlock primaryButtonText;
primaryButtonText.Text(GET_RESOURCE_STRING(IDS_OK_BUTTON));
Button primaryButton;
primaryButton.HorizontalAlignment(HorizontalAlignment::Stretch);
primaryButton.Margin({ 2, 2, 2, 2 });
primaryButton.Content(primaryButtonText);
// OK button
primaryButton.Click([onAccept](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
onAccept();
});
// NOTE: UnregisterKeys should never be called on the DelayThread, as it will re-enter the mutex. To avoid this it is run on the dispatcher thread
keyboardManagerState.RegisterKeyDelay(
VK_RETURN,
selectDetectedShortcutAndResetKeys,
[primaryButton, onPressEnter, detectShortcutBox](DWORD) {
detectShortcutBox.Dispatcher().RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
[primaryButton, onPressEnter] {
// Use the base medium low brush to be consistent with the theme
primaryButton.Background(Windows::UI::Xaml::Application::Current().Resources().Lookup(box_value(L"SystemControlBackgroundBaseMediumLowBrush")).as<Windows::UI::Xaml::Media::SolidColorBrush>());
onPressEnter();
});
},
[onReleaseEnter, detectShortcutBox](DWORD) {
detectShortcutBox.Dispatcher().RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
[onReleaseEnter]() {
onReleaseEnter();
});
});
TextBlock cancelButtonText;
cancelButtonText.Text(GET_RESOURCE_STRING(IDS_CANCEL_BUTTON));
auto onCancel = [&keyboardManagerState,
detectShortcutBox,
unregisterKeys,
isSingleKeyWindow,
parentWindow] {
detectShortcutBox.Hide();
// Reset the keyboard manager UI state
keyboardManagerState.ResetUIState();
if (isSingleKeyWindow)
{
// Revert UI state back to Edit Keyboard window
keyboardManagerState.SetUIState(KeyboardManagerUIState::EditKeyboardWindowActivated, parentWindow);
}
else
{
// Revert UI state back to Edit Shortcut window
keyboardManagerState.SetUIState(KeyboardManagerUIState::EditShortcutsWindowActivated, parentWindow);
}
unregisterKeys();
};
Button cancelButton;
cancelButton.HorizontalAlignment(HorizontalAlignment::Stretch);
cancelButton.Margin({ 2, 2, 2, 2 });
cancelButton.Content(cancelButtonText);
// Cancel button
cancelButton.Click([onCancel](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
onCancel();
});
// NOTE: UnregisterKeys should never be called on the DelayThread, as it will re-enter the mutex. To avoid this it is run on the dispatcher thread
keyboardManagerState.RegisterKeyDelay(
VK_ESCAPE,
selectDetectedShortcutAndResetKeys,
[onCancel, detectShortcutBox](DWORD) {
detectShortcutBox.Dispatcher().RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
[onCancel] {
onCancel();
});
},
nullptr);
// StackPanel parent for the displayed text in the dialog
Windows::UI::Xaml::Controls::StackPanel stackPanel;
detectShortcutBox.Content(stackPanel);
// Header textblock
TextBlock text;
text.Text(GET_RESOURCE_STRING(IDS_TYPESHORTCUT_HEADER));
text.Margin({ 0, 0, 0, 10 });
stackPanel.Children().Append(text);
// Target StackPanel to place the selected key - first row (for 1-3 keys)
Windows::UI::Xaml::Controls::StackPanel keyStackPanel1;
keyStackPanel1.Orientation(Orientation::Horizontal);
stackPanel.Children().Append(keyStackPanel1);
// Target StackPanel to place the selected key - second row (for 4-5 keys)
Windows::UI::Xaml::Controls::StackPanel keyStackPanel2;
keyStackPanel2.Orientation(Orientation::Horizontal);
keyStackPanel2.Margin({ 0, 20, 0, 0 });
keyStackPanel2.Visibility(Visibility::Collapsed);
stackPanel.Children().Append(keyStackPanel2);
TextBlock holdEscInfo;
holdEscInfo.Text(GET_RESOURCE_STRING(IDS_TYPE_HOLDESC));
holdEscInfo.FontSize(12);
holdEscInfo.Margin({ 0, 20, 0, 0 });
stackPanel.Children().Append(holdEscInfo);
TextBlock holdEnterInfo;
holdEnterInfo.Text(GET_RESOURCE_STRING(IDS_TYPE_HOLDENTER));
holdEnterInfo.FontSize(12);
holdEnterInfo.Margin({ 0, 0, 0, 0 });
stackPanel.Children().Append(holdEnterInfo);
ColumnDefinition primaryButtonColumn;
ColumnDefinition cancelButtonColumn;
Grid buttonPanel;
buttonPanel.Margin({ 0, 20, 0, 0 });
buttonPanel.HorizontalAlignment(HorizontalAlignment::Stretch);
buttonPanel.ColumnDefinitions().Append(primaryButtonColumn);
buttonPanel.ColumnDefinitions().Append(cancelButtonColumn);
buttonPanel.SetColumn(primaryButton, 0);
buttonPanel.SetColumn(cancelButton, 1);
buttonPanel.Children().Append(primaryButton);
buttonPanel.Children().Append(cancelButton);
stackPanel.Children().Append(buttonPanel);
stackPanel.UpdateLayout();
// Configure the keyboardManagerState to store the UI information.
keyboardManagerState.ConfigureDetectShortcutUI(keyStackPanel1, keyStackPanel2);
// Show the dialog
detectShortcutBox.ShowAsync();
}

View File

@@ -0,0 +1,60 @@
#pragma once
#include <Shortcut.h>
class KeyboardManagerState;
class KeyDropDownControl;
namespace winrt::Windows::UI::Xaml
{
struct XamlRoot;
namespace Controls
{
struct StackPanel;
struct TextBox;
struct Button;
}
}
class ShortcutControl
{
private:
// Stack panel for the drop downs to display the selected shortcut
winrt::Windows::Foundation::IInspectable shortcutDropDownStackPanel;
// Button to type the shortcut
winrt::Windows::Foundation::IInspectable typeShortcut;
// StackPanel to parent the above controls
winrt::Windows::Foundation::IInspectable shortcutControlLayout;
// Function to set the accessible name of the target app text box
static void SetAccessibleNameForTextBox(TextBox targetAppTextBox, int rowIndex);
// Function to set the accessible names for all the controls in a row
static void UpdateAccessibleNames(StackPanel sourceColumn, StackPanel mappedToColumn, TextBox targetAppTextBox, Button deleteButton, int rowIndex);
public:
// Handle to the current Edit Shortcuts Window
static HWND editShortcutsWindowHandle;
// Pointer to the keyboard manager state
static KeyboardManagerState* keyboardManagerState;
// Stores the current list of remappings
static RemapBuffer shortcutRemapBuffer;
// Vector to store dynamically allocated KeyDropDownControl objects to avoid early destruction
std::vector<std::unique_ptr<KeyDropDownControl>> keyDropDownControlObjects;
// constructor
ShortcutControl(StackPanel table, StackPanel row, const int colIndex, TextBox targetApp);
// Function to add a new row to the shortcut table. If the originalKeys and newKeys args are provided, then the displayed shortcuts are set to those values.
static void AddNewShortcutControlRow(StackPanel& parent, std::vector<std::vector<std::unique_ptr<ShortcutControl>>>& keyboardRemapControlObjects, const Shortcut& originalKeys = Shortcut(), const KeyShortcutUnion& newKeys = Shortcut(), const std::wstring& targetAppName = L"");
// Function to return the stack panel element of the ShortcutControl. This is the externally visible UI element which can be used to add it to other layouts
StackPanel GetShortcutControl();
// Function to create the detect shortcut UI window
static void CreateDetectShortcutWindow(winrt::Windows::Foundation::IInspectable const& sender, XamlRoot xamlRoot, KeyboardManagerState& keyboardManagerState, const int colIndex, StackPanel table, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, StackPanel controlLayout, TextBox targetApp, bool isHybridControl, bool isSingleKeyWindow, HWND parentWindow, RemapBuffer& remapBuffer);
};

View File

@@ -0,0 +1,362 @@
#include "pch.h"
#include "SingleKeyRemapControl.h"
#include <KeyboardManagerState.h>
#include <ShortcutControl.h>
#include <UIHelpers.h>
//Both static members are initialized to null
HWND SingleKeyRemapControl::EditKeyboardWindowHandle = nullptr;
KeyboardManagerState* SingleKeyRemapControl::keyboardManagerState = nullptr;
// Initialized as new vector
RemapBuffer SingleKeyRemapControl::singleKeyRemapBuffer;
SingleKeyRemapControl::SingleKeyRemapControl(StackPanel table, StackPanel row, const int colIndex)
{
typeKey = Button();
typeKey.as<Button>().Width(KeyboardManagerConstants::RemapTableDropDownWidth);
typeKey.as<Button>().Content(winrt::box_value(GET_RESOURCE_STRING(IDS_TYPE_BUTTON)));
singleKeyRemapControlLayout = StackPanel();
singleKeyRemapControlLayout.as<StackPanel>().Spacing(10);
singleKeyRemapControlLayout.as<StackPanel>().Children().Append(typeKey.as<Button>());
// Key column
if (colIndex == 0)
{
keyDropDownControlObjects.emplace_back(std::make_unique<KeyDropDownControl>(false));
singleKeyRemapControlLayout.as<StackPanel>().Children().Append(keyDropDownControlObjects[0]->GetComboBox());
// Set selection handler for the drop down
keyDropDownControlObjects[0]->SetSelectionHandler(table, row, colIndex, singleKeyRemapBuffer);
}
// Hybrid column
else
{
hybridDropDownStackPanel = StackPanel();
hybridDropDownStackPanel.as<StackPanel>().Spacing(KeyboardManagerConstants::ShortcutTableDropDownSpacing);
hybridDropDownStackPanel.as<StackPanel>().Orientation(Windows::UI::Xaml::Controls::Orientation::Horizontal);
KeyDropDownControl::AddDropDown(table, row, hybridDropDownStackPanel.as<StackPanel>(), colIndex, singleKeyRemapBuffer, keyDropDownControlObjects, nullptr, true, true);
singleKeyRemapControlLayout.as<StackPanel>().Children().Append(hybridDropDownStackPanel.as<StackPanel>());
}
typeKey.as<Button>().Click([&, table, colIndex, row](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
// Using the XamlRoot of the typeKey to get the root of the XAML host
if (colIndex == 0)
{
keyboardManagerState->SetUIState(KeyboardManagerUIState::DetectSingleKeyRemapWindowActivated, EditKeyboardWindowHandle);
createDetectKeyWindow(sender, sender.as<Button>().XamlRoot(), *keyboardManagerState);
}
else
{
keyboardManagerState->SetUIState(KeyboardManagerUIState::DetectShortcutWindowInEditKeyboardWindowActivated, EditKeyboardWindowHandle);
ShortcutControl::CreateDetectShortcutWindow(sender, sender.as<Button>().XamlRoot(), *keyboardManagerState, colIndex, table, keyDropDownControlObjects, row, nullptr, true, true, EditKeyboardWindowHandle, singleKeyRemapBuffer);
}
});
singleKeyRemapControlLayout.as<StackPanel>().UpdateLayout();
}
// Function to set the accessible names for all the controls in a row
void SingleKeyRemapControl::UpdateAccessibleNames(StackPanel sourceColumn, StackPanel mappedToColumn, Button deleteButton, int rowIndex)
{
sourceColumn.SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_AUTOMATIONPROPERTIES_ROW) + std::to_wstring(rowIndex) + L", " + GET_RESOURCE_STRING(IDS_EDITKEYBOARD_SOURCEHEADER)));
mappedToColumn.SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_AUTOMATIONPROPERTIES_ROW) + std::to_wstring(rowIndex) + L", " + GET_RESOURCE_STRING(IDS_EDITKEYBOARD_TARGETHEADER)));
deleteButton.SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_AUTOMATIONPROPERTIES_ROW) + std::to_wstring(rowIndex) + L", " + GET_RESOURCE_STRING(IDS_DELETE_REMAPPING_BUTTON)));
}
// Function to add a new row to the remap keys table. If the originalKey and newKey args are provided, then the displayed remap keys are set to those values.
void SingleKeyRemapControl::AddNewControlKeyRemapRow(StackPanel& parent, std::vector<std::vector<std::unique_ptr<SingleKeyRemapControl>>>& keyboardRemapControlObjects, const DWORD originalKey, const KeyShortcutUnion newKey)
{
// Create new SingleKeyRemapControl objects dynamically so that we does not get destructed
std::vector<std::unique_ptr<SingleKeyRemapControl>> newrow;
StackPanel row = StackPanel();
parent.Children().Append(row);
newrow.emplace_back(std::make_unique<SingleKeyRemapControl>(parent, row, 0));
newrow.emplace_back(std::make_unique<SingleKeyRemapControl>(parent, row, 1));
keyboardRemapControlObjects.push_back(std::move(newrow));
row.Padding({ 10, 10, 10, 10 });
row.Orientation(Orientation::Horizontal);
auto brush = Windows::UI::Xaml::Application::Current().Resources().Lookup(box_value(L"SystemControlBackgroundListLowBrush")).as<Windows::UI::Xaml::Media::SolidColorBrush>();
if (keyboardRemapControlObjects.size() % 2)
{
row.Background(brush);
}
// SingleKeyRemapControl for the original key.
auto originalElement = keyboardRemapControlObjects.back()[0]->getSingleKeyRemapControl();
originalElement.Width(KeyboardManagerConstants::RemapTableDropDownWidth);
row.Children().Append(originalElement);
// Arrow icon
FontIcon arrowIcon;
arrowIcon.FontFamily(Media::FontFamily(L"Segoe MDL2 Assets"));
arrowIcon.Glyph(L"\xE72A");
arrowIcon.VerticalAlignment(VerticalAlignment::Center);
arrowIcon.HorizontalAlignment(HorizontalAlignment::Center);
auto arrowIconContainer = UIHelpers::GetWrapped(arrowIcon, KeyboardManagerConstants::TableArrowColWidth).as<StackPanel>();
arrowIconContainer.Orientation(Orientation::Vertical);
arrowIconContainer.VerticalAlignment(VerticalAlignment::Center);
row.Children().Append(arrowIconContainer);
// SingleKeyRemapControl for the new remap key
auto targetElement = keyboardRemapControlObjects.back()[1]->getSingleKeyRemapControl();
targetElement.Width(KeyboardManagerConstants::ShortcutTargetColumnWidth);
row.Children().Append(targetElement);
// Set the key text if the two keys are not null (i.e. default args)
if (originalKey != NULL && !(newKey.index() == 0 && std::get<DWORD>(newKey) == NULL) && !(newKey.index() == 1 && !std::get<Shortcut>(newKey).IsValidShortcut()))
{
singleKeyRemapBuffer.push_back(std::make_pair<RemapBufferItem, std::wstring>(RemapBufferItem{ originalKey, newKey }, L""));
keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][0]->keyDropDownControlObjects[0]->SetSelectedValue(std::to_wstring(originalKey));
if (newKey.index() == 0)
{
keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][1]->keyDropDownControlObjects[0]->SetSelectedValue(std::to_wstring(std::get<DWORD>(newKey)));
}
else
{
KeyDropDownControl::AddShortcutToControl(std::get<Shortcut>(newKey), parent, keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][1]->hybridDropDownStackPanel.as<StackPanel>(), *keyboardManagerState, 1, keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][1]->keyDropDownControlObjects, singleKeyRemapBuffer, row, nullptr, true, true);
}
}
else
{
// Initialize both keys to NULL
singleKeyRemapBuffer.push_back(std::make_pair<RemapBufferItem, std::wstring>(RemapBufferItem{ NULL, NULL }, L""));
}
// Delete row button
Windows::UI::Xaml::Controls::Button deleteRemapKeys;
FontIcon deleteSymbol;
deleteSymbol.FontFamily(Media::FontFamily(L"Segoe MDL2 Assets"));
deleteSymbol.Glyph(L"\xE74D");
deleteRemapKeys.Content(deleteSymbol);
deleteRemapKeys.Background(Media::SolidColorBrush(Colors::Transparent()));
deleteRemapKeys.HorizontalAlignment(HorizontalAlignment::Center);
deleteRemapKeys.Click([&, parent, row, brush](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
uint32_t rowIndex;
// Get index of delete button
UIElementCollection children = parent.Children();
bool indexFound = children.IndexOf(row, rowIndex);
// IndexOf could fail if the the row got deleted and the button handler was invoked twice. In this case it should return
if (!indexFound)
{
return;
}
// Update accessible names and background for each row after the deleted row
for (uint32_t i = rowIndex + 1; i < children.Size(); i++)
{
StackPanel row = children.GetAt(i).as<StackPanel>();
row.Background(i % 2 ? brush : Media::SolidColorBrush(Colors::Transparent()));
StackPanel sourceCol = row.Children().GetAt(0).as<StackPanel>();
StackPanel targetCol = row.Children().GetAt(2).as<StackPanel>();
Button delButton = row.Children().GetAt(3).as<Button>();
UpdateAccessibleNames(sourceCol, targetCol, delButton, i);
}
children.RemoveAt(rowIndex);
parent.UpdateLayout();
singleKeyRemapBuffer.erase(singleKeyRemapBuffer.begin() + rowIndex);
// delete the SingleKeyRemapControl objects so that they get destructed
keyboardRemapControlObjects.erase(keyboardRemapControlObjects.begin() + rowIndex);
});
// To set the accessible name of the delete button
deleteRemapKeys.SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_DELETE_REMAPPING_BUTTON)));
// Add tooltip for delete button which would appear on hover
ToolTip deleteRemapKeystoolTip;
deleteRemapKeystoolTip.Content(box_value(GET_RESOURCE_STRING(IDS_DELETE_REMAPPING_BUTTON)));
ToolTipService::SetToolTip(deleteRemapKeys, deleteRemapKeystoolTip);
row.Children().Append(deleteRemapKeys);
parent.UpdateLayout();
// Set accessible names
UpdateAccessibleNames(keyboardRemapControlObjects.back()[0]->getSingleKeyRemapControl(), keyboardRemapControlObjects.back()[1]->getSingleKeyRemapControl(), deleteRemapKeys, (int)keyboardRemapControlObjects.size());
}
// Function to return the stack panel element of the SingleKeyRemapControl. This is the externally visible UI element which can be used to add it to other layouts
StackPanel SingleKeyRemapControl::getSingleKeyRemapControl()
{
return singleKeyRemapControlLayout.as<StackPanel>();
}
// Function to create the detect remap key UI window
void SingleKeyRemapControl::createDetectKeyWindow(winrt::Windows::Foundation::IInspectable const& sender, XamlRoot xamlRoot, KeyboardManagerState& keyboardManagerState)
{
// ContentDialog for detecting remap key. This is the parent UI element.
ContentDialog detectRemapKeyBox;
// ContentDialog requires manually setting the XamlRoot (https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.contentdialog#contentdialog-in-appwindow-or-xaml-islands)
detectRemapKeyBox.XamlRoot(xamlRoot);
detectRemapKeyBox.Title(box_value(GET_RESOURCE_STRING(IDS_TYPEKEY_TITLE)));
detectRemapKeyBox.IsPrimaryButtonEnabled(false);
detectRemapKeyBox.IsSecondaryButtonEnabled(false);
// Get the linked text block for the "Type Key" button that was clicked
ComboBox linkedRemapDropDown = UIHelpers::GetSiblingElement(sender).as<ComboBox>();
auto unregisterKeys = [&keyboardManagerState]() {
keyboardManagerState.ClearRegisteredKeyDelays();
};
auto onPressEnter = [linkedRemapDropDown,
detectRemapKeyBox,
&keyboardManagerState,
unregisterKeys] {
// Save the detected key in the linked text block
DWORD detectedKey = keyboardManagerState.GetDetectedSingleRemapKey();
if (detectedKey != NULL)
{
std::vector<DWORD> keyCodeList = keyboardManagerState.keyboardMap.GetKeyCodeList();
// Update the drop down list with the new language to ensure that the correct key is displayed
linkedRemapDropDown.ItemsSource(UIHelpers::ToBoxValue(keyboardManagerState.keyboardMap.GetKeyNameList()));
linkedRemapDropDown.SelectedValue(winrt::box_value(std::to_wstring(detectedKey)));
}
// Hide the type key UI
detectRemapKeyBox.Hide();
};
auto onReleaseEnter = [&keyboardManagerState,
unregisterKeys] {
// Reset the keyboard manager UI state
keyboardManagerState.ResetUIState();
// Revert UI state back to Edit Keyboard window
keyboardManagerState.SetUIState(KeyboardManagerUIState::EditKeyboardWindowActivated, EditKeyboardWindowHandle);
unregisterKeys();
};
auto onAccept = [onPressEnter,
onReleaseEnter] {
onPressEnter();
onReleaseEnter();
};
TextBlock primaryButtonText;
primaryButtonText.Text(GET_RESOURCE_STRING(IDS_OK_BUTTON));
Button primaryButton;
primaryButton.HorizontalAlignment(HorizontalAlignment::Stretch);
primaryButton.Margin({ 2, 2, 2, 2 });
primaryButton.Content(primaryButtonText);
primaryButton.Click([onAccept](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
onAccept();
});
// NOTE: UnregisterKeys should never be called on the DelayThread, as it will re-enter the mutex. To avoid this it is run on the dispatcher thread
keyboardManagerState.RegisterKeyDelay(
VK_RETURN,
std::bind(&KeyboardManagerState::SelectDetectedRemapKey, &keyboardManagerState, std::placeholders::_1),
[primaryButton, onPressEnter, detectRemapKeyBox](DWORD) {
detectRemapKeyBox.Dispatcher().RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
[primaryButton, onPressEnter] {
// Use the base medium low brush to be consistent with the theme
primaryButton.Background(Windows::UI::Xaml::Application::Current().Resources().Lookup(box_value(L"SystemControlBackgroundBaseMediumLowBrush")).as<Windows::UI::Xaml::Media::SolidColorBrush>());
onPressEnter();
});
},
[onReleaseEnter, detectRemapKeyBox](DWORD) {
detectRemapKeyBox.Dispatcher().RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
[onReleaseEnter]() {
onReleaseEnter();
});
});
TextBlock cancelButtonText;
cancelButtonText.Text(GET_RESOURCE_STRING(IDS_CANCEL_BUTTON));
auto onCancel = [&keyboardManagerState,
detectRemapKeyBox,
unregisterKeys] {
detectRemapKeyBox.Hide();
// Reset the keyboard manager UI state
keyboardManagerState.ResetUIState();
// Revert UI state back to Edit Keyboard window
keyboardManagerState.SetUIState(KeyboardManagerUIState::EditKeyboardWindowActivated, EditKeyboardWindowHandle);
unregisterKeys();
};
Button cancelButton;
cancelButton.HorizontalAlignment(HorizontalAlignment::Stretch);
cancelButton.Margin({ 2, 2, 2, 2 });
cancelButton.Content(cancelButtonText);
// Cancel button
cancelButton.Click([onCancel](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
onCancel();
});
// NOTE: UnregisterKeys should never be called on the DelayThread, as it will re-enter the mutex. To avoid this it is run on the dispatcher thread
keyboardManagerState.RegisterKeyDelay(
VK_ESCAPE,
std::bind(&KeyboardManagerState::SelectDetectedRemapKey, &keyboardManagerState, std::placeholders::_1),
[onCancel, detectRemapKeyBox](DWORD) {
detectRemapKeyBox.Dispatcher().RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
[onCancel] {
onCancel();
});
},
nullptr);
// StackPanel parent for the displayed text in the dialog
Windows::UI::Xaml::Controls::StackPanel stackPanel;
detectRemapKeyBox.Content(stackPanel);
// Header textblock
TextBlock text;
text.Text(GET_RESOURCE_STRING(IDS_TYPEKEY_HEADER));
text.Margin({ 0, 0, 0, 10 });
stackPanel.Children().Append(text);
// Target StackPanel to place the selected key
Windows::UI::Xaml::Controls::StackPanel keyStackPanel;
keyStackPanel.Orientation(Orientation::Horizontal);
stackPanel.Children().Append(keyStackPanel);
TextBlock holdEscInfo;
holdEscInfo.Text(GET_RESOURCE_STRING(IDS_TYPE_HOLDESC));
holdEscInfo.FontSize(12);
holdEscInfo.Margin({ 0, 20, 0, 0 });
stackPanel.Children().Append(holdEscInfo);
TextBlock holdEnterInfo;
holdEnterInfo.Text(GET_RESOURCE_STRING(IDS_TYPE_HOLDENTER));
holdEnterInfo.FontSize(12);
holdEnterInfo.Margin({ 0, 0, 0, 0 });
stackPanel.Children().Append(holdEnterInfo);
ColumnDefinition primaryButtonColumn;
ColumnDefinition cancelButtonColumn;
Grid buttonPanel;
buttonPanel.Margin({ 0, 20, 0, 0 });
buttonPanel.HorizontalAlignment(HorizontalAlignment::Stretch);
buttonPanel.ColumnDefinitions().Append(primaryButtonColumn);
buttonPanel.ColumnDefinitions().Append(cancelButtonColumn);
buttonPanel.SetColumn(primaryButton, 0);
buttonPanel.SetColumn(cancelButton, 1);
buttonPanel.Children().Append(primaryButton);
buttonPanel.Children().Append(cancelButton);
stackPanel.Children().Append(buttonPanel);
stackPanel.UpdateLayout();
// Configure the keyboardManagerState to store the UI information.
keyboardManagerState.ConfigureDetectSingleKeyRemapUI(keyStackPanel);
// Show the dialog
detectRemapKeyBox.ShowAsync();
}

View File

@@ -0,0 +1,58 @@
#pragma once
#include <Shortcut.h>
#include <KeyDropDownControl.h>
class KeyboardManagerState;
namespace winrt::Windows::UI::Xaml
{
struct XamlRoot;
namespace Controls
{
struct StackPanel;
struct Grid;
struct Button;
}
}
class SingleKeyRemapControl
{
private:
// Button to type the remap key
winrt::Windows::Foundation::IInspectable typeKey;
// StackPanel to parent the above controls
winrt::Windows::Foundation::IInspectable singleKeyRemapControlLayout;
// Stack panel for the drop downs to display the selected shortcut for the hybrid case
winrt::Windows::Foundation::IInspectable hybridDropDownStackPanel;
// Function to set the accessible names for all the controls in a row
static void UpdateAccessibleNames(StackPanel sourceColumn, StackPanel mappedToColumn, Button deleteButton, int rowIndex);
public:
// Vector to store dynamically allocated KeyDropDownControl objects to avoid early destruction
std::vector<std::unique_ptr<KeyDropDownControl>> keyDropDownControlObjects;
// Handle to the current Edit Keyboard Window
static HWND EditKeyboardWindowHandle;
// Pointer to the keyboard manager state
static KeyboardManagerState* keyboardManagerState;
// Stores the current list of remappings
static RemapBuffer singleKeyRemapBuffer;
// constructor
SingleKeyRemapControl(StackPanel table, StackPanel row, const int colIndex);
// Function to add a new row to the remap keys table. If the originalKey and newKey args are provided, then the displayed remap keys are set to those values.
static void AddNewControlKeyRemapRow(StackPanel& parent, std::vector<std::vector<std::unique_ptr<SingleKeyRemapControl>>>& keyboardRemapControlObjects, const DWORD originalKey = NULL, const KeyShortcutUnion newKey = NULL);
// Function to return the stack panel element of the SingleKeyRemapControl. This is the externally visible UI element which can be used to add it to other layouts
winrt::Windows::UI::Xaml::Controls::StackPanel getSingleKeyRemapControl();
// Function to create the detect remap keys UI window
void createDetectKeyWindow(winrt::Windows::Foundation::IInspectable const& sender, XamlRoot xamlRoot, KeyboardManagerState& keyboardManagerState);
};

View File

@@ -0,0 +1,15 @@
#include "pch.h"
#include "Styles.h"
#include <common/themes/windows_colors.h>
Style AccentButtonStyle()
{
Style style{ winrt::xaml_typename<Controls::Button>() };
style.Setters().Append(Setter{
Controls::Control::BackgroundProperty(),
winrt::Windows::UI::Xaml::Media::SolidColorBrush{ WindowsColors::get_accent_color() } });
style.Setters().Append(Setter{
Controls::Control::ForegroundProperty(),
winrt::Windows::UI::Xaml::Media::SolidColorBrush{ winrt::Windows::UI::Colors::White() } });
return style;
}

View File

@@ -0,0 +1,7 @@
#pragma once
namespace winrt::Windows::UI::Xaml
{
struct Style;
}
winrt::Windows::UI::Xaml::Style AccentButtonStyle();

View File

@@ -0,0 +1,71 @@
#include "pch.h"
#include "UIHelpers.h"
#include <common/monitor_utils.h>
namespace UIHelpers
{
// This method sets focus to the first Type button on the last row of the Grid
void SetFocusOnTypeButtonInLastRow(StackPanel& parent, long colCount)
{
// First element in the last row (StackPanel)
StackPanel firstElementInLastRow = parent.Children().GetAt(parent.Children().Size() - 1).as<StackPanel>().Children().GetAt(0).as<StackPanel>();
// Type button is the first child in the StackPanel
Button firstTypeButtonInLastRow = firstElementInLastRow.Children().GetAt(0).as<Button>();
// Set programmatic focus on the button
firstTypeButtonInLastRow.Focus(FocusState::Programmatic);
}
RECT GetForegroundWindowDesktopRect()
{
HWND window = GetForegroundWindow();
HMONITOR settingsMonitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL);
RECT desktopRect{};
auto monitors = GetAllMonitorRects<&MONITORINFOEX::rcWork>();
for (const auto& monitor : monitors)
{
if (settingsMonitor == monitor.first)
{
desktopRect = monitor.second;
break;
}
}
return desktopRect;
}
// Function to return the next sibling element for an element under a stack panel
winrt::Windows::Foundation::IInspectable GetSiblingElement(winrt::Windows::Foundation::IInspectable const& element)
{
FrameworkElement frameworkElement = element.as<FrameworkElement>();
StackPanel parentElement = frameworkElement.Parent().as<StackPanel>();
uint32_t index;
parentElement.Children().IndexOf(frameworkElement, index);
return parentElement.Children().GetAt(index + 1);
}
winrt::Windows::Foundation::IInspectable GetWrapped(const winrt::Windows::Foundation::IInspectable& element, double width)
{
StackPanel sp = StackPanel();
sp.Width(width);
sp.Children().Append(element.as<FrameworkElement>());
return sp;
}
winrt::Windows::Foundation::Collections::IVector<winrt::Windows::Foundation::IInspectable> ToBoxValue(const std::vector<std::pair<DWORD, std::wstring>>& list)
{
winrt::Windows::Foundation::Collections::IVector<winrt::Windows::Foundation::IInspectable> boxList = single_threaded_vector<winrt::Windows::Foundation::IInspectable>();
for (auto& val : list)
{
auto comboBox = ComboBoxItem();
comboBox.DataContext(winrt::box_value(std::to_wstring(val.first)));
comboBox.Content(winrt::box_value(val.second));
boxList.Append(winrt::box_value(comboBox));
}
return boxList;
}
}

View File

@@ -0,0 +1,32 @@
#pragma once
namespace winrt
{
struct hstring;
namespace Windows::Foundation
{
struct IInspectable;
namespace Collections
{
template<typename T>
struct IVector;
}
}
}
// This namespace contains UI methods that are to be used for both KBM windows
namespace UIHelpers
{
// This method sets focus to the first Type button on the last row of the Grid
void SetFocusOnTypeButtonInLastRow(StackPanel& parent, long colCount);
RECT GetForegroundWindowDesktopRect();
// Function to return the next sibling element for an element under a stack panel
winrt::Windows::Foundation::IInspectable GetSiblingElement(winrt::Windows::Foundation::IInspectable const& element);
winrt::Windows::Foundation::IInspectable GetWrapped(const winrt::Windows::Foundation::IInspectable& element, double width);
// Function to return the list of key name in the order for the drop down based on the key codes
winrt::Windows::Foundation::Collections::IVector<winrt::Windows::Foundation::IInspectable> ToBoxValue(const std::vector<std::pair<DWORD, std::wstring>>& list);
}

View File

@@ -0,0 +1,333 @@
#include "pch.h"
#include "XamlBridge.h"
#include <windowsx.h>
#include <string>
bool XamlBridge::FilterMessage(const MSG* msg)
{
// When multiple child windows are present it is needed to pre dispatch messages to all
// DesktopWindowXamlSource instances so keyboard accelerators and
// keyboard focus work correctly.
BOOL xamlSourceProcessedMessage = FALSE;
{
for (auto xamlSource : m_xamlSources)
{
auto xamlSourceNative2 = xamlSource.as<IDesktopWindowXamlSourceNative2>();
const auto hr = xamlSourceNative2->PreTranslateMessage(msg, &xamlSourceProcessedMessage);
winrt::check_hresult(hr);
if (xamlSourceProcessedMessage)
{
break;
}
}
}
return !!xamlSourceProcessedMessage;
}
const auto static invalidReason = static_cast<winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason>(-1);
winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason GetReasonFromKey(WPARAM key)
{
auto reason = invalidReason;
if (key == VK_TAB)
{
byte keyboardState[256] = {};
WINRT_VERIFY(::GetKeyboardState(keyboardState));
reason = (keyboardState[VK_SHIFT] & 0x80) ?
winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Last :
winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::First;
}
else if (key == VK_LEFT)
{
reason = winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Left;
}
else if (key == VK_RIGHT)
{
reason = winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Right;
}
else if (key == VK_UP)
{
reason = winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Up;
}
else if (key == VK_DOWN)
{
reason = winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Down;
}
return reason;
}
// Function to return the next xaml island in focus
winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource XamlBridge::GetNextFocusedIsland(MSG* msg)
{
if (msg->message == WM_KEYDOWN)
{
const auto key = msg->wParam;
auto reason = GetReasonFromKey(key);
if (reason != invalidReason)
{
const BOOL previous =
(reason == winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::First ||
reason == winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Down ||
reason == winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Right) ?
false :
true;
const auto currentFocusedWindow = ::GetFocus();
const auto nextElement = ::GetNextDlgTabItem(parentWindow, currentFocusedWindow, previous);
for (auto xamlSource : m_xamlSources)
{
const auto nativeIsland = xamlSource.as<IDesktopWindowXamlSourceNative>();
HWND islandWnd = nullptr;
winrt::check_hresult(nativeIsland->get_WindowHandle(&islandWnd));
if (nextElement == islandWnd)
{
return xamlSource;
}
}
}
}
return nullptr;
}
// Function to return the xaml island currently in focus
winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource XamlBridge::GetFocusedIsland()
{
for (auto xamlSource : m_xamlSources)
{
if (xamlSource.HasFocus())
{
return xamlSource;
}
}
return nullptr;
}
// Function to handle focus navigation
bool XamlBridge::NavigateFocus(MSG* msg)
{
if (const auto nextFocusedIsland = GetNextFocusedIsland(msg))
{
const auto previousFocusedWindow = ::GetFocus();
RECT rect = {};
WINRT_VERIFY(::GetWindowRect(previousFocusedWindow, &rect));
const auto nativeIsland = nextFocusedIsland.as<IDesktopWindowXamlSourceNative>();
HWND islandWnd = nullptr;
winrt::check_hresult(nativeIsland->get_WindowHandle(&islandWnd));
POINT pt = { rect.left, rect.top };
SIZE size = { rect.right - rect.left, rect.bottom - rect.top };
::ScreenToClient(islandWnd, &pt);
const auto hintRect = winrt::Windows::Foundation::Rect({ static_cast<float>(pt.x), static_cast<float>(pt.y), static_cast<float>(size.cx), static_cast<float>(size.cy) });
const auto reason = GetReasonFromKey(msg->wParam);
const auto request = winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationRequest(reason, hintRect);
lastFocusRequestId = request.CorrelationId();
const auto result = nextFocusedIsland.NavigateFocus(request);
return result.WasFocusMoved();
}
else
{
const bool islandIsFocused = GetFocusedIsland() != nullptr;
byte keyboardState[256] = {};
WINRT_VERIFY(::GetKeyboardState(keyboardState));
const bool isMenuModifier = keyboardState[VK_MENU] & 0x80;
if (islandIsFocused && !isMenuModifier)
{
return false;
}
const bool isDialogMessage = !!IsDialogMessage(parentWindow, msg);
return isDialogMessage;
}
}
// Function to run the message loop for the xaml island window
int XamlBridge::MessageLoop()
{
MSG msg = {};
HRESULT hr = S_OK;
Logger::trace("XamlBridge::MessageLoop()");
while (GetMessage(&msg, nullptr, 0, 0))
{
const bool xamlSourceProcessedMessage = FilterMessage(&msg);
if (!xamlSourceProcessedMessage)
{
if (!NavigateFocus(&msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
Logger::trace("XamlBridge::MessageLoop() stopped");
return (int)msg.wParam;
}
static const WPARAM invalidKey = (WPARAM)-1;
WPARAM GetKeyFromReason(winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason reason)
{
auto key = invalidKey;
if (reason == winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Last || reason == winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::First)
{
key = VK_TAB;
}
else if (reason == winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Left)
{
key = VK_LEFT;
}
else if (reason == winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Right)
{
key = VK_RIGHT;
}
else if (reason == winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Up)
{
key = VK_UP;
}
else if (reason == winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Down)
{
key = VK_DOWN;
}
return key;
}
// Event triggered when focus is requested
void XamlBridge::OnTakeFocusRequested(winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource const& sender, winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSourceTakeFocusRequestedEventArgs const& args)
{
Logger::trace("XamlBridge::OnTakeFocusRequested()");
if (args.Request().CorrelationId() != lastFocusRequestId)
{
const auto reason = args.Request().Reason();
const BOOL previous =
(reason == winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::First ||
reason == winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Down ||
reason == winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Right) ?
false :
true;
const auto nativeXamlSource = sender.as<IDesktopWindowXamlSourceNative>();
HWND senderHwnd = nullptr;
winrt::check_hresult(nativeXamlSource->get_WindowHandle(&senderHwnd));
MSG msg = {};
msg.hwnd = senderHwnd;
msg.message = WM_KEYDOWN;
msg.wParam = GetKeyFromReason(reason);
if (!NavigateFocus(&msg))
{
const auto nextElement = ::GetNextDlgTabItem(parentWindow, senderHwnd, previous);
::SetFocus(nextElement);
}
}
else
{
const auto request = winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationRequest(winrt::Windows::UI::Xaml::Hosting::XamlSourceFocusNavigationReason::Restore);
lastFocusRequestId = request.CorrelationId();
sender.NavigateFocus(request);
}
}
// Function to initialise the xaml source object
HWND XamlBridge::InitDesktopWindowsXamlSource(winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource desktopSource)
{
Logger::trace("XamlBridge::InitDesktopWindowsXamlSource()");
HRESULT hr = S_OK;
winrt::init_apartment(apartment_type::single_threaded);
winxamlmanager = WindowsXamlManager::InitializeForCurrentThread();
auto interop = desktopSource.as<IDesktopWindowXamlSourceNative>();
// Parent the DesktopWindowXamlSource object to current window
hr = interop->AttachToWindow(parentWindow);
winrt::check_hresult(hr);
// Get the new child window's hwnd
HWND hWndXamlIsland = nullptr;
hr = interop->get_WindowHandle(&hWndXamlIsland);
winrt::check_hresult(hr);
m_takeFocusEventRevokers.push_back(desktopSource.TakeFocusRequested(winrt::auto_revoke, { this, &XamlBridge::OnTakeFocusRequested }));
m_xamlSources.push_back(desktopSource);
return hWndXamlIsland;
}
// Function to close and delete all the xaml source objects
void XamlBridge::ClearXamlIslands()
{
Logger::trace("XamlBridge::ClearXamlIslands() {} focus event revokers", m_takeFocusEventRevokers.size());
for (auto& takeFocusRevoker : m_takeFocusEventRevokers)
{
takeFocusRevoker.revoke();
}
m_takeFocusEventRevokers.clear();
for (auto xamlSource : m_xamlSources)
{
xamlSource.Close();
}
m_xamlSources.clear();
winxamlmanager.Close();
}
// Function invoked when the window is destroyed
void XamlBridge::OnDestroy(HWND)
{
Logger::trace("XamlBridge::OnDestroy()");
PostQuitMessage(0);
}
// Function invoked when the window is activated
void XamlBridge::OnActivate(HWND, UINT state, HWND hwndActDeact, BOOL fMinimized)
{
if (state == WA_INACTIVE)
{
Logger::trace("XamlBridge::OnActivate()");
m_hwndLastFocus = GetFocus();
}
}
// Function invoked when the window is set to focus
void XamlBridge::OnSetFocus(HWND, HWND hwndOldFocus)
{
if (m_hwndLastFocus)
{
Logger::trace("XamlBridge::OnSetFocus()");
SetFocus(m_hwndLastFocus);
}
}
std::wstring getMessageString(const UINT message)
{
switch (message)
{
case WM_NCDESTROY:
return L"WM_NCDESTROY";
case WM_ACTIVATE:
return L"WM_ACTIVATE";
case WM_SETFOCUS:
return L"WM_SETFOCUS";
default:
return L"";
}
}
// Message Handler function for Xaml Island windows
LRESULT XamlBridge::MessageHandler(UINT const message, WPARAM const wParam, LPARAM const lParam) noexcept
{
auto msg = getMessageString(message);
if (msg != L"")
{
Logger::trace(L"XamlBridge::MessageHandler() message: {}", msg);
}
switch (message)
{
HANDLE_MSG(parentWindow, WM_NCDESTROY, OnDestroy);
HANDLE_MSG(parentWindow, WM_ACTIVATE, OnActivate);
HANDLE_MSG(parentWindow, WM_SETFOCUS, OnSetFocus);
}
return DefWindowProc(parentWindow, message, wParam, lParam);
}

View File

@@ -0,0 +1,67 @@
#pragma once
// This class is used for handling XAML Island operations
class XamlBridge
{
public:
// Function to run the message loop for the xaml island window
int MessageLoop();
// Constructor
XamlBridge(HWND parent) :
parentWindow(parent), lastFocusRequestId(winrt::guid()), winxamlmanager(nullptr)
{
}
// Function to initialise the xaml source object
HWND InitDesktopWindowsXamlSource(winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource);
// Function to close and delete all the xaml source objects
void ClearXamlIslands();
// Message Handler function for Xaml Island windows
LRESULT MessageHandler(UINT const message, WPARAM const wParam, LPARAM const lParam) noexcept;
private:
// Stores the last window handle in focus
HWND m_hwndLastFocus = nullptr;
// Stores the handle of the parent native window
HWND parentWindow = nullptr;
// Window xaml manager for UI thread.
WindowsXamlManager winxamlmanager;
// Stores the GUID of the last focus request
winrt::guid lastFocusRequestId;
// Function to return the xaml island currently in focus
winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource GetFocusedIsland();
// Function to pre process the message on the xaml source object
bool FilterMessage(const MSG* msg);
// Event triggered when focus is requested
void OnTakeFocusRequested(winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource const& sender, winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSourceTakeFocusRequestedEventArgs const& args);
// Function to return the next xaml island in focus
winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource GetNextFocusedIsland(MSG* msg);
// Function to handle focus navigation
bool NavigateFocus(MSG* msg);
// Stores the focus event objects
std::vector<winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource::TakeFocusRequested_revoker> m_takeFocusEventRevokers;
// Stores the xaml source objects
std::vector<winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource> m_xamlSources;
// Function invoked when the window is destroyed
void OnDestroy(HWND);
// Function invoked when the window is activated
void OnActivate(HWND, UINT state, HWND hwndActDeact, BOOL fMinimized);
// Function invoked when the window is set to focus
void OnSetFocus(HWND, HWND hwndOldFocus);
};

View File

@@ -0,0 +1,5 @@
// pch.cpp: source file corresponding to the pre-compiled header
#include "pch.h"
// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.

View File

@@ -0,0 +1,39 @@
#pragma once
#include "targetver.h"
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#include <unknwn.h>
#include <windows.h>
#include <shellapi.h>
#include <windows.ui.xaml.hosting.desktopwindowxamlsource.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.UI.Core.h>
#include <winrt/Windows.UI.Text.h>
#pragma push_macro("GetCurrentTime")
#undef GetCurrentTime
#include <winrt/Windows.UI.Xaml.Automation.h>
#include <winrt/Windows.UI.Xaml.Controls.Primitives.h>
#include <winrt/Windows.UI.Xaml.Hosting.h>
#include <winrt/Windows.UI.Xaml.Interop.h>
#include <winrt/Windows.ui.xaml.media.h>
#pragma pop_macro("GetCurrentTime")
#include <common/logger/logger.h>
#include <common/utils/resources.h>
#include <ProjectTelemetry.h>
#include <keyboardmanager/KeyboardManagerEditor/Generated Files/resource.h>
//#include <Generated Files/resource.h>
using namespace winrt;
using namespace Windows::UI;
using namespace Windows::UI::Composition;
using namespace Windows::UI::Xaml::Hosting;
using namespace Windows::Foundation::Numerics;
using namespace Windows::UI::Xaml;
using namespace Windows::UI::Xaml::Controls;

View File

@@ -0,0 +1,6 @@
#pragma once
// // Including SDKDDKVer.h defines the highest available Windows platform.
// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.
#include <SDKDDKVer.h>

View File

@@ -0,0 +1,71 @@
#include "pch.h"
#include "trace.h"
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
void Trace::RegisterProvider() noexcept
{
TraceLoggingRegister(g_hProvider);
}
void Trace::UnregisterProvider() noexcept
{
TraceLoggingUnregister(g_hProvider);
}
// Log number of key remaps when the user uses Edit Keyboard and saves settings
void Trace::KeyRemapCount(const DWORD keyToKeyCount, const DWORD keyToShortcutCount) noexcept
{
TraceLoggingWrite(
g_hProvider,
"KeyboardManager_KeyRemapCount",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingValue(keyToKeyCount + keyToShortcutCount, "KeyRemapCount"),
TraceLoggingValue(keyToKeyCount, "KeyToKeyRemapCount"),
TraceLoggingValue(keyToShortcutCount, "KeyToShortcutRemapCount"));
}
// Log number of os level shortcut remaps when the user uses Edit Shortcuts and saves settings
void Trace::OSLevelShortcutRemapCount(const DWORD shortcutToShortcutCount, const DWORD shortcutToKeyCount) noexcept
{
TraceLoggingWrite(
g_hProvider,
"KeyboardManager_OSLevelShortcutRemapCount",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingValue(shortcutToShortcutCount + shortcutToKeyCount, "OSLevelShortcutRemapCount"),
TraceLoggingValue(shortcutToShortcutCount, "OSLevelShortcutToShortcutRemapCount"),
TraceLoggingValue(shortcutToKeyCount, "OSLevelShortcutToKeyRemapCount"));
}
// Log number of app specific shortcut remaps when the user uses Edit Shortcuts and saves settings
void Trace::AppSpecificShortcutRemapCount(const DWORD shortcutToShortcutCount, const DWORD shortcutToKeyCount) noexcept
{
TraceLoggingWrite(
g_hProvider,
"KeyboardManager_AppSpecificShortcutRemapCount",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingValue(shortcutToShortcutCount + shortcutToKeyCount, "AppSpecificShortcutRemapCount"),
TraceLoggingValue(shortcutToShortcutCount, "AppSpecificShortcutToShortcutRemapCount"),
TraceLoggingValue(shortcutToKeyCount, "AppSpecificShortcutToKeyRemapCount"));
}
// Log if an error occurs in KBM
void Trace::Error(const DWORD errorCode, std::wstring errorMessage, std::wstring methodName) noexcept
{
TraceLoggingWrite(
g_hProvider,
"KeyboardManager_Error",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingValue(methodName.c_str(), "MethodName"),
TraceLoggingValue(errorCode, "ErrorCode"),
TraceLoggingValue(errorMessage.c_str(), "ErrorMessage"));
}

View File

@@ -0,0 +1,20 @@
#pragma once
class Trace
{
public:
static void RegisterProvider() noexcept;
static void UnregisterProvider() noexcept;
// Log number of key remaps when the user uses Edit Keyboard and saves settings
static void KeyRemapCount(const DWORD keyToKeyCount, const DWORD keyToShortcutCount) noexcept;
// Log number of os level shortcut remaps when the user uses Edit Shortcuts and saves settings
static void OSLevelShortcutRemapCount(const DWORD shortcutToShortcutCount, const DWORD shortcutToKeyCount) noexcept;
// Log number of app specific shortcut remaps when the user uses Edit Shortcuts and saves settings
static void AppSpecificShortcutRemapCount(const DWORD shortcutToShortcutCount, const DWORD shortcutToKeyCount) noexcept;
// Log if an error occurs in KBM
static void Error(const DWORD errorCode, std::wstring errorMessage, std::wstring methodName) noexcept;
};