Compare commits

...

15 Commits

Author SHA1 Message Date
Zachary Teutsch
6e9a4be1a5 update output path for kbm 2025-12-10 14:12:33 -05:00
Hao Liu
161240e9c0 [Keyboard Manager] New Keyboard Manager - Text Remapping Page and Code-behind functionalities (#39888)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

> [Target feature branch ``feature/KeyboardManagerWinUI3``]

Refactor the New Keyboard Manager code to generalize functions (like
Keyboard Hook) for use across pages. Implement Code-behind and the Text
Remapping Page.

- Implement the Text Remapping page
- Restructure the Key Remappings code
- Implement unified Keyboard Hook for all Pages
- Implement Save and Delete for Text Page
- Adjust the Text Page list UI
- Create Input Control for Text Page
- Validate the user input in Text Page and show teaching tip for the
validation error
- Move Remapping Saving into Helper
- Move Remapping Delete into Helper
- Don't check for duplicates if the original keys are the same as the
remapping being edited
- Use InputMode to indicate the active toggle for Original Keys/Remapped
Keys
- Add Rectangle for all pages


![image](https://github.com/user-attachments/assets/e109d82f-01c5-4da7-9efb-facea4070808)

![image](https://github.com/user-attachments/assets/09cc4462-43eb-4823-9b01-ab630a0bf9ed)

![image](https://github.com/user-attachments/assets/9b85c0f6-cbce-40cc-8e65-b755cb852eae)

![image](https://github.com/user-attachments/assets/ff7945cc-6535-4697-812e-93c2effab14e)

![image](https://github.com/user-attachments/assets/7cf887c8-24f3-45bd-956f-b3d655844974)

![image](https://github.com/user-attachments/assets/d958bb16-66b9-44e0-8918-32ddcc780f02)
2025-06-09 15:10:16 +08:00
Hao Liu
3e69b2a411 Merge branch 'main' into feature/KeyboardManagerWinUI3 2025-04-30 10:48:15 +08:00
Hao Liu
d349d81bd5 [Keyboard Manager] New Keyboard Manager - Keys Remapping Window, Keyboard Hook and Input Validator (#39081)
* Add delete remapping function

* Fix the problem that Keyboard Hook is not cleaned properly when the toggle button lost focus (user clicked other checkbox or switched to another app)

* Make sure original toggle button is checked for InputControl

* Disable app setting for single key remapping

* Update KBM Editor UI Signing

* Ensuring Modifier Keys Don't Repeat

* Make sure the shortcut takes at most 4 modifier keys and show notification to the user

* Show teaching tip for reserved illegal shortcuts - Win+L and Ctrl+Alt+Delete

* Add validation to check if the user is trying to remap a key or shortcut that's already mapped for the same app context

* Add validation to make sure key/shortcut cannot be mapped to itself

* Add validation to check the original/new keys are not empty. Application name must be entered if the specific app checkbox is checked.

* Add validation to check shortcuts must contain at least one action key in addition to modifier keys

* Add validation to warn orphaned key to user

* Unify and consolidate the validation code

* Refactor the validation mechanism

* Delete the existing remapping when user modified the content
2025-04-29 17:01:31 +08:00
Hao Liu
ed8b9c6ade Merge branch 'main' into feature/KeyboardManagerWinUI3 2025-04-10 10:56:41 +08:00
Hao Liu
c5635c1e3e [Keyboard Manager] Functional Improvements on new KBM UX (#38652)
* Make Disable toggle button functional in Shortcuts page

* Clean up main window and aggregate Constants

* Update key remapping naming

* Fix the key visual stretch out issue when the shortcut contains too many keys

* Refactor code structure

* fix wrapping in dialog

* update naming

* Implement Keyboard hook to get the user input of remappings

* Implement Reset for InputControl to make sure InputControl can have correct status for various dialog
2025-04-09 10:30:25 +08:00
Hao Liu
c641fd17d2 Merge branch 'main' into feature/KeyboardManagerWinUI3 2025-03-12 12:25:10 +08:00
Hao Liu
4f4bcbfb53 Merge the new WinUI3 Keyboard Manager FHL change into feature branch 2025-03-11 17:04:54 +08:00
Niels Laute
85dea93a50 Design tweaks and XAML refactoring 2025-03-10 11:11:28 +01:00
chenmy77
e184808068 [FHL] Add Select Key UI in Inputcontrol (#37788)
* add press button

* add right press button

* add right press button by original button

* change toggle button position

* fix type error

* merge

* add link

* brush blue in new keys

* remove unuse annotations

* remove unuse keyvisual
2025-03-06 20:48:55 +08:00
Hao Liu
e52ac85a1b [FHL] Save Remapping Config and Data (#37787)
* load URL remapping

* Save Remapping Config and Data

* update mock data for testing

* fix app mapping

* update xaml
2025-03-06 17:56:13 +08:00
Hao Liu
1e3108efbc [FHL] Load Remapping Config and Data for Keyboard Manager new UX (#37762)
* Load remapping config and data

* display single key to shortcuts mapping properly

* get remapping with operation type so we can display them in different tabs

* clean up the xaml
2025-03-05 18:48:16 +08:00
Hao Liu
f7ed043446 [FHL] New static UX for Keyboard Manager Editor (#37747)
* Set up static new UX for Keyboard Manager Editor

* Improve the formatting and performance

* fix spelling
2025-03-04 19:09:28 +08:00
Hao Liu
d90215ee8b [FHL] Set up dev UX for Keyboard Manager Editor (#37730)
* Implement a dev version ui which is copy of current WinUI2 UX

* Implement WinUI3 dropdown and P/Invoke the key list from existing C++ Library

* Update Dllimport path
2025-03-03 10:55:44 +08:00
Hao Liu
b2dae5b48e Initial commit: Have the WinUI3 Editor up and run 2025-02-27 15:42:30 +08:00
44 changed files with 5535 additions and 32 deletions

View File

@@ -127,6 +127,7 @@
"PowerToys.KeyboardManager.dll",
"KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe",
"KeyboardManagerEditorUI\\PowerToys.KeyboardManagerEditorUI.exe",
"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe",
"PowerToys.KeyboardManagerEditorLibraryWrapper.dll",

View File

@@ -1,20 +1,709 @@
#include "pch.h"
#include "KeyboardManagerEditorLibraryWrapper.h"
#include <algorithm>
#include <cstring>
#include <vector>
#include <string>
#include <memory>
#include <common/utils/logger_helper.h>
#include <keyboardmanager/KeyboardManagerEditor/KeyboardManagerEditor.h>
#include <keyboardmanager/KeyboardManagerEditorLibrary/EditorHelpers.h>
#include <common/interop/keyboard_layout.h>
// Test function to call the remapping helper function
bool CheckIfRemappingsAreValid()
extern "C"
{
RemapBuffer remapBuffer;
void* CreateMappingConfiguration()
{
return new MappingConfiguration();
}
// Mock valid key to key remappings
remapBuffer.push_back(RemapBufferRow{ RemapBufferItem({ (DWORD)0x41, (DWORD)0x42 }), std::wstring() });
remapBuffer.push_back(RemapBufferRow{ RemapBufferItem({ (DWORD)0x42, (DWORD)0x43 }), std::wstring() });
void DestroyMappingConfiguration(void* config)
{
delete static_cast<MappingConfiguration*>(config);
}
auto result = LoadingAndSavingRemappingHelper::CheckIfRemappingsAreValid(remapBuffer);
bool LoadMappingSettings(void* config)
{
return static_cast<MappingConfiguration*>(config)->LoadSettings();
}
return result == ShortcutErrorType::NoError;
bool SaveMappingSettings(void* config)
{
return static_cast<MappingConfiguration*>(config)->SaveSettingsToFile();
}
wchar_t* AllocateAndCopyString(const std::wstring& str)
{
size_t len = str.length();
wchar_t* buffer = new wchar_t[len + 1];
wcscpy_s(buffer, len + 1, str.c_str());
return buffer;
}
int GetSingleKeyRemapCount(void* config)
{
auto mapping = static_cast<MappingConfiguration*>(config);
return static_cast<int>(mapping->singleKeyReMap.size());
}
bool GetSingleKeyRemap(void* config, int index, SingleKeyMapping* mapping)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
std::vector<std::pair<DWORD, KeyShortcutTextUnion>> allMappings;
for (const auto& kv : mappingConfig->singleKeyReMap)
{
allMappings.push_back(kv);
}
if (index < 0 || index >= allMappings.size())
{
return false;
}
const auto& kv = allMappings[index];
mapping->originalKey = static_cast<int>(kv.first);
// Remap to single key
if (kv.second.index() == 0)
{
mapping->targetKey = AllocateAndCopyString(std::to_wstring(std::get<DWORD>(kv.second)));
mapping->isShortcut = false;
}
// Remap to shortcut
else if (kv.second.index() == 1)
{
mapping->targetKey = AllocateAndCopyString(std::get<Shortcut>(kv.second).ToHstringVK().c_str());
mapping->isShortcut = true;
}
else
{
mapping->targetKey = AllocateAndCopyString(L"");
mapping->isShortcut = false;
}
return true;
}
int GetSingleKeyToTextRemapCount(void* config)
{
auto mapping = static_cast<MappingConfiguration*>(config);
return static_cast<int>(mapping->singleKeyToTextReMap.size());
}
bool GetSingleKeyToTextRemap(void* config, int index, KeyboardTextMapping* mapping)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
if (index < 0 || index >= mappingConfig->singleKeyToTextReMap.size())
{
return false;
}
auto it = mappingConfig->singleKeyToTextReMap.begin();
std::advance(it, index);
mapping->originalKey = static_cast<int>(it->first);
std::wstring text = std::get<std::wstring>(it->second);
mapping->targetText = AllocateAndCopyString(text);
return true;
}
int GetShortcutRemapCountByType(void* config, int operationType)
{
auto mapping = static_cast<MappingConfiguration*>(config);
int count = 0;
for (const auto& kv : mapping->osLevelShortcutReMap)
{
bool shouldCount = false;
if (operationType == 0)
{
if ((kv.second.targetShortcut.index() == 0) ||
(kv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(kv.second.targetShortcut).operationType == Shortcut::OperationType::RemapShortcut))
{
shouldCount = true;
}
}
else if (operationType == 1)
{
if (kv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(kv.second.targetShortcut).operationType == Shortcut::OperationType::RunProgram)
{
shouldCount = true;
}
}
else if (operationType == 2)
{
if (kv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(kv.second.targetShortcut).operationType == Shortcut::OperationType::OpenURI)
{
shouldCount = true;
}
}
else if (operationType == 3)
{
if (kv.second.targetShortcut.index() == 2)
{
shouldCount = true;
}
}
if (shouldCount)
{
count++;
}
}
for (const auto& appMap : mapping->appSpecificShortcutReMap)
{
for (const auto& shortcutKv : appMap.second)
{
bool shouldCount = false;
if (operationType == 0)
{
if ((shortcutKv.second.targetShortcut.index() == 0) ||
(shortcutKv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(shortcutKv.second.targetShortcut).operationType == Shortcut::OperationType::RemapShortcut))
{
shouldCount = true;
}
}
else if (operationType == 1)
{
if (shortcutKv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(shortcutKv.second.targetShortcut).operationType == Shortcut::OperationType::RunProgram)
{
shouldCount = true;
}
}
else if (operationType == 2)
{
if (shortcutKv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(shortcutKv.second.targetShortcut).operationType == Shortcut::OperationType::OpenURI)
{
shouldCount = true;
}
}
else if (operationType == 3)
{
if (shortcutKv.second.targetShortcut.index() == 2)
{
shouldCount = true;
}
}
if (shouldCount)
{
count++;
}
}
}
return count;
}
bool GetShortcutRemapByType(void* config, int operationType, int index, ShortcutMapping* mapping)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
std::vector<std::tuple<Shortcut, KeyShortcutTextUnion, std::wstring>> filteredMappings;
for (const auto& kv : mappingConfig->osLevelShortcutReMap)
{
bool shouldAdd = false;
if (operationType == 0) // RemapShortcut
{
if ((kv.second.targetShortcut.index() == 0) ||
(kv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(kv.second.targetShortcut).operationType == Shortcut::OperationType::RemapShortcut))
{
shouldAdd = true;
}
}
else if (operationType == 1) // RunProgram
{
if (kv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(kv.second.targetShortcut).operationType == Shortcut::OperationType::RunProgram)
{
shouldAdd = true;
}
}
else if (operationType == 2) // OpenURI
{
if (kv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(kv.second.targetShortcut).operationType == Shortcut::OperationType::OpenURI)
{
shouldAdd = true;
}
}
else if (operationType == 3)
{
if (kv.second.targetShortcut.index() == 2)
{
shouldAdd = true;
}
}
if (shouldAdd)
{
filteredMappings.push_back(std::make_tuple(kv.first, kv.second.targetShortcut, L""));
}
}
for (const auto& appKv : mappingConfig->appSpecificShortcutReMap)
{
for (const auto& shortcutKv : appKv.second)
{
bool shouldAdd = false;
if (operationType == 0) // RemapShortcut
{
if ((shortcutKv.second.targetShortcut.index() == 0) ||
(shortcutKv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(shortcutKv.second.targetShortcut).operationType == Shortcut::OperationType::RemapShortcut))
{
shouldAdd = true;
}
}
else if (operationType == 1) // RunProgram
{
if (shortcutKv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(shortcutKv.second.targetShortcut).operationType == Shortcut::OperationType::RunProgram)
{
shouldAdd = true;
}
}
else if (operationType == 2) // OpenURI
{
if (shortcutKv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(shortcutKv.second.targetShortcut).operationType == Shortcut::OperationType::OpenURI)
{
shouldAdd = true;
}
}
else if (operationType == 3)
{
if (shortcutKv.second.targetShortcut.index() == 2)
{
shouldAdd = true;
}
}
if (shouldAdd)
{
filteredMappings.push_back(std::make_tuple(
shortcutKv.first, shortcutKv.second.targetShortcut, appKv.first));
}
}
}
if (index < 0 || index >= filteredMappings.size())
{
return false;
}
const auto& [origShortcut, targetShortcutUnion, app] = filteredMappings[index];
std::wstring origKeysStr = origShortcut.ToHstringVK().c_str();
mapping->originalKeys = AllocateAndCopyString(origKeysStr);
mapping->targetApp = AllocateAndCopyString(app);
if (targetShortcutUnion.index() == 0)
{
DWORD targetKey = std::get<DWORD>(targetShortcutUnion);
mapping->targetKeys = AllocateAndCopyString(std::to_wstring(targetKey));
mapping->operationType = 0;
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcutUnion.index() == 1)
{
Shortcut targetShortcut = std::get<Shortcut>(targetShortcutUnion);
std::wstring targetKeysStr = targetShortcut.ToHstringVK().c_str();
mapping->operationType = static_cast<int>(targetShortcut.operationType);
if (targetShortcut.operationType == Shortcut::OperationType::RunProgram)
{
mapping->targetKeys = AllocateAndCopyString(targetKeysStr);
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(targetShortcut.runProgramFilePath);
mapping->programArgs = AllocateAndCopyString(targetShortcut.runProgramArgs);
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcut.operationType == Shortcut::OperationType::OpenURI)
{
mapping->targetKeys = AllocateAndCopyString(targetKeysStr);
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(targetShortcut.uriToOpen);
}
else
{
mapping->targetKeys = AllocateAndCopyString(targetKeysStr);
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(L"");
}
}
else if (targetShortcutUnion.index() == 2)
{
std::wstring text = std::get<std::wstring>(targetShortcutUnion);
mapping->targetKeys = AllocateAndCopyString(L"");
mapping->operationType = 0;
mapping->targetText = AllocateAndCopyString(text);
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(L"");
}
return true;
}
int GetShortcutRemapCount(void* config)
{
auto mapping = static_cast<MappingConfiguration*>(config);
int count = static_cast<int>(mapping->osLevelShortcutReMap.size());
for (const auto& appMap : mapping->appSpecificShortcutReMap)
{
count += static_cast<int>(appMap.second.size());
}
return count;
}
bool GetShortcutRemap(void* config, int index, ShortcutMapping* mapping)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
std::vector<std::tuple<Shortcut, KeyShortcutTextUnion, std::wstring>> allMappings;
for (const auto& kv : mappingConfig->osLevelShortcutReMap)
{
allMappings.push_back(std::make_tuple(kv.first, kv.second.targetShortcut, L""));
}
for (const auto& appKv : mappingConfig->appSpecificShortcutReMap)
{
for (const auto& shortcutKv : appKv.second)
{
allMappings.push_back(std::make_tuple(
shortcutKv.first, shortcutKv.second.targetShortcut, appKv.first));
}
}
if (index < 0 || index >= allMappings.size())
{
return false;
}
const auto& [origShortcut, targetShortcutUnion, app] = allMappings[index];
std::wstring origKeysStr = origShortcut.ToHstringVK().c_str();
mapping->originalKeys = AllocateAndCopyString(origKeysStr);
mapping->targetApp = AllocateAndCopyString(app);
if (targetShortcutUnion.index() == 0)
{
DWORD targetKey = std::get<DWORD>(targetShortcutUnion);
mapping->targetKeys = AllocateAndCopyString(std::to_wstring(targetKey));
mapping->operationType = 0;
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcutUnion.index() == 1)
{
Shortcut targetShortcut = std::get<Shortcut>(targetShortcutUnion);
std::wstring targetKeysStr = targetShortcut.ToHstringVK().c_str();
mapping->operationType = static_cast<int>(targetShortcut.operationType);
if (targetShortcut.operationType == Shortcut::OperationType::RunProgram)
{
mapping->targetKeys = AllocateAndCopyString(targetKeysStr);
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(targetShortcut.runProgramFilePath);
mapping->programArgs = AllocateAndCopyString(targetShortcut.runProgramArgs);
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcut.operationType == Shortcut::OperationType::OpenURI)
{
mapping->targetKeys = AllocateAndCopyString(targetKeysStr);
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(targetShortcut.uriToOpen);
}
else
{
mapping->targetKeys = AllocateAndCopyString(targetKeysStr);
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(L"");
}
}
else if (targetShortcutUnion.index() == 2)
{
std::wstring text = std::get<std::wstring>(targetShortcutUnion);
mapping->targetKeys = AllocateAndCopyString(L"");
mapping->operationType = 0;
mapping->targetText = AllocateAndCopyString(text);
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(L"");
}
return true;
}
void FreeString(wchar_t* str)
{
delete[] str;
}
bool AddSingleKeyRemap(void* config, int originalKey, int targetKey)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
return mappingConfig->AddSingleKeyRemap(static_cast<DWORD>(originalKey), static_cast<DWORD>(targetKey));
}
bool AddSingleKeyToTextRemap(void* config, int originalKey, const wchar_t* text)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
if (text == nullptr)
{
return false;
}
return mappingConfig->AddSingleKeyToTextRemap(static_cast<DWORD>(originalKey), text);
}
bool AddSingleKeyToShortcutRemap(void* config, int originalKey, const wchar_t* targetKeys)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
if (!targetKeys)
{
return false;
}
Shortcut targetShortcut(targetKeys);
return mappingConfig->AddSingleKeyRemap(static_cast<DWORD>(originalKey), targetShortcut);
}
bool AddShortcutRemap(void* config,
const wchar_t* originalKeys,
const wchar_t* targetKeys,
const wchar_t* targetApp,
int operationType = 0)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
Shortcut originalShortcut(originalKeys);
KeyShortcutTextUnion targetShortcut;
switch (operationType)
{
case 3:
targetShortcut = std::wstring(targetKeys);
break;
default:
targetShortcut = Shortcut(targetKeys);
std::get<Shortcut>(targetShortcut).operationType = static_cast<Shortcut::OperationType>(operationType);
break;
}
std::wstring app(targetApp ? targetApp : L"");
if (app.empty())
{
return mappingConfig->AddOSLevelShortcut(originalShortcut, targetShortcut);
}
else
{
return mappingConfig->AddAppSpecificShortcut(app, originalShortcut, targetShortcut);
}
}
void GetKeyDisplayName(int keyCode, wchar_t* keyName, int maxCount)
{
if (keyName == nullptr || maxCount <= 0)
{
return;
}
LayoutMap layoutMap;
std::wstring name = layoutMap.GetKeyName(static_cast<DWORD>(keyCode));
wcsncpy_s(keyName, maxCount, name.c_str(), _TRUNCATE);
}
int GetKeyCodeFromName(const wchar_t* keyName)
{
if (keyName == nullptr)
{
return 0;
}
LayoutMap layoutMap;
std::wstring name(keyName);
return static_cast<int>(layoutMap.GetKeyFromName(name));
}
// Function to get the type of a key (Win, Ctrl, Alt, Shift, or Action)
int GetKeyType(int key)
{
return static_cast<int>(Helpers::GetKeyType(static_cast<DWORD>(key)));
}
// Function to check if a shortcut is illegal
bool IsShortcutIllegal(const wchar_t* shortcutKeys)
{
if (!shortcutKeys)
{
return false;
}
Shortcut shortcut(shortcutKeys);
ShortcutErrorType result = EditorHelpers::IsShortcutIllegal(shortcut);
// Return true if an error was detected (anything other than NoError)
return result != ShortcutErrorType::NoError;
}
// Function to check if two shortcuts are equal
bool AreShortcutsEqual(const wchar_t* lShort, const wchar_t* rShort)
{
if (!lShort || !rShort)
{
return false;
}
Shortcut lhs(lShort);
Shortcut rhs(rShort);
return lhs == rhs;
}
// Function to delete a single key remapping
bool DeleteSingleKeyRemap(void* config, int originalKey)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
// Find and delete the single key remapping
auto it = mappingConfig->singleKeyReMap.find(static_cast<DWORD>(originalKey));
if (it != mappingConfig->singleKeyReMap.end())
{
mappingConfig->singleKeyReMap.erase(it);
return true;
}
return false;
}
bool DeleteSingleKeyToTextRemap(void* config, int originalKey)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
auto it = mappingConfig->singleKeyToTextReMap.find(originalKey);
if (it != mappingConfig->singleKeyToTextReMap.end())
{
mappingConfig->singleKeyToTextReMap.erase(it);
return true;
}
return false;
}
// Function to delete a shortcut remapping
bool DeleteShortcutRemap(void* config, const wchar_t* originalKeys, const wchar_t* targetApp)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
if (originalKeys == nullptr)
{
return false;
}
std::wstring appName = targetApp ? targetApp : L"";
Shortcut shortcut(originalKeys);
// Determine the type of remapping to delete based on the app name
if (appName.empty())
{
// Delete OS level shortcut mapping
auto it = mappingConfig->osLevelShortcutReMap.find(shortcut);
if (it != mappingConfig->osLevelShortcutReMap.end())
{
mappingConfig->osLevelShortcutReMap.erase(it);
return true;
}
}
else
{
// Delete app-specific shortcut mapping
auto appIt = mappingConfig->appSpecificShortcutReMap.find(appName);
if (appIt != mappingConfig->appSpecificShortcutReMap.end())
{
auto shortcutIt = appIt->second.find(shortcut);
if (shortcutIt != appIt->second.end())
{
appIt->second.erase(shortcutIt);
// If the app-specific mapping is empty, remove the app entry
if (appIt->second.empty())
{
mappingConfig->appSpecificShortcutReMap.erase(appIt);
}
return true;
}
}
}
return false;
}
}
// Get the list of keyboard keys in Editor
int GetKeyboardKeysList(bool isShortcut, KeyNamePair* keyList, int maxCount)
{
if (keyList == nullptr || maxCount <= 0)
{
return 0;
}
LayoutMap layoutMap;
auto keyNameList = layoutMap.GetKeyNameList(isShortcut);
int count = (std::min)(static_cast<int>(keyNameList.size()), maxCount);
// Transfer the key list to the output struct format
for (int i = 0; i < count; ++i)
{
keyList[i].keyCode = static_cast<int>(keyNameList[i].first);
wcsncpy_s(keyList[i].keyName, keyNameList[i].second.c_str(), _countof(keyList[i].keyName) - 1);
}
return count;
}

View File

@@ -4,4 +4,77 @@
#include <keyboardmanager/common/Input.h>
#include <keyboardmanager/common/MappingConfiguration.h>
extern "C" __declspec(dllexport) bool CheckIfRemappingsAreValid();
struct KeyNamePair
{
int keyCode;
wchar_t keyName[64];
};
struct SingleKeyMapping
{
int originalKey;
wchar_t* targetKey;
bool isShortcut;
};
struct KeyboardTextMapping
{
int originalKey;
wchar_t* targetText;
};
struct ShortcutMapping
{
wchar_t* originalKeys;
wchar_t* targetKeys;
wchar_t* targetApp;
int operationType;
wchar_t* targetText;
wchar_t* programPath;
wchar_t* programArgs;
wchar_t* uriToOpen;
};
extern "C"
{
__declspec(dllexport) void* CreateMappingConfiguration();
__declspec(dllexport) void DestroyMappingConfiguration(void* config);
__declspec(dllexport) bool LoadMappingSettings(void* config);
__declspec(dllexport) bool SaveMappingSettings(void* config);
__declspec(dllexport) int GetSingleKeyRemapCount(void* config);
__declspec(dllexport) bool GetSingleKeyRemap(void* config, int index, SingleKeyMapping* mapping);
__declspec(dllexport) int GetSingleKeyToTextRemapCount(void* config);
__declspec(dllexport) bool GetSingleKeyToTextRemap(void* config, int index, KeyboardTextMapping* mapping);
__declspec(dllexport) int GetShortcutRemapCountByType(void* config, int operationType);
__declspec(dllexport) bool GetShortcutRemapByType(void* config, int operationType, int index, ShortcutMapping* mapping);
__declspec(dllexport) int GetShortcutRemapCount(void* config);
__declspec(dllexport) bool GetShortcutRemap(void* config, int index, ShortcutMapping* mapping);
__declspec(dllexport) bool AddSingleKeyRemap(void* config, int originalKey, int targetKey);
__declspec(dllexport) bool AddSingleKeyToTextRemap(void* config, int originalKey, const wchar_t* text);
__declspec(dllexport) bool AddSingleKeyToShortcutRemap(void* config,
int originalKey,
const wchar_t* targetKeys);
__declspec(dllexport) bool AddShortcutRemap(void* config,
const wchar_t* originalKeys,
const wchar_t* targetKeys,
const wchar_t* targetApp,
int operationType);
__declspec(dllexport) void GetKeyDisplayName(int keyCode, wchar_t* keyName, int maxCount);
__declspec(dllexport) int GetKeyCodeFromName(const wchar_t* keyName);
__declspec(dllexport) void FreeString(wchar_t* str);
__declspec(dllexport) int GetKeyType(int keyCode);
__declspec(dllexport) bool IsShortcutIllegal(const wchar_t* shortcutKeys);
__declspec(dllexport) bool AreShortcutsEqual(const wchar_t* lShort, const wchar_t* rShort);
__declspec(dllexport) bool DeleteSingleKeyRemap(void* config, int originalKey);
__declspec(dllexport) bool DeleteSingleKeyToTextRemap(void* config, int originalKey);
__declspec(dllexport) bool DeleteShortcutRemap(void* config, const wchar_t* originalKeys, const wchar_t* targetApp);
}
extern "C" __declspec(dllexport) int GetKeyboardKeysList(bool isShortcut, KeyNamePair* keyList, int maxCount);

View File

@@ -8,7 +8,8 @@
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here -->
<ResourceDictionary Source="/Styles/KeyVisual.xaml" />
<ResourceDictionary Source="/Styles/CommonStyle.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>

View File

@@ -7,7 +7,11 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Helpers;
using ManagedCommon;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
@@ -35,8 +39,13 @@ namespace KeyboardManagerEditorUI
public App()
{
this.InitializeComponent();
Logger.InitializeLogger("\\Keyboard Manager\\WinUI3Editor\\Logs");
Logger.LogInfo("keyboard-manager WinUI3 editor logger is initialized");
Task.Run(() =>
{
Logger.InitializeLogger("\\Keyboard Manager\\WinUI3Editor\\Logs");
});
UnhandledException += App_UnhandledException;
}
/// <summary>
@@ -46,10 +55,37 @@ namespace KeyboardManagerEditorUI
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
window = new MainWindow();
window.Activate();
var appWindow = window.AppWindow;
var windowSize = new Windows.Graphics.SizeInt32(EditorConstants.DefaultEditorWindowWidth, EditorConstants.DefaultEditorWindowHeight);
appWindow.Resize(windowSize);
window.DispatcherQueue.TryEnqueue(() =>
{
window.Activate();
window.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
(window.Content as FrameworkElement)?.UpdateLayout();
});
});
Logger.LogInfo("keyboard-manager WinUI3 editor window is launched");
}
/// <summary>
/// Log the unhandled exception for the editor.
/// </summary>
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError("Unhandled exception", e.Exception);
}
public Window? GetWindow()
{
return window;
}
private Window? window;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Helpers
{
public static class EditorConstants
{
// Default window size
public const int DefaultEditorWindowWidth = 960;
public const int DefaultEditorWindowHeight = 600;
// Default notification timeout
public const int DefaultNotificationTimeout = 1500;
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace KeyboardManagerEditorUI.Helpers
{
public enum KeyInputMode
{
OriginalKeys,
RemappedKeys,
}
}

View File

@@ -0,0 +1,239 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Interop;
using Microsoft.PowerToys.Settings.UI.Library;
using Windows.System;
namespace KeyboardManagerEditorUI.Helpers
{
public class KeyboardHookHelper : IDisposable
{
private static KeyboardHookHelper? _instance;
public static KeyboardHookHelper Instance => _instance ??= new KeyboardHookHelper();
private KeyboardMappingService _mappingService;
private HotkeySettingsControlHook? _keyboardHook;
// The active page using this keyboard hook
private IKeyboardHookTarget? _activeTarget;
private HashSet<VirtualKey> _currentlyPressedKeys = new();
private List<VirtualKey> _keyPressOrder = new();
private bool _disposed;
// Singleton to make sure only one instance of the hook is active
private KeyboardHookHelper()
{
_mappingService = new KeyboardMappingService();
}
public void ActivateHook(IKeyboardHookTarget target)
{
CleanupHook();
_activeTarget = target;
_currentlyPressedKeys.Clear();
_keyPressOrder.Clear();
_keyboardHook = new HotkeySettingsControlHook(
KeyDown,
KeyUp,
() => true,
(key, extraInfo) => true);
}
public void CleanupHook()
{
if (_keyboardHook != null)
{
_keyboardHook.Dispose();
_keyboardHook = null;
}
_currentlyPressedKeys.Clear();
_keyPressOrder.Clear();
_activeTarget = null;
}
private void KeyDown(int key)
{
if (_activeTarget == null)
{
return;
}
VirtualKey virtualKey = (VirtualKey)key;
if (_currentlyPressedKeys.Contains(virtualKey))
{
return;
}
// if no keys are pressed, clear the lists when a new key is pressed
if (_currentlyPressedKeys.Count == 0)
{
_activeTarget.ClearKeys();
_keyPressOrder.Clear();
}
// Count current modifiers
int modifierCount = _currentlyPressedKeys.Count(k => RemappingHelper.IsModifierKey(k));
// If adding this key would exceed the limits (4 modifiers + 1 action key), don't add it and show notification
if ((RemappingHelper.IsModifierKey(virtualKey) && modifierCount >= 4) ||
(!RemappingHelper.IsModifierKey(virtualKey) && _currentlyPressedKeys.Count >= 5))
{
_activeTarget.OnInputLimitReached();
return;
}
// Check if this is a different variant of a modifier key already pressed
if (RemappingHelper.IsModifierKey(virtualKey))
{
// Remove existing variant of this modifier key if a new one is pressed
// This is to ensure that only one variant of a modifier key is displayed at a time
RemoveExistingModifierVariant(virtualKey);
}
if (_currentlyPressedKeys.Add(virtualKey))
{
_keyPressOrder.Add(virtualKey);
// Notify the target page
_activeTarget.OnKeyDown(virtualKey, GetFormattedKeyList());
}
}
private void KeyUp(int key)
{
if (_activeTarget == null)
{
return;
}
VirtualKey virtualKey = (VirtualKey)key;
if (_currentlyPressedKeys.Remove(virtualKey))
{
_keyPressOrder.Remove(virtualKey);
_activeTarget.OnKeyUp(virtualKey, GetFormattedKeyList());
}
}
// Display the modifier keys and the action key in order, e.g. "Ctrl + Alt + A"
private List<string> GetFormattedKeyList()
{
if (_activeTarget == null)
{
return new List<string>();
}
List<string> keyList = new List<string>();
List<VirtualKey> modifierKeys = new List<VirtualKey>();
VirtualKey? actionKey = null;
foreach (var key in _keyPressOrder)
{
if (!_currentlyPressedKeys.Contains(key))
{
continue;
}
if (RemappingHelper.IsModifierKey(key))
{
if (!modifierKeys.Contains(key))
{
modifierKeys.Add(key);
}
}
else
{
actionKey = key;
}
}
foreach (var key in modifierKeys)
{
keyList.Add(_mappingService.GetKeyDisplayName((int)key));
}
if (actionKey.HasValue)
{
keyList.Add(_mappingService.GetKeyDisplayName((int)actionKey.Value));
}
return keyList;
}
private void RemoveExistingModifierVariant(VirtualKey key)
{
KeyType keyType = (KeyType)KeyboardManagerInterop.GetKeyType((int)key);
// No need to remove if the key is an action key
if (keyType == KeyType.Action)
{
return;
}
foreach (var existingKey in _currentlyPressedKeys.ToList())
{
if (existingKey != key)
{
KeyType existingKeyType = (KeyType)KeyboardManagerInterop.GetKeyType((int)existingKey);
// Remove the existing key if it is a modifier key and has the same type as the new key
if (existingKeyType == keyType)
{
_currentlyPressedKeys.Remove(existingKey);
_keyPressOrder.Remove(existingKey);
}
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
CleanupHook();
_mappingService?.Dispose();
}
_disposed = true;
}
}
}
public interface IKeyboardHookTarget
{
void OnKeyDown(VirtualKey key, List<string> formattedKeys);
void OnKeyUp(VirtualKey key, List<string> formattedKeys)
{
}
void ClearKeys();
void OnInputLimitReached();
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Helpers
{
public partial class Remapping : INotifyPropertyChanged
{
public List<string> OriginalKeys { get; set; } = new List<string>();
public List<string> RemappedKeys { get; set; } = new List<string>();
public bool IsAllApps { get; set; } = true;
public string AppName { get; set; } = "All Apps";
private bool IsEnabledValue { get; set; } = true;
public event PropertyChangedEventHandler? PropertyChanged;
public bool IsEnabled
{
get => IsEnabledValue;
set
{
if (IsEnabledValue != value)
{
IsEnabledValue = value;
OnPropertyChanged();
}
}
}
private void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -0,0 +1,144 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Interop;
using ManagedCommon;
using Windows.System;
namespace KeyboardManagerEditorUI.Helpers
{
public static class RemappingHelper
{
public static bool SaveMapping(KeyboardMappingService mappingService, List<string> originalKeys, List<string> remappedKeys, bool isAppSpecific, string appName)
{
if (mappingService == null)
{
Logger.LogError("Mapping service is null, cannot save mapping");
return false;
}
try
{
if (originalKeys == null || originalKeys.Count == 0 || remappedKeys == null || remappedKeys.Count == 0)
{
return false;
}
if (originalKeys.Count == 1)
{
int originalKey = mappingService.GetKeyCodeFromName(originalKeys[0]);
if (originalKey != 0)
{
if (remappedKeys.Count == 1)
{
int targetKey = mappingService.GetKeyCodeFromName(remappedKeys[0]);
if (targetKey != 0)
{
mappingService.AddSingleKeyMapping(originalKey, targetKey);
}
}
else
{
string targetKeys = string.Join(";", remappedKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
mappingService.AddSingleKeyMapping(originalKey, targetKeys);
}
}
}
else
{
string originalKeysString = string.Join(";", originalKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
string targetKeysString = string.Join(";", remappedKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
if (isAppSpecific && !string.IsNullOrEmpty(appName))
{
mappingService.AddShortcutMapping(originalKeysString, targetKeysString, appName);
}
else
{
mappingService.AddShortcutMapping(originalKeysString, targetKeysString);
}
}
return mappingService.SaveSettings();
}
catch (Exception ex)
{
Logger.LogError("Error saving mapping: " + ex.Message);
return false;
}
}
public static bool DeleteRemapping(KeyboardMappingService mappingService, Remapping remapping)
{
if (mappingService == null)
{
return false;
}
try
{
if (remapping.OriginalKeys.Count == 1)
{
// Single key mapping
int originalKey = mappingService.GetKeyCodeFromName(remapping.OriginalKeys[0]);
if (originalKey != 0)
{
if (mappingService.DeleteSingleKeyMapping(originalKey))
{
// Save settings after successful deletion
return mappingService.SaveSettings();
}
}
}
else if (remapping.OriginalKeys.Count > 1)
{
// Shortcut mapping
string originalKeysString = string.Join(";", remapping.OriginalKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
bool deleteResult;
if (!remapping.IsAllApps && !string.IsNullOrEmpty(remapping.AppName))
{
// App-specific shortcut key mapping
deleteResult = mappingService.DeleteShortcutMapping(originalKeysString, remapping.AppName);
}
else
{
// Global shortcut key mapping
deleteResult = mappingService.DeleteShortcutMapping(originalKeysString);
}
return deleteResult ? mappingService.SaveSettings() : false;
}
return false;
}
catch (Exception ex)
{
Logger.LogError($"Error deleting remapping: {ex.Message}");
return false;
}
}
public static bool IsModifierKey(VirtualKey key)
{
return key == VirtualKey.Control
|| key == VirtualKey.LeftControl
|| key == VirtualKey.RightControl
|| key == VirtualKey.Menu
|| key == VirtualKey.LeftMenu
|| key == VirtualKey.RightMenu
|| key == VirtualKey.Shift
|| key == VirtualKey.LeftShift
|| key == VirtualKey.RightShift
|| key == VirtualKey.LeftWindows
|| key == VirtualKey.RightWindows;
}
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Helpers
{
public class TextMapping
{
public List<string> Keys { get; set; } = new List<string>();
public string Text { get; set; } = string.Empty;
public bool IsAllApps { get; set; } = true;
public string AppName { get; set; } = "All Apps";
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Helpers
{
public class URLShortcut
{
public List<string> Shortcut { get; set; } = new List<string>();
public string URL { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Helpers
{
public enum ValidationErrorType
{
NoError,
EmptyOriginalKeys,
EmptyRemappedKeys,
ModifierOnly,
EmptyAppName,
IllegalShortcut,
DuplicateMapping,
SelfMapping,
EmptyTargetText,
}
}

View File

@@ -0,0 +1,299 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Interop;
namespace KeyboardManagerEditorUI.Helpers
{
public static class ValidationHelper
{
public static readonly Dictionary<ValidationErrorType, (string Title, string Message)> ValidationMessages = new()
{
{ ValidationErrorType.EmptyOriginalKeys, ("Missing Original Keys", "Please enter at least one original key to create a remapping.") },
{ ValidationErrorType.EmptyRemappedKeys, ("Missing Target Keys", "Please enter at least one target key to create a remapping.") },
{ ValidationErrorType.ModifierOnly, ("Invalid Shortcut", "Shortcuts must contain at least one action key in addition to modifier keys (Ctrl, Alt, Shift, Win).") },
{ ValidationErrorType.EmptyAppName, ("Missing Application Name", "You've selected app-specific remapping but haven't specified an application name. Please enter the application name.") },
{ ValidationErrorType.IllegalShortcut, ("Reserved System Shortcut", "Win+L and Ctrl+Alt+Delete are reserved system shortcuts and cannot be remapped.") },
{ ValidationErrorType.DuplicateMapping, ("Duplicate Remapping", "This key or shortcut is already remapped.") },
{ ValidationErrorType.SelfMapping, ("Invalid Remapping", "A key or shortcut cannot be remapped to itself. Please choose a different target.") },
{ ValidationErrorType.EmptyTargetText, ("Missing Target Text", "Please enter the text to be inserted when the shortcut is pressed.") },
};
public static ValidationErrorType ValidateKeyMapping(
List<string> originalKeys,
List<string> remappedKeys,
bool isAppSpecific,
string appName,
KeyboardMappingService mappingService,
bool isEditMode = false,
Remapping? editingRemapping = null)
{
// Check if original keys are empty
if (originalKeys == null || originalKeys.Count == 0)
{
return ValidationErrorType.EmptyOriginalKeys;
}
// Check if remapped keys are empty
if (remappedKeys == null || remappedKeys.Count == 0)
{
return ValidationErrorType.EmptyRemappedKeys;
}
// Check if shortcut contains only modifier keys
if ((originalKeys.Count > 1 && ContainsOnlyModifierKeys(originalKeys)) ||
(remappedKeys.Count > 1 && ContainsOnlyModifierKeys(remappedKeys)))
{
return ValidationErrorType.ModifierOnly;
}
// Check if app specific is checked but no app name is provided
if (isAppSpecific && string.IsNullOrWhiteSpace(appName))
{
return ValidationErrorType.EmptyAppName;
}
// Check if this is a shortcut (multiple keys) and if it's an illegal combination
if (originalKeys.Count > 1)
{
string shortcutKeysString = string.Join(";", originalKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
if (KeyboardManagerInterop.IsShortcutIllegal(shortcutKeysString))
{
return ValidationErrorType.IllegalShortcut;
}
}
// Check for duplicate mappings
if (IsDuplicateMapping(originalKeys, isAppSpecific, appName, mappingService, isEditMode, editingRemapping))
{
return ValidationErrorType.DuplicateMapping;
}
// Check for self-mapping
if (IsSelfMapping(originalKeys, remappedKeys, mappingService))
{
return ValidationErrorType.SelfMapping;
}
return ValidationErrorType.NoError;
}
public static ValidationErrorType ValidateTextMapping(
List<string> keys,
string textContent,
bool isAppSpecific,
string appName,
KeyboardMappingService mappingService)
{
// Check if original keys are empty
if (keys == null || keys.Count == 0)
{
return ValidationErrorType.EmptyOriginalKeys;
}
// Check if text content is empty
if (string.IsNullOrWhiteSpace(textContent))
{
return ValidationErrorType.EmptyTargetText;
}
// Check if shortcut contains only modifier keys
if (keys.Count > 1 && ContainsOnlyModifierKeys(keys))
{
return ValidationErrorType.ModifierOnly;
}
// Check if app specific is checked but no app name is provided
if (isAppSpecific && string.IsNullOrWhiteSpace(appName))
{
return ValidationErrorType.EmptyAppName;
}
// Check if this is a shortcut (multiple keys) and if it's an illegal combination
if (keys.Count > 1)
{
string shortcutKeysString = string.Join(";", keys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
if (KeyboardManagerInterop.IsShortcutIllegal(shortcutKeysString))
{
return ValidationErrorType.IllegalShortcut;
}
}
// No errors found
return ValidationErrorType.NoError;
}
public static bool IsDuplicateMapping(
List<string> originalKeys,
bool isAppSpecific,
string appName,
KeyboardMappingService mappingService,
bool isEditMode = false,
Remapping? editingRemapping = null)
{
if (mappingService == null || originalKeys == null || originalKeys.Count == 0)
{
return false;
}
// For single key remapping
if (originalKeys.Count == 1)
{
int originalKeyCode = mappingService.GetKeyCodeFromName(originalKeys[0]);
if (originalKeyCode == 0)
{
return false;
}
// Check if the key is already remapped
foreach (var mapping in mappingService.GetSingleKeyMappings())
{
if (mapping.OriginalKey == originalKeyCode)
{
// Skip if the remapping is the same as the one being edited
if (isEditMode && editingRemapping != null &&
editingRemapping.OriginalKeys.Count == 1 &&
mappingService.GetKeyCodeFromName(editingRemapping.OriginalKeys[0]) == originalKeyCode)
{
continue;
}
return true;
}
}
}
// For shortcut remapping
else
{
string originalKeysString = string.Join(";", originalKeys.Select(
k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
// Don't check for duplicates if the original keys are the same as the remapping being edited
bool isEditingExistingRemapping = false;
if (isEditMode && editingRemapping != null)
{
string editingOriginalKeysString = string.Join(";", editingRemapping.OriginalKeys.Select(k =>
mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
if (KeyboardManagerInterop.AreShortcutsEqual(originalKeysString, editingOriginalKeysString))
{
isEditingExistingRemapping = true;
}
}
// Check if the shortcut is already remapped in the same app context
foreach (var mapping in mappingService.GetShortcutMappingsByType(ShortcutOperationType.RemapShortcut))
{
if (KeyboardManagerInterop.AreShortcutsEqual(originalKeysString, mapping.OriginalKeys))
{
// If both are global (all apps)
if (!isAppSpecific && string.IsNullOrEmpty(mapping.TargetApp))
{
// Skip if the remapping is the same as the one being edited
if (editingRemapping != null && editingRemapping.OriginalKeys.Count > 1 && editingRemapping.IsAllApps && isEditingExistingRemapping)
{
continue;
}
return true;
}
// If both are for the same specific app
else if (isAppSpecific && !string.IsNullOrEmpty(mapping.TargetApp)
&& string.Equals(mapping.TargetApp, appName, StringComparison.OrdinalIgnoreCase))
{
// Skip if the remapping is the same as the one being edited
if (editingRemapping != null && editingRemapping.OriginalKeys.Count > 1 && !editingRemapping.IsAllApps &&
string.Equals(editingRemapping.AppName, appName, StringComparison.OrdinalIgnoreCase) && isEditingExistingRemapping)
{
continue;
}
return true;
}
}
}
}
return false;
}
public static bool IsSelfMapping(List<string> originalKeys, List<string> remappedKeys, KeyboardMappingService mappingService)
{
if (mappingService == null)
{
return false;
}
// If either list is empty, it's not a self-mapping
if (originalKeys == null || remappedKeys == null ||
originalKeys.Count == 0 || remappedKeys.Count == 0)
{
return false;
}
string originalKeysString = string.Join(";", originalKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
string remappedKeysString = string.Join(";", remappedKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
return KeyboardManagerInterop.AreShortcutsEqual(originalKeysString, remappedKeysString);
}
public static bool ContainsOnlyModifierKeys(List<string> keys)
{
if (keys == null || keys.Count == 0)
{
return false;
}
foreach (string key in keys)
{
int keyCode = KeyboardManagerInterop.GetKeyCodeFromName(key);
var keyType = (KeyType)KeyboardManagerInterop.GetKeyType(keyCode);
// If any key is an action key, return false
if (keyType == KeyType.Action)
{
return false;
}
}
// All keys are modifier keys
return true;
}
public static bool IsKeyOrphaned(int originalKey, KeyboardMappingService mappingService)
{
// Check all single key mappings
foreach (var mapping in mappingService.GetSingleKeyMappings())
{
if (!mapping.IsShortcut && int.TryParse(mapping.TargetKey, out int targetKey) && targetKey == originalKey)
{
return false;
}
}
// Check all shortcut mappings
foreach (var mapping in mappingService.GetShortcutMappings())
{
string[] targetKeys = mapping.TargetKeys.Split(';');
if (targetKeys.Length == 1 && int.TryParse(targetKeys[0], out int shortcutTargetKey) && shortcutTargetKey == originalKey)
{
return false;
}
}
// No mapping found for the original key
return true;
}
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Interop
{
public class KeyMapping
{
public int OriginalKey { get; set; }
public string TargetKey { get; set; } = string.Empty;
public bool IsShortcut { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Interop
{
public class KeyToTextMapping
{
public int OriginalKey { get; set; }
public string TargetText { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Interop
{
public enum KeyType
{
Win = 0,
Ctrl = 1,
Alt = 2,
Shift = 3,
Action = 4,
}
}

View File

@@ -0,0 +1,159 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Interop
{
public static class KeyboardManagerInterop
{
private const string DllName = "Powertoys.KeyboardManagerEditorLibraryWrapper.dll";
// Configuration Management
[DllImport(DllName)]
internal static extern IntPtr CreateMappingConfiguration();
[DllImport(DllName)]
internal static extern void DestroyMappingConfiguration(IntPtr config);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool LoadMappingSettings(IntPtr config);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SaveMappingSettings(IntPtr config);
// Get Mapping Functions
[DllImport(DllName)]
internal static extern int GetSingleKeyRemapCount(IntPtr config);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetSingleKeyRemap(IntPtr config, int index, ref SingleKeyMapping mapping);
[DllImport(DllName)]
internal static extern int GetSingleKeyToTextRemapCount(IntPtr config);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetSingleKeyToTextRemap(IntPtr config, int index, ref KeyboardTextMapping mapping);
[DllImport(DllName)]
internal static extern int GetShortcutRemapCount(IntPtr config);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetShortcutRemap(IntPtr config, int index, ref ShortcutMapping mapping);
[DllImport(DllName)]
internal static extern int GetShortcutRemapCountByType(IntPtr config, int operationType);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetShortcutRemapByType(IntPtr config, int operationType, int index, ref ShortcutMapping mapping);
// Add Mapping Functions
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AddSingleKeyRemap(IntPtr config, int originalKey, int targetKey);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AddSingleKeyToTextRemap(IntPtr config, int originalKey, [MarshalAs(UnmanagedType.LPWStr)] string targetText);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AddSingleKeyToShortcutRemap(IntPtr config, int originalKey, [MarshalAs(UnmanagedType.LPWStr)] string targetKeys);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AddShortcutRemap(
IntPtr config,
[MarshalAs(UnmanagedType.LPWStr)] string originalKeys,
[MarshalAs(UnmanagedType.LPWStr)] string targetKeys,
[MarshalAs(UnmanagedType.LPWStr)] string targetApp,
int operationType = 0);
// Delete Mapping Functions
[DllImport(DllName)]
internal static extern bool DeleteSingleKeyRemap(IntPtr mappingConfiguration, int originalKey);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeleteSingleKeyToTextRemap(IntPtr config, int originalKey);
[DllImport(DllName)]
internal static extern bool DeleteShortcutRemap(IntPtr mappingConfiguration, [MarshalAs(UnmanagedType.LPWStr)] string originalKeys, [MarshalAs(UnmanagedType.LPWStr)] string targetApp);
// Key Utility Functions
[DllImport(DllName)]
internal static extern int GetKeyCodeFromName([MarshalAs(UnmanagedType.LPWStr)] string keyName);
[DllImport(DllName, CharSet = CharSet.Unicode)]
internal static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
[DllImport(DllName)]
internal static extern int GetKeyType(int keyCode);
// Validation Functions
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool IsShortcutIllegal([MarshalAs(UnmanagedType.LPWStr)] string shortcutKeys);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AreShortcutsEqual([MarshalAs(UnmanagedType.LPWStr)] string lShort, [MarshalAs(UnmanagedType.LPWStr)] string rShortcut);
// String Management Functions
[DllImport(DllName)]
internal static extern void FreeString(IntPtr str);
public static string GetStringAndFree(IntPtr handle)
{
if (handle == IntPtr.Zero)
{
return string.Empty;
}
string? result = Marshal.PtrToStringUni(handle);
FreeString(handle);
return result ?? string.Empty;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct SingleKeyMapping
{
public int OriginalKey;
public IntPtr TargetKey;
[MarshalAs(UnmanagedType.Bool)]
public bool IsShortcut;
}
[StructLayout(LayoutKind.Sequential)]
public struct KeyboardTextMapping
{
public int OriginalKey;
public IntPtr TargetText;
}
[StructLayout(LayoutKind.Sequential)]
public struct ShortcutMapping
{
public IntPtr OriginalKeys;
public IntPtr TargetKeys;
public IntPtr TargetApp;
public int OperationType;
public IntPtr TargetText;
public IntPtr ProgramPath;
public IntPtr ProgramArgs;
public IntPtr UriToOpen;
}
}

View File

@@ -0,0 +1,243 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using ManagedCommon;
namespace KeyboardManagerEditorUI.Interop
{
public class KeyboardMappingService : IDisposable
{
private IntPtr _configHandle;
private bool _disposed;
public KeyboardMappingService()
{
_configHandle = KeyboardManagerInterop.CreateMappingConfiguration();
if (_configHandle == IntPtr.Zero)
{
Logger.LogError("Failed to create mapping configuration");
throw new InvalidOperationException("Failed to create mapping configuration");
}
KeyboardManagerInterop.LoadMappingSettings(_configHandle);
}
public List<KeyMapping> GetSingleKeyMappings()
{
var result = new List<KeyMapping>();
int count = KeyboardManagerInterop.GetSingleKeyRemapCount(_configHandle);
for (int i = 0; i < count; i++)
{
var mapping = default(SingleKeyMapping);
if (KeyboardManagerInterop.GetSingleKeyRemap(_configHandle, i, ref mapping))
{
result.Add(new KeyMapping
{
OriginalKey = mapping.OriginalKey,
TargetKey = KeyboardManagerInterop.GetStringAndFree(mapping.TargetKey),
IsShortcut = mapping.IsShortcut,
});
}
}
return result;
}
public List<ShortcutKeyMapping> GetShortcutMappings()
{
var result = new List<ShortcutKeyMapping>();
int count = KeyboardManagerInterop.GetShortcutRemapCount(_configHandle);
for (int i = 0; i < count; i++)
{
var mapping = default(ShortcutMapping);
if (KeyboardManagerInterop.GetShortcutRemap(_configHandle, i, ref mapping))
{
result.Add(new ShortcutKeyMapping
{
OriginalKeys = KeyboardManagerInterop.GetStringAndFree(mapping.OriginalKeys),
TargetKeys = KeyboardManagerInterop.GetStringAndFree(mapping.TargetKeys),
TargetApp = KeyboardManagerInterop.GetStringAndFree(mapping.TargetApp),
OperationType = (ShortcutOperationType)mapping.OperationType,
TargetText = KeyboardManagerInterop.GetStringAndFree(mapping.TargetText),
ProgramPath = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramPath),
ProgramArgs = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramArgs),
UriToOpen = KeyboardManagerInterop.GetStringAndFree(mapping.UriToOpen),
});
}
}
return result;
}
public List<ShortcutKeyMapping> GetShortcutMappingsByType(ShortcutOperationType operationType)
{
var result = new List<ShortcutKeyMapping>();
int count = KeyboardManagerInterop.GetShortcutRemapCountByType(_configHandle, (int)operationType);
for (int i = 0; i < count; i++)
{
var mapping = default(ShortcutMapping);
if (KeyboardManagerInterop.GetShortcutRemapByType(_configHandle, (int)operationType, i, ref mapping))
{
result.Add(new ShortcutKeyMapping
{
OriginalKeys = KeyboardManagerInterop.GetStringAndFree(mapping.OriginalKeys),
TargetKeys = KeyboardManagerInterop.GetStringAndFree(mapping.TargetKeys),
TargetApp = KeyboardManagerInterop.GetStringAndFree(mapping.TargetApp),
OperationType = (ShortcutOperationType)mapping.OperationType,
TargetText = KeyboardManagerInterop.GetStringAndFree(mapping.TargetText),
ProgramPath = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramPath),
ProgramArgs = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramArgs),
UriToOpen = KeyboardManagerInterop.GetStringAndFree(mapping.UriToOpen),
});
}
}
return result;
}
public List<KeyToTextMapping> GetKeyToTextMappings()
{
var result = new List<KeyToTextMapping>();
int count = KeyboardManagerInterop.GetSingleKeyToTextRemapCount(_configHandle);
for (int i = 0; i < count; i++)
{
var mapping = default(KeyboardTextMapping);
if (KeyboardManagerInterop.GetSingleKeyToTextRemap(_configHandle, i, ref mapping))
{
result.Add(new KeyToTextMapping
{
OriginalKey = mapping.OriginalKey,
TargetText = KeyboardManagerInterop.GetStringAndFree(mapping.TargetText),
});
}
}
return result;
}
public string GetKeyDisplayName(int keyCode)
{
var keyName = new StringBuilder(64);
KeyboardManagerInterop.GetKeyDisplayName(keyCode, keyName, keyName.Capacity);
return keyName.ToString();
}
public int GetKeyCodeFromName(string keyName)
{
if (string.IsNullOrEmpty(keyName))
{
return 0;
}
return KeyboardManagerInterop.GetKeyCodeFromName(keyName);
}
public bool AddSingleKeyMapping(int originalKey, int targetKey)
{
return KeyboardManagerInterop.AddSingleKeyRemap(_configHandle, originalKey, targetKey);
}
public bool AddSingleKeyMapping(int originalKey, string targetKeys)
{
if (string.IsNullOrEmpty(targetKeys))
{
return false;
}
if (!targetKeys.Contains(';') && int.TryParse(targetKeys, out int targetKey))
{
return KeyboardManagerInterop.AddSingleKeyRemap(_configHandle, originalKey, targetKey);
}
else
{
return KeyboardManagerInterop.AddSingleKeyToShortcutRemap(_configHandle, originalKey, targetKeys);
}
}
public bool AddSingleKeyToTextMapping(int originalKey, string targetText)
{
if (string.IsNullOrEmpty(targetText))
{
return false;
}
return KeyboardManagerInterop.AddSingleKeyToTextRemap(_configHandle, originalKey, targetText);
}
public bool AddShortcutMapping(string originalKeys, string targetKeys, string targetApp = "", ShortcutOperationType operationType = ShortcutOperationType.RemapShortcut)
{
if (string.IsNullOrEmpty(originalKeys) || string.IsNullOrEmpty(targetKeys))
{
return false;
}
return KeyboardManagerInterop.AddShortcutRemap(_configHandle, originalKeys, targetKeys, targetApp, (int)operationType);
}
public bool SaveSettings()
{
return KeyboardManagerInterop.SaveMappingSettings(_configHandle);
}
public bool DeleteSingleKeyMapping(int originalKey)
{
return KeyboardManagerInterop.DeleteSingleKeyRemap(_configHandle, originalKey);
}
public bool DeleteSingleKeyToTextMapping(int originalKey)
{
if (originalKey == 0)
{
return false;
}
return KeyboardManagerInterop.DeleteSingleKeyToTextRemap(_configHandle, originalKey);
}
public bool DeleteShortcutMapping(string originalKeys, string targetApp = "")
{
if (string.IsNullOrEmpty(originalKeys))
{
return false;
}
return KeyboardManagerInterop.DeleteShortcutRemap(_configHandle, originalKeys, targetApp ?? string.Empty);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (_configHandle != IntPtr.Zero)
{
KeyboardManagerInterop.DestroyMappingConfiguration(_configHandle);
_configHandle = IntPtr.Zero;
}
_disposed = true;
}
}
~KeyboardMappingService()
{
Dispose(false);
}
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Interop
{
public class ShortcutKeyMapping
{
public string OriginalKeys { get; set; } = string.Empty;
public string TargetKeys { get; set; } = string.Empty;
public string TargetApp { get; set; } = string.Empty;
public ShortcutOperationType OperationType { get; set; }
public string TargetText { get; set; } = string.Empty;
public string ProgramPath { get; set; } = string.Empty;
public string ProgramArgs { get; set; } = string.Empty;
public string UriToOpen { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Interop
{
public enum ShortcutOperationType
{
RemapShortcut = 0,
RunProgram = 1,
OpenUri = 2,
RemapText = 3,
}
}

View File

@@ -18,6 +18,22 @@
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)</OutputPath>
</PropertyGroup>
<PropertyGroup>
<EnableDefaultXamlItems>true</EnableDefaultXamlItems>
<EnableXamlJitOptimization>true</EnableXamlJitOptimization>
</PropertyGroup>
<ItemGroup>
<None Remove="Pages\ExistingUI.xaml" />
<None Remove="Pages\Programs.xaml" />
<None Remove="Pages\Text.xaml" />
<None Remove="Pages\URLs.xaml" />
<None Remove="Styles\CommonStyle.xaml" />
<None Remove="Styles\InputControl.xaml" />
<None Remove="Styles\KeyVisual.xaml" />
<None Remove="Styles\TextPageInputControl.xaml" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
@@ -33,6 +49,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls.Markdown" />
<!-- This line forces the WebView2 version used by Windows App SDK to be the one we expect from Directory.Packages.props . -->
<PackageReference Include="Microsoft.Web.WebView2" />
</ItemGroup>
@@ -42,7 +59,49 @@
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\" />
<Page Update="Styles\TextPageInputControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\CommonStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Pages\URLs.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Pages\Text.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Pages\Programs.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Pages\Remappings.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\InputControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\KeyVisual.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Pages\ExistingUI.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!--

View File

@@ -6,13 +6,68 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:KeyboardManagerEditorUI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pages="using:KeyboardManagerEditorUI.Pages"
Title="KeyboardManagerEditorUI"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid
x:Name="LayoutRoot"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid x:Name="titleBar">
<StackPanel
Grid.ColumnSpan="2"
Margin="16,8,8,8"
VerticalAlignment="Top"
Orientation="Horizontal">
<Image Width="16" Source="ms-appx:///Assets/FluentIconsKeyboardManager.png" />
<TextBlock
Margin="12,0,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="Keyboard Manager" />
</StackPanel>
</Grid>
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<Button x:Name="myButton" Click="MyButton_Click">Click Me</Button>
</StackPanel>
</Window>
<NavigationView
x:Name="RootView"
Grid.Row="1"
IsBackButtonVisible="Collapsed"
IsBackEnabled="False"
IsPaneToggleButtonVisible="False"
IsSettingsVisible="False"
PaneDisplayMode="Top"
SelectionChanged="RootView_SelectionChanged">
<NavigationView.MenuItems>
<NavigationViewItem Content="Remappings" Tag="Remappings">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xEDA7;" />
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem Content="Text" Tag="Text">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xE8D2;" />
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem Content="Programs" Tag="Programs">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xECAA;" />
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem Content="URLs" Tag="URLs">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xE8A7;" />
</NavigationViewItem.Icon>
</NavigationViewItem>
</NavigationView.MenuItems>
<NavigationView.Content>
<Frame x:Name="NavigationFrame" Margin="0,0,0,0" />
</NavigationView.Content>
</NavigationView>
</Grid>
</Window>

View File

@@ -4,10 +4,12 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using KeyboardManagerEditorUI.Helpers;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
@@ -20,23 +22,52 @@ using Windows.Foundation.Collections;
namespace KeyboardManagerEditorUI
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : Window
{
[DllImport("KeyboardManagerEditorLibraryWrapper.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern bool CheckIfRemappingsAreValid();
public MainWindow()
{
this.InitializeComponent();
this.ExtendsContentIntoTitleBar = true;
this.SetTitleBar(titleBar);
this.Activated += MainWindow_Activated;
this.Closed += MainWindow_Closed;
// Set the default page
RootView.SelectedItem = RootView.MenuItems[0];
}
private void MyButton_Click(object sender, RoutedEventArgs e)
private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
// Call the C++ function to check if the current remappings are valid
myButton.Content = CheckIfRemappingsAreValid() ? "Valid" : "Invalid";
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
// Release the keyboard hook when the window is deactivated
KeyboardHookHelper.Instance.CleanupHook();
}
}
private void MainWindow_Closed(object sender, WindowEventArgs args)
{
KeyboardHookHelper.Instance.Dispose();
this.Activated -= MainWindow_Activated;
this.Closed -= MainWindow_Closed;
}
private void RootView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
// Cleanup the keyboard hook when the selected page changes
KeyboardHookHelper.Instance.CleanupHook();
if (args.SelectedItem is NavigationViewItem selectedItem)
{
switch ((string)selectedItem.Tag)
{
case "Remappings": NavigationFrame.Navigate(typeof(Pages.Remappings)); break;
case "Programs": NavigationFrame.Navigate(typeof(Pages.Programs)); break;
case "Text": NavigationFrame.Navigate(typeof(Pages.Text)); break;
case "URLs": NavigationFrame.Navigate(typeof(Pages.URLs)); break;
}
}
}
}
}

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="KeyboardManagerEditorUI.Pages.ExistingUI"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:KeyboardManagerEditorUI.Pages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<!-- WinUI3 implementation of the existing Keyboard Manager UI -->
<Grid Padding="16">
<StackPanel>
<TextBlock
Margin="10"
FontSize="24"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Text="Remap keys" />
<TextBlock
Margin="10"
FontSize="16"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Select the key you want to change and then configure the key, shortcut or text you want it to send."
TextWrapping="Wrap" />
<TextBlock
Margin="10"
FontSize="16"
FontStyle="Italic"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Example of a remapping: Select A and send &quot;Ctrl+C&quot;, &quot;A&quot; would be your &quot;Select&quot; and &quot;Ctrl+C&quot; would be your &quot;To send&quot; command."
TextWrapping="Wrap" />
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="240" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="240" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Margin="0,12,0,0"
FontSize="16"
FontWeight="Bold"
Text="Selected:" />
<Grid Grid.Row="1" Margin="0,8,0,0">
<StackPanel>
<ToggleButton Click="Button_Click" Content="Select" />
<ComboBox
x:Name="keyComboBox"
Width="250"
PlaceholderText="select keys please" />
</StackPanel>
</Grid>
<TextBlock
Grid.Row="1"
Grid.Column="1"
Margin="24,0,24,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Text="&#xE0AB;" />
<TextBlock
Grid.Column="2"
Margin="0,12,0,0"
FontSize="16"
FontWeight="Bold"
Text="To Send:" />
<Grid
Grid.Row="1"
Grid.Column="2"
Margin="0,8,0,0">
<StackPanel>
<ToggleButton Click="Button_Click" Content="Click to select" />
<ComboBox
x:Name="newKeyComboBox"
Width="250"
PlaceholderText="select keys please" />
</StackPanel>
</Grid>
</Grid>
<Button
Margin="10"
Content="+ Add Key Remapping"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,91 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;
// WinUI3 implementation of the Existing Keyboard Manager UI
namespace KeyboardManagerEditorUI.Pages
{
public sealed partial class ExistingUI : UserControl
{
public class KeyboardKey
{
public int KeyCode { get; set; }
public string KeyName { get; set; } = string.Empty;
public override string ToString() => KeyName;
}
// Struct to hold key code and name pairs
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct KeyNamePair
{
public int KeyCode;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string KeyName;
}
[DllImport("PowerToys.KeyboardManagerEditorLibraryWrapper.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int GetKeyboardKeysList(bool isShortcut, [Out] KeyNamePair[] keyList, int maxCount);
public List<KeyboardKey> KeysList { get; private set; } = new List<KeyboardKey>();
public ExistingUI()
{
this.InitializeComponent();
LoadKeyboardKeys();
keyComboBox.ItemsSource = KeysList;
keyComboBox.DisplayMemberPath = "KeyName";
newKeyComboBox.ItemsSource = KeysList;
newKeyComboBox.DisplayMemberPath = "KeyCode";
}
private void LoadKeyboardKeys()
{
const int MaxKeys = 300;
KeyNamePair[] keyNamePairs = new KeyNamePair[MaxKeys];
int count = GetKeyboardKeysList(false, keyNamePairs, MaxKeys);
KeysList = new List<KeyboardKey>(count);
for (int i = 0; i < count; i++)
{
KeysList.Add(new KeyboardKey
{
KeyCode = keyNamePairs[i].KeyCode,
KeyName = keyNamePairs[i].KeyName,
});
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var button = sender as Button;
if (button == null)
{
// button.Background = (SolidColorBrush)Application.Current.Resources["SystemControlBackgroundAccentBrush"];
return;
}
}
}
}

View File

@@ -0,0 +1,221 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="KeyboardManagerEditorUI.Pages.Programs"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helper="using:KeyboardManagerEditorUI.Helpers"
xmlns:local="using:KeyboardManagerEditorUI.Pages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:styles="using:KeyboardManagerEditorUI.Styles"
mc:Ignorable="d">
<Grid Padding="16">
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Program shortcuts allow you to open specific applications when you use the configured shortcut"
TextWrapping="Wrap" />
<Button
x:Name="NewShortcutBtn"
Height="36"
Margin="0,12,0,0"
Click="NewShortcutBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE109;" />
<TextBlock VerticalAlignment="Center" Text="New" />
</StackPanel>
</Button>
<Grid
HorizontalAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{StaticResource OverlayCornerRadius}">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="236" />
<ColumnDefinition Width="236" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="16,-2,0,0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Shortcut" />
<AppBarSeparator
Grid.Column="1"
Margin="-6,4,0,4"
HorizontalAlignment="Left" />
<TextBlock
Grid.Column="1"
Margin="12,-2,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Program" />
<Rectangle
Grid.ColumnSpan="4"
Height="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
<ListView
Grid.Row="1"
Grid.ColumnSpan="4"
IsItemClickEnabled="True"
ItemClick="ListView_ItemClick"
ItemsSource="{x:Bind Shortcuts}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helper:URLShortcut">
<Grid Height="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="232" />
<ColumnDefinition Width="238" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
<Rectangle
Grid.ColumnSpan="5"
Height="1"
Margin="-16,0,-16,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}"
Opacity="0.8" />
<ItemsControl
Grid.Column="0"
VerticalAlignment="Center"
ItemsSource="{x:Bind Shortcut}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<styles:KeyVisual
HorizontalAlignment="Left"
Content="{Binding}"
VisualType="SmallOutline" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel
Grid.Column="1"
Orientation="Horizontal"
Spacing="8">
<TextBlock VerticalAlignment="Center" Text="WindowsTerminal.exe" />
<FontIcon
VerticalAlignment="Center"
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE946;">
<ToolTipService.ToolTip>
<TextBlock>
<Run FontWeight="SemiBold" Text="Arguments:" />
<Run Text="-config json" />
<LineBreak />
<Run FontWeight="SemiBold" Text="Starting directory:" />
<Run Text="C:\Users\Dev\PowerToys\bin" />
<LineBreak />
<Run FontWeight="SemiBold" Text="Launch state:" />
<Run Text="Minimized" />
</TextBlock>
</ToolTipService.ToolTip>
</FontIcon>
</StackPanel>
<ToggleSwitch
Grid.ColumnSpan="4"
Margin="0,0,-112,0"
HorizontalAlignment="Right"
IsOn="True"
OffContent=""
OnContent="" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</StackPanel>
<ContentDialog
x:Name="KeyDialog"
Title="New program shortcut"
Width="480"
MinWidth="600"
MaxWidth="600"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}"
PrimaryButtonText="Save"
SecondaryButtonText="Cancel">
<StackPanel Width="480" Orientation="Vertical">
<TextBlock Margin="0,12,0,8" Text="Shortcut" />
<ToggleButton
x:Name="OriginalToggleBtn"
Padding="0,24,0,24"
HorizontalAlignment="Stretch"
Style="{StaticResource CustomShortcutToggleButtonStyle}">
<ToggleButton.Content>
<StackPanel Orientation="Horizontal" Spacing="8">
<styles:KeyVisual Content="Win (Left)" VisualType="SmallOutline" />
<styles:KeyVisual Content="T" VisualType="SmallOutline" />
</StackPanel>
</ToggleButton.Content>
</ToggleButton>
<Grid
Margin="0,24,0,0"
ColumnSpacing="4"
RowSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBox Header="Application" Text="WindowsTerminal.exe" />
<Button
Grid.Column="1"
Height="32"
VerticalAlignment="Bottom">
<FontIcon FontSize="14" Glyph="&#xE8DA;" />
</Button>
<TextBox
Grid.Row="1"
Header="Arguments"
Text="-config" />
<TextBox
Grid.Row="2"
Header="Start in directory"
Text="C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.21.10351.0_x64__8wekyb3d8bbwe" />
<Button
Grid.Row="2"
Grid.Column="1"
Height="32"
VerticalAlignment="Bottom">
<FontIcon FontSize="14" Glyph="&#xE8DA;" />
</Button>
<ComboBox
Grid.Row="3"
Header="Launch state"
SelectedIndex="0">
<ComboBoxItem Content="Show window" />
<ComboBoxItem Content="Minimized" />
</ComboBox>
</Grid>
</StackPanel>
</ContentDialog>
</Grid>
</Page>

View File

@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using KeyboardManagerEditorUI.Helpers;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;
namespace KeyboardManagerEditorUI.Pages
{
public sealed partial class Programs : Page
{
public ObservableCollection<URLShortcut> Shortcuts { get; set; }
public Programs()
{
this.InitializeComponent();
Shortcuts = new ObservableCollection<URLShortcut>();
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Win (Left)", "T", }, URL = "https://www.bing.com" });
}
private async void NewShortcutBtn_Click(object sender, RoutedEventArgs e)
{
await KeyDialog.ShowAsync();
}
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
{
await KeyDialog.ShowAsync();
}
}
}

View File

@@ -0,0 +1,221 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="KeyboardManagerEditorUI.Pages.Remappings"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helper="using:KeyboardManagerEditorUI.Helpers"
xmlns:local="using:KeyboardManagerEditorUI.Pages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:styles="using:KeyboardManagerEditorUI.Styles"
mc:Ignorable="d">
<Grid Padding="16">
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Remappings allow you to reconfigure a single key or shortcut to a new key or shortcut"
TextWrapping="Wrap" />
<Button
x:Name="NewRemappingBtn"
Height="36"
Margin="0,12,0,0"
Click="NewRemappingBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE109;" />
<TextBlock VerticalAlignment="Center" Text="New" />
</StackPanel>
</Button>
<Grid
HorizontalAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{StaticResource OverlayCornerRadius}">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="236" />
<ColumnDefinition Width="236" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="16,-2,0,0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Original key(s)" />
<AppBarSeparator
Grid.Column="1"
Margin="-6,4,0,4"
HorizontalAlignment="Left" />
<TextBlock
Grid.Column="1"
Margin="12,-2,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="New key(s)" />
<AppBarSeparator
Grid.Column="2"
Margin="-6,4,0,4"
HorizontalAlignment="Left" />
<TextBlock
Grid.Column="2"
Margin="12,-2,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Applicable apps" />
<Rectangle
Grid.ColumnSpan="4"
Height="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
<ListView
Grid.Row="1"
Grid.ColumnSpan="4"
IsItemClickEnabled="True"
ItemClick="ListView_ItemClick"
ItemsSource="{x:Bind RemappingList}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helper:Remapping">
<Grid Height="Auto" MinHeight="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="236" />
<ColumnDefinition Width="Auto" MinWidth="236" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
<Rectangle
Grid.ColumnSpan="5"
Height="1"
Margin="-16,0,-16,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}"
Opacity="0.8" />
<ItemsControl
Grid.Column="0"
VerticalAlignment="Center"
IsEnabled="{x:Bind IsEnabled, Mode=OneWay}"
ItemsSource="{x:Bind OriginalKeys}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel
MaxWidth="230"
HorizontalSpacing="4"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<styles:KeyVisual
HorizontalAlignment="Left"
Content="{Binding}"
VisualType="SmallOutline" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl
Grid.Column="1"
Margin="0,6,0,6"
VerticalAlignment="Center"
IsEnabled="{x:Bind IsEnabled, Mode=OneWay}"
ItemsSource="{x:Bind RemappedKeys}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel
MaxWidth="230"
HorizontalSpacing="4"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<styles:KeyVisual
HorizontalAlignment="Left"
Content="{Binding}"
VisualType="Small" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock
Grid.Column="2"
Margin="-4,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="12"
Text="{x:Bind AppName}" />
<Button
Grid.Column="3"
Margin="0,0,4,0"
Padding="8,4"
HorizontalAlignment="Right"
VerticalAlignment="Center"
AutomationProperties.Name="Delete remapping"
Background="Transparent"
BorderThickness="0"
Click="DeleteButton_Click"
ToolTipService.ToolTip="Delete remapping">
<FontIcon
FontFamily="{StaticResource SymbolThemeFontFamily}"
FontSize="16"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE74D;" />
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</StackPanel>
<ContentDialog
x:Name="KeyDialog"
Title="Remappings"
MinWidth="600"
MaxWidth="600"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}"
PrimaryButtonText="Save"
SecondaryButtonText="Cancel">
<Grid>
<styles:InputControl x:Name="RemappingControl" />
</Grid>
</ContentDialog>
<TeachingTip
x:Name="OrphanedKeysTeachingTip"
Title="Orphaned Keys Warning"
ActionButtonClick="OrphanedKeysTeachingTip_ActionButtonClick"
ActionButtonContent="Continue anyway"
ActionButtonStyle="{StaticResource AccentButtonStyle}"
CloseButtonClick="OrphanedKeysTeachingTip_CloseButtonClick"
CloseButtonContent="Cancel"
IsLightDismissEnabled="False"
PreferredPlacement="Center">
<TeachingTip.IconSource>
<SymbolIconSource Symbol="Important" />
</TeachingTip.IconSource>
</TeachingTip>
<TeachingTip
x:Name="ValidationTeachingTip"
CloseButtonClick="ValidationTeachingTip_CloseButtonClick"
CloseButtonContent="OK"
IsLightDismissEnabled="True"
PreferredPlacement="Center">
<TeachingTip.IconSource>
<SymbolIconSource Symbol="Important" />
</TeachingTip.IconSource>
</TeachingTip>
</Grid>
</Page>

View File

@@ -0,0 +1,370 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Helpers;
using KeyboardManagerEditorUI.Interop;
using ManagedCommon;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using static KeyboardManagerEditorUI.Helpers.ValidationHelper;
namespace KeyboardManagerEditorUI.Pages
{
/// <summary>
/// The Remapping page that allow users to configure a single key or shortcut to a new key or shortcut
/// </summary>
public sealed partial class Remappings : Page, IDisposable
{
private KeyboardMappingService? _mappingService;
// Flag to indicate if the user is editing an existing remapping
private bool _isEditMode;
private Remapping? _editingRemapping;
private bool _disposed;
// The list of single key mappings
public ObservableCollection<KeyMapping> SingleKeyMappings { get; } = new ObservableCollection<KeyMapping>();
// The list of shortcut key mappings
public ObservableCollection<ShortcutKeyMapping> ShortcutKeyMappings { get; } = new ObservableCollection<ShortcutKeyMapping>();
// The full list of remappings
public ObservableCollection<Remapping> RemappingList { get; set; }
public Remappings()
{
this.InitializeComponent();
RemappingList = new ObservableCollection<Remapping>();
_mappingService = new KeyboardMappingService();
// Load all existing remappings
LoadMappings();
this.Unloaded += Remappings_Unloaded;
}
private void Remappings_Unloaded(object sender, RoutedEventArgs e)
{
// Make sure we unregister the handler when the page is unloaded
UnregisterWindowActivationHandler();
KeyboardHookHelper.Instance.CleanupHook();
}
private void RegisterWindowActivationHandler()
{
// Get the current window that contains this page
var app = Application.Current as App;
if (app?.GetWindow() is Window window)
{
// Register for window activation events
window.Activated += Dialog_WindowActivated;
}
}
private void UnregisterWindowActivationHandler()
{
var app = Application.Current as App;
if (app?.GetWindow() is Window window)
{
// Unregister to prevent memory leaks
window.Activated -= Dialog_WindowActivated;
}
}
private void Dialog_WindowActivated(object sender, WindowActivatedEventArgs args)
{
// When window is deactivated (user switched to another app)
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
// Make sure to cleanup the keyboard hook when the window loses focus
KeyboardHookHelper.Instance.CleanupHook();
RemappingControl.ResetToggleButtons();
RemappingControl.UpdateAllAppsCheckBoxState();
}
}
private async void NewRemappingBtn_Click(object sender, RoutedEventArgs e)
{
_isEditMode = false;
_editingRemapping = null;
RemappingControl.SetOriginalKeys(new List<string>());
RemappingControl.SetRemappedKeys(new List<string>());
RemappingControl.SetApp(false, string.Empty);
RemappingControl.SetUpToggleButtonInitialStatus();
RegisterWindowActivationHandler();
// Show the dialog to add a new remapping
KeyDialog.PrimaryButtonClick += KeyDialog_PrimaryButtonClick;
await KeyDialog.ShowAsync();
KeyDialog.PrimaryButtonClick -= KeyDialog_PrimaryButtonClick;
UnregisterWindowActivationHandler();
KeyboardHookHelper.Instance.CleanupHook();
}
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is Remapping selectedRemapping && selectedRemapping.IsEnabled)
{
// Set to edit mode
_isEditMode = true;
_editingRemapping = selectedRemapping;
RemappingControl.SetOriginalKeys(selectedRemapping.OriginalKeys);
RemappingControl.SetRemappedKeys(selectedRemapping.RemappedKeys);
RemappingControl.SetApp(!selectedRemapping.IsAllApps, selectedRemapping.AppName);
RemappingControl.SetUpToggleButtonInitialStatus();
RegisterWindowActivationHandler();
KeyDialog.PrimaryButtonClick += KeyDialog_PrimaryButtonClick;
await KeyDialog.ShowAsync();
KeyDialog.PrimaryButtonClick -= KeyDialog_PrimaryButtonClick;
UnregisterWindowActivationHandler();
KeyboardHookHelper.Instance.CleanupHook();
// Reset the edit status
_isEditMode = false;
_editingRemapping = null;
}
}
private void KeyDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
List<string> originalKeys = RemappingControl.GetOriginalKeys();
List<string> remappedKeys = RemappingControl.GetRemappedKeys();
bool isAppSpecific = RemappingControl.GetIsAppSpecific();
string appName = RemappingControl.GetAppName();
// Make sure _mappingService is not null before validating and saving
if (_mappingService == null)
{
Logger.LogError("Mapping service is null, cannot validate mapping");
return;
}
// Validate the remapping
ValidationErrorType errorType = ValidationHelper.ValidateKeyMapping(
originalKeys, remappedKeys, isAppSpecific, appName, _mappingService, _isEditMode, _editingRemapping);
if (errorType != ValidationErrorType.NoError)
{
ShowValidationError(errorType, args);
return;
}
// Check for orphaned keys
if (originalKeys.Count == 1 && _mappingService != null)
{
int originalKeyCode = _mappingService.GetKeyCodeFromName(originalKeys[0]);
if (IsKeyOrphaned(originalKeyCode, _mappingService))
{
string keyName = _mappingService.GetKeyDisplayName(originalKeyCode);
OrphanedKeysTeachingTip.Target = RemappingControl;
OrphanedKeysTeachingTip.Subtitle = $"The key {keyName} will become orphaned (inaccessible) after remapping. Please confirm if you want to proceed.";
OrphanedKeysTeachingTip.Tag = args;
OrphanedKeysTeachingTip.IsOpen = true;
args.Cancel = true;
return;
}
}
// If in edit mode, delete the existing remapping before saving the new one
if (_isEditMode && _editingRemapping != null)
{
if (!RemappingHelper.DeleteRemapping(_mappingService!, _editingRemapping))
{
return;
}
}
// If no errors, proceed to save the remapping
bool saved = RemappingHelper.SaveMapping(_mappingService!, originalKeys, remappedKeys, isAppSpecific, appName);
if (saved)
{
// Display the remapping in the list after saving
LoadMappings();
}
}
private void DeleteButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button && button.DataContext is Remapping remapping)
{
if (RemappingHelper.DeleteRemapping(_mappingService!, remapping))
{
LoadMappings();
}
}
}
private void ValidationTeachingTip_CloseButtonClick(TeachingTip sender, object args)
{
sender.IsOpen = false;
}
private void OrphanedKeysTeachingTip_ActionButtonClick(TeachingTip sender, object args)
{
// User pressed continue anyway button
sender.IsOpen = false;
if (_isEditMode && _editingRemapping != null)
{
if (!RemappingHelper.DeleteRemapping(_mappingService!, _editingRemapping))
{
return;
}
}
bool saved = RemappingHelper.SaveMapping(
_mappingService!, RemappingControl.GetOriginalKeys(), RemappingControl.GetRemappedKeys(), RemappingControl.GetIsAppSpecific(), RemappingControl.GetAppName());
if (saved)
{
KeyDialog.Hide();
LoadMappings();
}
}
private void OrphanedKeysTeachingTip_CloseButtonClick(TeachingTip sender, object args)
{
// Just close the teaching tip if the user canceled
sender.IsOpen = false;
}
private void LoadMappings()
{
if (_mappingService == null)
{
return;
}
SingleKeyMappings.Clear();
ShortcutKeyMappings.Clear();
RemappingList.Clear();
// Load all single key mappings
foreach (var mapping in _mappingService.GetSingleKeyMappings())
{
SingleKeyMappings.Add(mapping);
string[] targetKeyCodes = mapping.TargetKey.Split(';');
var targetKeyNames = new List<string>();
foreach (var keyCode in targetKeyCodes)
{
if (int.TryParse(keyCode, out int code))
{
targetKeyNames.Add(_mappingService.GetKeyDisplayName(code));
}
}
RemappingList.Add(new Remapping
{
OriginalKeys = new List<string> { _mappingService.GetKeyDisplayName(mapping.OriginalKey) },
RemappedKeys = targetKeyNames,
IsAllApps = true,
});
}
// Load all shortcut key mappings
foreach (var mapping in _mappingService.GetShortcutMappingsByType(ShortcutOperationType.RemapShortcut))
{
ShortcutKeyMappings.Add(mapping);
string[] originalKeyCodes = mapping.OriginalKeys.Split(';');
string[] targetKeyCodes = mapping.TargetKeys.Split(';');
var originalKeyNames = new List<string>();
var targetKeyNames = new List<string>();
foreach (var keyCode in originalKeyCodes)
{
if (int.TryParse(keyCode, out int code))
{
originalKeyNames.Add(_mappingService.GetKeyDisplayName(code));
}
}
foreach (var keyCode in targetKeyCodes)
{
if (int.TryParse(keyCode, out int code))
{
targetKeyNames.Add(_mappingService.GetKeyDisplayName(code));
}
}
RemappingList.Add(new Remapping
{
OriginalKeys = originalKeyNames,
RemappedKeys = targetKeyNames,
IsAllApps = string.IsNullOrEmpty(mapping.TargetApp),
AppName = string.IsNullOrEmpty(mapping.TargetApp) ? "All Apps" : mapping.TargetApp,
});
}
}
private bool ShowValidationError(ValidationErrorType errorType, ContentDialogButtonClickEventArgs args)
{
var (title, message) = ValidationMessages[errorType];
ValidationTeachingTip.Title = title;
ValidationTeachingTip.Subtitle = message;
ValidationTeachingTip.Target = RemappingControl;
ValidationTeachingTip.Tag = args;
ValidationTeachingTip.IsOpen = true;
args.Cancel = true;
return false;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
_mappingService?.Dispose();
_mappingService = null;
}
_disposed = true;
}
}
}
}

View File

@@ -0,0 +1,207 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="KeyboardManagerEditorUI.Pages.Text"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helper="using:KeyboardManagerEditorUI.Helpers"
xmlns:local="using:KeyboardManagerEditorUI.Pages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:styles="using:KeyboardManagerEditorUI.Styles"
mc:Ignorable="d">
<Grid Padding="16">
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Text shortcuts allow you to insert text in any input field when you use the configured keyboard shortcut"
TextWrapping="Wrap" />
<Button
x:Name="NewShortcutBtn"
Height="36"
Margin="0,12,0,0"
Click="NewShortcutBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE109;" />
<TextBlock VerticalAlignment="Center" Text="New" />
</StackPanel>
</Button>
<Grid
HorizontalAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{StaticResource OverlayCornerRadius}">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="236" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="120" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="16,-2,0,0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Original key(s)" />
<AppBarSeparator
Grid.Column="1"
Margin="-6,4,0,4"
HorizontalAlignment="Left" />
<TextBlock
Grid.Column="1"
Margin="12,-2,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Text" />
<AppBarSeparator
Grid.Column="2"
Margin="-6,4,0,4"
HorizontalAlignment="Left" />
<TextBlock
Grid.Column="2"
Margin="12,-2,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Applicable apps" />
<Rectangle
Grid.Row="0"
Grid.ColumnSpan="4"
Height="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
<ListView
x:Name="MappingsList"
Grid.Row="1"
Grid.ColumnSpan="4"
IsItemClickEnabled="True"
ItemClick="ListView_ItemClick"
ItemsSource="{x:Bind TextMappings}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helper:TextMapping">
<Grid Height="Auto" MinHeight="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="236" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="120" />
<ColumnDefinition Width="48" />
</Grid.ColumnDefinitions>
<Rectangle
Grid.ColumnSpan="5"
Height="1"
Margin="-16,0,-16,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}"
Opacity="0.8" />
<!-- Shortcut keys -->
<ItemsControl
Grid.Column="0"
VerticalAlignment="Center"
ItemsSource="{x:Bind Keys}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel
MaxWidth="230"
HorizontalSpacing="4"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<styles:KeyVisual
HorizontalAlignment="Left"
Content="{Binding}"
VisualType="SmallOutline" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Text content -->
<TextBlock
Grid.Column="1"
Margin="-4,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="12"
Text="{x:Bind Text}"
TextTrimming="CharacterEllipsis" />
<!-- App name -->
<TextBlock
Grid.Column="2"
Margin="-4,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind AppName}"
TextTrimming="CharacterEllipsis" />
<!-- Delete button -->
<Button
Grid.Column="3"
Margin="0,0,4,0"
Padding="8,4"
HorizontalAlignment="Right"
VerticalAlignment="Center"
AutomationProperties.Name="Delete remapping"
Background="Transparent"
BorderThickness="0"
Click="DeleteButton_Click"
ToolTipService.ToolTip="Delete">
<FontIcon
FontFamily="{StaticResource SymbolThemeFontFamily}"
FontSize="16"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE74D;" />
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</StackPanel>
<ContentDialog
x:Name="KeyDialog"
Title="Text Shortcut"
Width="480"
Height="360"
MinWidth="600"
MaxWidth="600"
PrimaryButtonClick="KeyDialog_PrimaryButtonClick"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}"
PrimaryButtonText="Save"
SecondaryButtonText="Cancel">
<Grid>
<styles:TextPageInputControl x:Name="TextInputControl" />
</Grid>
</ContentDialog>
<TeachingTip
x:Name="ValidationTip"
CloseButtonContent="OK"
IsLightDismissEnabled="True"
PreferredPlacement="Center">
<TeachingTip.IconSource>
<SymbolIconSource Symbol="Important" />
</TeachingTip.IconSource>
</TeachingTip>
</Grid>
</Page>

View File

@@ -0,0 +1,278 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Helpers;
using KeyboardManagerEditorUI.Interop;
using ManagedCommon;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
namespace KeyboardManagerEditorUI.Pages
{
public sealed partial class Text : Page, IDisposable
{
private KeyboardMappingService? _mappingService;
// Flag to indicate if the user is editing an existing mapping
private bool _isEditMode;
private TextMapping? _editingMapping;
private bool _disposed;
// The list of text mappings
public ObservableCollection<TextMapping> TextMappings { get; } = new ObservableCollection<TextMapping>();
public Text()
{
this.InitializeComponent();
try
{
_mappingService = new KeyboardMappingService();
LoadTextMappings();
}
catch (Exception ex)
{
Logger.LogError("Failed to initialize KeyboardMappingService: " + ex.Message);
}
this.Unloaded += Text_Unloaded;
}
private void Text_Unloaded(object sender, RoutedEventArgs e)
{
Dispose();
}
private void LoadTextMappings()
{
if (_mappingService == null)
{
return;
}
TextMappings.Clear();
// Load key-to-text mappings
var keyToTextMappings = _mappingService.GetKeyToTextMappings();
foreach (var mapping in keyToTextMappings)
{
TextMappings.Add(new TextMapping
{
Keys = new List<string> { _mappingService.GetKeyDisplayName(mapping.OriginalKey) },
Text = mapping.TargetText,
IsAllApps = true,
AppName = "All Apps",
});
}
// Load shortcut-to-text mappings
foreach (var mapping in _mappingService.GetShortcutMappingsByType(ShortcutOperationType.RemapText))
{
string[] originalKeyCodes = mapping.OriginalKeys.Split(';');
var originalKeyNames = new List<string>();
foreach (var keyCode in originalKeyCodes)
{
if (int.TryParse(keyCode, out int code))
{
originalKeyNames.Add(_mappingService.GetKeyDisplayName(code));
}
}
TextMappings.Add(new TextMapping
{
Keys = originalKeyNames,
Text = mapping.TargetText,
IsAllApps = string.IsNullOrEmpty(mapping.TargetApp),
AppName = string.IsNullOrEmpty(mapping.TargetApp) ? "All Apps" : mapping.TargetApp,
});
}
}
private async void NewShortcutBtn_Click(object sender, RoutedEventArgs e)
{
_isEditMode = false;
_editingMapping = null;
TextInputControl.ClearKeys();
TextInputControl.SetTextContent(string.Empty);
TextInputControl.SetAppSpecific(false, string.Empty);
await KeyDialog.ShowAsync();
}
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is TextMapping selectedMapping)
{
_isEditMode = true;
_editingMapping = selectedMapping;
TextInputControl.SetShortcutKeys(selectedMapping.Keys);
TextInputControl.SetTextContent(selectedMapping.Text);
TextInputControl.SetAppSpecific(!selectedMapping.IsAllApps, selectedMapping.AppName);
await KeyDialog.ShowAsync();
}
}
private void KeyDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
if (_mappingService == null)
{
return;
}
List<string> keys = TextInputControl.GetShortcutKeys();
string textContent = TextInputControl.GetTextContent();
bool isAppSpecific = TextInputControl.GetIsAppSpecific();
string appName = TextInputControl.GetAppName();
// Validate inputs
ValidationErrorType errorType = ValidationHelper.ValidateTextMapping(
keys, textContent, isAppSpecific, appName, _mappingService);
if (errorType != ValidationErrorType.NoError)
{
ShowValidationError(errorType, args);
return;
}
bool saved = false;
try
{
// Delete existing mapping if in edit mode
if (_isEditMode && _editingMapping != null)
{
if (_editingMapping.Keys.Count == 1)
{
int originalKey = _mappingService.GetKeyCodeFromName(_editingMapping.Keys[0]);
if (originalKey != 0)
{
_mappingService.DeleteSingleKeyToTextMapping(originalKey);
}
}
else
{
string originalKeys = string.Join(";", _editingMapping.Keys.Select(k => _mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
_mappingService.DeleteShortcutMapping(originalKeys, _editingMapping.IsAllApps ? string.Empty : _editingMapping.AppName);
}
}
// Add new mapping
if (keys.Count == 1)
{
// Single key to text mapping
int originalKey = _mappingService.GetKeyCodeFromName(keys[0]);
if (originalKey != 0)
{
saved = _mappingService.AddSingleKeyToTextMapping(originalKey, textContent);
}
}
else
{
// Shortcut to text mapping
string originalKeysString = string.Join(";", keys.Select(k => _mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
if (isAppSpecific && !string.IsNullOrEmpty(appName))
{
saved = _mappingService.AddShortcutMapping(originalKeysString, textContent, appName, ShortcutOperationType.RemapText);
}
else
{
saved = _mappingService.AddShortcutMapping(originalKeysString, textContent, operationType: ShortcutOperationType.RemapText);
}
}
if (saved)
{
_mappingService.SaveSettings();
LoadTextMappings(); // Refresh the list
}
}
catch (Exception ex)
{
Logger.LogError("Error saving text mapping: " + ex.Message);
args.Cancel = true;
}
}
private void DeleteButton_Click(object sender, RoutedEventArgs e)
{
if (_mappingService == null || !(sender is Button button) || !(button.DataContext is TextMapping mapping))
{
return;
}
try
{
bool deleted = false;
if (mapping.Keys.Count == 1)
{
// Single key mapping
int originalKey = _mappingService.GetKeyCodeFromName(mapping.Keys[0]);
if (originalKey != 0)
{
deleted = _mappingService.DeleteSingleKeyToTextMapping(originalKey);
}
}
else
{
// Shortcut mapping
string originalKeys = string.Join(";", mapping.Keys.Select(k => _mappingService.GetKeyCodeFromName(k)));
deleted = _mappingService.DeleteShortcutMapping(originalKeys, mapping.IsAllApps ? string.Empty : mapping.AppName);
}
if (deleted)
{
_mappingService.SaveSettings();
TextMappings.Remove(mapping);
}
}
catch (Exception ex)
{
Logger.LogError("Error deleting text mapping: " + ex.Message);
}
}
private void ShowValidationError(ValidationErrorType errorType, ContentDialogButtonClickEventArgs args)
{
if (ValidationHelper.ValidationMessages.TryGetValue(errorType, out (string Title, string Message) error))
{
ValidationTip.Title = error.Title;
ValidationTip.Subtitle = error.Message;
ValidationTip.IsOpen = true;
args.Cancel = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_mappingService?.Dispose();
_mappingService = null;
}
_disposed = true;
}
}
}
}

View File

@@ -0,0 +1,165 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="KeyboardManagerEditorUI.Pages.URLs"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helper="using:KeyboardManagerEditorUI.Helpers"
xmlns:local="using:KeyboardManagerEditorUI.Pages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:styles="using:KeyboardManagerEditorUI.Styles"
mc:Ignorable="d">
<Grid Padding="16">
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="URL shortcuts allow you to open a URL when you use the configured shortcut"
TextWrapping="Wrap" />
<Button
x:Name="NewShortcutBtn"
Height="36"
Margin="0,12,0,0"
Click="NewShortcutBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE109;" />
<TextBlock VerticalAlignment="Center" Text="New" />
</StackPanel>
</Button>
<Grid
HorizontalAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{ThemeResource OverlayCornerRadius}">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="236" />
<ColumnDefinition Width="236" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="16,-2,0,0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Shortcut" />
<AppBarSeparator
Grid.Column="1"
Margin="-6,4,0,4"
HorizontalAlignment="Left" />
<TextBlock
Grid.Column="1"
Margin="12,-2,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="URL" />
<Rectangle
Grid.ColumnSpan="4"
Height="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
<ListView
Grid.Row="1"
Grid.ColumnSpan="4"
IsItemClickEnabled="True"
ItemClick="ListView_ItemClick"
ItemsSource="{x:Bind Shortcuts}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helper:URLShortcut">
<Grid Height="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="232" />
<ColumnDefinition Width="238" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
<Rectangle
Grid.ColumnSpan="5"
Height="1"
Margin="-16,0,-16,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}"
Opacity="0.8" />
<ItemsControl
Grid.Column="0"
VerticalAlignment="Center"
ItemsSource="{x:Bind Shortcut}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<styles:KeyVisual
HorizontalAlignment="Left"
Content="{Binding}"
VisualType="SmallOutline" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<HyperlinkButton
Grid.Column="1"
Margin="-12,0,0,0"
Content="{x:Bind URL}" />
<ToggleSwitch
Grid.ColumnSpan="4"
Margin="0,0,-112,0"
HorizontalAlignment="Right"
IsOn="True"
OffContent=""
OnContent="" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</StackPanel>
<ContentDialog
x:Name="KeyDialog"
Title="New URL shortcut"
Width="480"
Height="360"
MinWidth="600"
MaxWidth="600"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}"
PrimaryButtonText="Save"
SecondaryButtonText="Cancel">
<StackPanel
Width="320"
Height="264"
Orientation="Vertical">
<TextBlock Margin="0,12,0,8" Text="Shortcut" />
<ToggleButton
x:Name="OriginalToggleBtn"
Padding="0,24,0,24"
HorizontalAlignment="Stretch"
Style="{StaticResource CustomShortcutToggleButtonStyle}">
<ToggleButton.Content>
<StackPanel Orientation="Horizontal" Spacing="8">
<styles:KeyVisual Content="Win (Left)" VisualType="SmallOutline" />
<styles:KeyVisual Content="B" VisualType="SmallOutline" />
</StackPanel>
</ToggleButton.Content>
</ToggleButton>
<TextBox
Margin="0,24,0,0"
Header="URL to open"
Text="https://www.bing.com" />
</StackPanel>
</ContentDialog>
</Grid>
</Page>

View File

@@ -0,0 +1,117 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
using KeyboardManagerEditorUI.Helpers;
using KeyboardManagerEditorUI.Interop;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;
namespace KeyboardManagerEditorUI.Pages
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class URLs : Page, IDisposable
{
private KeyboardMappingService? _mappingService;
private bool _disposed;
public ObservableCollection<URLShortcut> Shortcuts { get; set; }
[DllImport("PowerToys.KeyboardManagerEditorLibraryWrapper.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
public URLs()
{
this.InitializeComponent();
Shortcuts = new ObservableCollection<URLShortcut>();
_mappingService = new KeyboardMappingService();
foreach (var mapping in _mappingService.GetShortcutMappingsByType(ShortcutOperationType.OpenUri))
{
string[] originalKeyCodes = mapping.OriginalKeys.Split(';');
var originalKeyNames = new List<string>();
foreach (var keyCode in originalKeyCodes)
{
if (int.TryParse(keyCode, out int code))
{
originalKeyNames.Add(GetKeyDisplayName(code));
}
}
var shortcut = new URLShortcut
{
Shortcut = originalKeyNames,
URL = mapping.UriToOpen,
};
Shortcuts.Add(shortcut);
}
/*
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Shift", "Win", "M" }, URL = "https://www.microsoft.com" });
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Win", "P", }, URL = "https://www.bing.com" });
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Shift", "Win", "M" }, URL = "https://www.windows.com" });
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Win", "U", }, URL = "https://www.bing.com" });
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Ctrl", "P" }, URL = "https://www.surface.com" });
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Alt", "Ctrl", "Shift" }, URL = "https://www.bing.com" });
*/
}
public static string GetKeyDisplayName(int keyCode)
{
var keyName = new StringBuilder(64);
GetKeyDisplayName(keyCode, keyName, keyName.Capacity);
return keyName.ToString();
}
private async void NewShortcutBtn_Click(object sender, RoutedEventArgs e)
{
await KeyDialog.ShowAsync();
}
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
{
await KeyDialog.ShowAsync();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
_mappingService?.Dispose();
_mappingService = null;
}
_disposed = true;
}
}
}
}

View File

@@ -0,0 +1,203 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:KeyboardManagerEditorUI.Styles">
<x:Double x:Key="ContentDialogMaxWidth">960</x:Double>
<Style x:Key="CustomShortcutToggleButtonStyle" TargetType="ToggleButton">
<Setter Property="Background" Value="{ThemeResource ToggleButtonBackground}" />
<Setter Property="Foreground" Value="{ThemeResource ToggleButtonForeground}" />
<Setter Property="BorderBrush" Value="{ThemeResource ToggleButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="1,1,1,1" />
<Setter Property="Padding" Value="{StaticResource ButtonPadding}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-3" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<ContentPresenter
x:Name="ContentPresenter"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
CornerRadius="{TemplateBinding CornerRadius}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundPressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Checked">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundChecked}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundChecked}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderThickness">
<DiscreteObjectKeyFrame KeyTime="0" Value="1,1,1,4" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="CheckedPointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundCheckedPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundCheckedPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderThickness">
<DiscreteObjectKeyFrame KeyTime="0" Value="1,1,1,4" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="CheckedPressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundCheckedPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundCheckedPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderThickness">
<DiscreteObjectKeyFrame KeyTime="0" Value="1,1,1,4" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="CheckedDisabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundCheckedDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundCheckedDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushCheckedDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Indeterminate">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundIndeterminate}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundIndeterminate}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushIndeterminate}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="IndeterminatePointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundIndeterminatePointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushIndeterminatePointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundIndeterminatePointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="IndeterminatePressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundIndeterminatePressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushIndeterminatePressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundIndeterminatePressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="IndeterminateDisabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundIndeterminateDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundIndeterminateDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushIndeterminateDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</ContentPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="KeyboardManagerEditorUI.Styles.InputControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:KeyboardManagerEditorUI.Styles"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Loaded="UserControl_Loaded"
mc:Ignorable="d">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="240" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="240" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Margin="0,12,0,0" Text="Original key(s)" />
<Grid Grid.Column="2">
<TextBlock Margin="0,12,0,0" Text="New key(s)" />
</Grid>
<Grid Grid.Row="1" Margin="0,8,0,0">
<ToggleButton
x:Name="OriginalToggleBtn"
Padding="0,24,0,24"
HorizontalAlignment="Stretch"
Checked="OriginalToggleBtn_Checked"
Style="{StaticResource CustomShortcutToggleButtonStyle}">
<ToggleButton.Content>
<ItemsControl x:Name="OriginalKeys">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel HorizontalSpacing="4" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:KeyVisual Content="{Binding}" VisualType="SmallOutline" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ToggleButton.Content>
</ToggleButton>
</Grid>
<TextBlock
Grid.Row="1"
Grid.Column="1"
Margin="24,0,24,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Text="&#xE0AB;" />
<Grid
Grid.Row="1"
Grid.Column="2"
Margin="0,8,0,0">
<ToggleButton
x:Name="RemappedToggleBtn"
Padding="0,24,0,24"
HorizontalAlignment="Stretch"
Checked="RemappedToggleBtn_Checked"
Style="{StaticResource CustomShortcutToggleButtonStyle}">
<ToggleButton.Content>
<ItemsControl x:Name="RemappedKeys">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel HorizontalSpacing="4" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:KeyVisual Content="{Binding}" VisualType="Small" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ToggleButton.Content>
</ToggleButton>
</Grid>
</Grid>
<CheckBox
x:Name="AllAppsCheckBox"
Margin="0,24,0,12"
Content="Only apply this remapping to a specific application" />
<TextBox
x:Name="AppNameTextBox"
Header="Application name"
IsEnabled="{Binding ElementName=AllAppsCheckBox, Path=IsChecked}"
PlaceholderText="e.g.: outlook.exe" />
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,419 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using KeyboardManagerEditorUI.Helpers;
using KeyboardManagerEditorUI.Interop;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Windows.System;
namespace KeyboardManagerEditorUI.Styles
{
public sealed partial class InputControl : UserControl, IDisposable, IKeyboardHookTarget
{
// Collection to store original and remapped keys
private ObservableCollection<string> _originalKeys = new ObservableCollection<string>();
private ObservableCollection<string> _remappedKeys = new ObservableCollection<string>();
// TeachingTip for notifications
private TeachingTip? currentNotification;
private DispatcherTimer? notificationTimer;
private bool _disposed;
public static readonly DependencyProperty InputModeProperty =
DependencyProperty.Register(
"InputMode",
typeof(KeyInputMode),
typeof(InputControl),
new PropertyMetadata(KeyInputMode.OriginalKeys));
public KeyInputMode InputMode
{
get { return (KeyInputMode)GetValue(InputModeProperty); }
set { SetValue(InputModeProperty, value); }
}
public InputControl()
{
this.InitializeComponent();
this.OriginalKeys.ItemsSource = _originalKeys;
this.RemappedKeys.ItemsSource = _remappedKeys;
this.Unloaded += InputControl_Unloaded;
// Set the default focus state
OriginalToggleBtn.IsChecked = true;
// Ensure AllAppsCheckBox is in the correct state initially
UpdateAllAppsCheckBoxState();
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
AllAppsCheckBox.Checked += AllAppsCheckBox_Checked;
AllAppsCheckBox.Unchecked += AllAppsCheckBox_Unchecked;
AppNameTextBox.GotFocus += AppNameTextBox_GotFocus;
}
private void InputControl_Unloaded(object sender, RoutedEventArgs e)
{
// Reset the control when it is unloaded
Reset();
}
public void OnKeyDown(VirtualKey key, List<string> formattedKeys)
{
if (InputMode == KeyInputMode.RemappedKeys)
{
_remappedKeys.Clear();
foreach (var keyName in formattedKeys)
{
_remappedKeys.Add(keyName);
}
}
else
{
_originalKeys.Clear();
foreach (var keyName in formattedKeys)
{
_originalKeys.Add(keyName);
}
}
UpdateAllAppsCheckBoxState();
}
public void ClearKeys()
{
if (InputMode == KeyInputMode.RemappedKeys)
{
_remappedKeys.Clear();
}
else
{
_originalKeys.Clear();
}
}
public void OnInputLimitReached()
{
ShowNotificationTip("Shortcuts can only have up to 4 modifier keys");
}
public void CleanupKeyboardHook()
{
KeyboardHookHelper.Instance.CleanupHook();
}
private void RemappedToggleBtn_Checked(object sender, RoutedEventArgs e)
{
// Only set NewMode to true if RemappedToggleBtn is checked
if (RemappedToggleBtn.IsChecked == true)
{
InputMode = KeyInputMode.RemappedKeys;
// Make sure OriginalToggleBtn is unchecked
if (OriginalToggleBtn.IsChecked == true)
{
OriginalToggleBtn.IsChecked = false;
}
KeyboardHookHelper.Instance.ActivateHook(this);
}
else
{
CleanupKeyboardHook();
}
}
private void OriginalToggleBtn_Checked(object sender, RoutedEventArgs e)
{
// Only set NewMode to false if OriginalToggleBtn is checked
if (OriginalToggleBtn.IsChecked == true)
{
InputMode = KeyInputMode.OriginalKeys;
// Make sure RemappedToggleBtn is unchecked
if (RemappedToggleBtn.IsChecked == true)
{
RemappedToggleBtn.IsChecked = false;
}
KeyboardHookHelper.Instance.ActivateHook(this);
}
}
private void AllAppsCheckBox_Checked(object sender, RoutedEventArgs e)
{
if (RemappedToggleBtn != null && RemappedToggleBtn.IsChecked == true)
{
RemappedToggleBtn.IsChecked = false;
}
if (OriginalToggleBtn != null && OriginalToggleBtn.IsChecked == true)
{
OriginalToggleBtn.IsChecked = false;
}
CleanupKeyboardHook();
AppNameTextBox.Visibility = Visibility.Visible;
}
private void AllAppsCheckBox_Unchecked(object sender, RoutedEventArgs e)
{
AppNameTextBox.Visibility = Visibility.Collapsed;
}
private void AppNameTextBox_GotFocus(object sender, RoutedEventArgs e)
{
// Reset the focus state when the AppNameTextBox is focused
if (RemappedToggleBtn != null && RemappedToggleBtn.IsChecked == true)
{
RemappedToggleBtn.IsChecked = false;
}
if (OriginalToggleBtn != null && OriginalToggleBtn.IsChecked == true)
{
OriginalToggleBtn.IsChecked = false;
}
CleanupKeyboardHook();
}
public void SetRemappedKeys(List<string> keys)
{
_remappedKeys.Clear();
if (keys != null)
{
foreach (var key in keys)
{
_remappedKeys.Add(key);
}
}
UpdateAllAppsCheckBoxState();
}
public void SetOriginalKeys(List<string> keys)
{
_originalKeys.Clear();
if (keys != null)
{
foreach (var key in keys)
{
_originalKeys.Add(key);
}
}
}
public void SetApp(bool isSpecificApp, string appName)
{
if (isSpecificApp)
{
AllAppsCheckBox.IsChecked = true;
AppNameTextBox.Text = appName;
AppNameTextBox.Visibility = Visibility.Visible;
}
else
{
AllAppsCheckBox.IsChecked = false;
AppNameTextBox.Visibility = Visibility.Collapsed;
}
}
public List<string> GetOriginalKeys()
{
return _originalKeys.ToList();
}
public List<string> GetRemappedKeys()
{
return _remappedKeys.ToList();
}
public bool GetIsAppSpecific()
{
return AllAppsCheckBox.IsChecked ?? false;
}
public string GetAppName()
{
return AppNameTextBox.Text ?? string.Empty;
}
public void SetUpToggleButtonInitialStatus()
{
// Ensure OriginalToggleBtn is checked
if (OriginalToggleBtn != null && OriginalToggleBtn.IsChecked != true)
{
OriginalToggleBtn.IsChecked = true;
}
// Make sure RemappedToggleBtn is not checked
if (RemappedToggleBtn != null && RemappedToggleBtn.IsChecked == true)
{
RemappedToggleBtn.IsChecked = false;
}
}
public void UpdateAllAppsCheckBoxState()
{
// Only enable app-specific remapping for shortcuts (multiple keys)
bool isShortcut = _originalKeys.Count > 1;
AllAppsCheckBox.IsEnabled = isShortcut;
// If it's not a shortcut, ensure the checkbox is unchecked and app textbox is hidden
if (!isShortcut)
{
AllAppsCheckBox.IsChecked = false;
AppNameTextBox.Visibility = Visibility.Collapsed;
}
}
public void ShowNotificationTip(string message)
{
// If there's already an active notification, close and remove it first
CloseExistingNotification();
// Create a new notification
currentNotification = new TeachingTip
{
Title = "Input Limit Reached",
Subtitle = message,
IsLightDismissEnabled = true,
PreferredPlacement = TeachingTipPlacementMode.Top,
XamlRoot = this.XamlRoot,
IconSource = new SymbolIconSource { Symbol = Symbol.Important },
};
// Target the toggle button that triggered the notification
currentNotification.Target = InputMode == KeyInputMode.RemappedKeys ? RemappedToggleBtn : OriginalToggleBtn;
// Add the notification to the root panel and show it
if (this.Content is Panel rootPanel)
{
rootPanel.Children.Add(currentNotification);
currentNotification.IsOpen = true;
// Create a timer to auto-dismiss the notification
notificationTimer = new DispatcherTimer();
notificationTimer.Interval = TimeSpan.FromMilliseconds(EditorConstants.DefaultNotificationTimeout);
notificationTimer.Tick += (s, e) =>
{
CloseExistingNotification();
notificationTimer = null;
};
notificationTimer.Start();
}
}
// Helper method to close existing notifications
private void CloseExistingNotification()
{
// Stop any running timer
if (notificationTimer != null)
{
notificationTimer.Stop();
notificationTimer = null;
}
// Close and remove any existing notification
if (currentNotification != null && currentNotification.IsOpen)
{
currentNotification.IsOpen = false;
if (this.Content is Panel rootPanel)
{
rootPanel.Children.Remove(currentNotification);
}
currentNotification = null;
}
}
public void ResetToggleButtons()
{
// Reset toggle button status without clearing the key displays
if (RemappedToggleBtn != null)
{
RemappedToggleBtn.IsChecked = false;
}
if (OriginalToggleBtn != null)
{
OriginalToggleBtn.IsChecked = false;
}
}
public void Reset()
{
// Reset displayed keys
_originalKeys.Clear();
_remappedKeys.Clear();
// Reset toggle button status
if (RemappedToggleBtn != null)
{
RemappedToggleBtn.IsChecked = false;
}
if (OriginalToggleBtn != null)
{
OriginalToggleBtn.IsChecked = false;
}
InputMode = KeyInputMode.OriginalKeys;
// Reset app name text box
if (AppNameTextBox != null)
{
AppNameTextBox.Text = string.Empty;
}
UpdateAllAppsCheckBoxState();
// Close any existing notifications
CloseExistingNotification();
// Reset the focus status
if (this.FocusState != FocusState.Unfocused)
{
this.IsTabStop = false;
this.IsTabStop = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
CleanupKeyboardHook();
CloseExistingNotification();
Reset();
}
_disposed = true;
}
}
}
}

View File

@@ -0,0 +1,198 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Media;
using Windows.System;
namespace KeyboardManagerEditorUI.Styles
{
[TemplatePart(Name = KeyPresenter, Type = typeof(ContentPresenter))]
[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")]
[TemplateVisualState(Name = "Disabled", GroupName = "CommonStates")]
[TemplateVisualState(Name = "Default", GroupName = "StateStates")]
[TemplateVisualState(Name = "Error", GroupName = "StateStates")]
public sealed partial class KeyVisual : Control
{
private const string KeyPresenter = "KeyPresenter";
private KeyVisual? _keyVisual;
private ContentPresenter? _keyPresenter;
public object Content
{
get => (object)GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(object), typeof(KeyVisual), new PropertyMetadata(default(string), OnContentChanged));
public VisualType VisualType
{
get => (VisualType)GetValue(VisualTypeProperty);
set => SetValue(VisualTypeProperty, value);
}
public static readonly DependencyProperty VisualTypeProperty = DependencyProperty.Register("VisualType", typeof(VisualType), typeof(KeyVisual), new PropertyMetadata(default(VisualType), OnSizeChanged));
public bool IsError
{
get => (bool)GetValue(IsErrorProperty);
set => SetValue(IsErrorProperty, value);
}
public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(KeyVisual), new PropertyMetadata(false, OnIsErrorChanged));
public KeyVisual()
{
this.DefaultStyleKey = typeof(KeyVisual);
this.Style = GetStyleSize("TextKeyVisualStyle");
}
protected override void OnApplyTemplate()
{
IsEnabledChanged -= KeyVisual_IsEnabledChanged;
_keyVisual = (KeyVisual)this;
_keyPresenter = (ContentPresenter)_keyVisual.GetTemplateChild(KeyPresenter);
Update();
SetEnabledState();
SetErrorState();
IsEnabledChanged += KeyVisual_IsEnabledChanged;
base.OnApplyTemplate();
}
private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((KeyVisual)d).Update();
}
private static void OnSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((KeyVisual)d).Update();
}
private static void OnIsErrorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((KeyVisual)d).SetErrorState();
}
private void Update()
{
if (_keyVisual == null || _keyVisual._keyPresenter == null)
{
return;
}
if (_keyVisual.Content != null)
{
if (_keyVisual.Content.GetType() == typeof(string))
{
_keyVisual.Style = GetStyleSize("TextKeyVisualStyle");
_keyVisual._keyPresenter.Content = _keyVisual.Content;
}
else
{
_keyVisual.Style = GetStyleSize("IconKeyVisualStyle");
switch ((int)_keyVisual.Content)
{
/* We can enable other glyphs in the future
case 13: // The Enter key or button.
_keyVisual._keyPresenter.Content = "\uE751"; break;
case 8: // The Back key or button.
_keyVisual._keyPresenter.Content = "\uE750"; break;
case 16: // The right Shift key or button.
case 160: // The left Shift key or button.
case 161: // The Shift key or button.
_keyVisual._keyPresenter.Content = "\uE752"; break; */
case 38: _keyVisual._keyPresenter.Content = "\uE0E4"; break; // The Up Arrow key or button.
case 40: _keyVisual._keyPresenter.Content = "\uE0E5"; break; // The Down Arrow key or button.
case 37: _keyVisual._keyPresenter.Content = "\uE0E2"; break; // The Left Arrow key or button.
case 39: _keyVisual._keyPresenter.Content = "\uE0E3"; break; // The Right Arrow key or button.
case 91: // The left Windows key
case 92: // The right Windows key
PathIcon winIcon = (PathIcon)XamlReader.Load(@"<PathIcon xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" Data=""M9,17V9h8v8ZM0,17V9H8v8ZM9,8V0h8V8ZM0,8V0H8V8Z"" />") as PathIcon;
Viewbox winIconContainer = new Viewbox();
winIconContainer.Child = winIcon;
winIconContainer.HorizontalAlignment = HorizontalAlignment.Center;
winIconContainer.VerticalAlignment = VerticalAlignment.Center;
double iconDimensions = GetIconSize();
winIconContainer.Height = iconDimensions;
winIconContainer.Width = iconDimensions;
_keyVisual._keyPresenter.Content = winIconContainer;
break;
default: _keyVisual._keyPresenter.Content = ((VirtualKey)_keyVisual.Content).ToString(); break;
}
}
}
}
public Style GetStyleSize(string styleName)
{
if (VisualType == VisualType.Small)
{
return (Style)App.Current.Resources["Small" + styleName];
}
else if (VisualType == VisualType.SmallOutline)
{
return (Style)App.Current.Resources["SmallOutline" + styleName];
}
else if (VisualType == VisualType.LargeOutline)
{
return (Style)App.Current.Resources["LargeOutline" + styleName];
}
else
{
return (Style)App.Current.Resources["Default" + styleName];
}
}
public double GetIconSize()
{
if (VisualType == VisualType.Small || VisualType == VisualType.SmallOutline)
{
return (double)App.Current.Resources["SmallIconSize"];
}
else
{
return (double)App.Current.Resources["DefaultIconSize"];
}
}
private void KeyVisual_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
SetEnabledState();
}
private void SetErrorState()
{
VisualStateManager.GoToState(this, IsError ? "Error" : "Default", true);
}
private void SetEnabledState()
{
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
}
}
public enum VisualType
{
Small,
SmallOutline,
Large,
LargeOutline,
}
}

View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:KeyboardManagerEditorUI.Styles">
<x:Double x:Key="DefaultIconSize">16</x:Double>
<x:Double x:Key="SmallIconSize">12</x:Double>
<Style x:Key="DefaultTextKeyVisualStyle" TargetType="local:KeyVisual">
<Setter Property="MinWidth" Value="32" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Background" Value="{ThemeResource AccentButtonBackgroundPressed}" />
<Setter Property="Foreground" Value="{ThemeResource TextOnAccentFillColorPrimaryBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource ButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
<Setter Property="Padding" Value="4,0,0,4" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyVisual">
<Grid>
<Grid Height="{TemplateBinding MinHeight}">
<Border
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4" />
<Rectangle
x:Name="ContentHolder"
Fill="{TemplateBinding Background}"
RadiusX="4"
RadiusY="4" />
<ContentPresenter
x:Name="KeyPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center"
Content="{TemplateBinding Content}"
FontSize="12"
FontWeight="Normal"
Foreground="{TemplateBinding Foreground}" />
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="ContentHolder.Fill" Value="{ThemeResource AccentButtonBackgroundDisabled}" />
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource AccentButtonForegroundDisabled}" />
<Setter Target="ContentHolder.Stroke" Value="{ThemeResource AccentButtonBorderBrushDisabled}" />
<!--<Setter Target="ContentHolder.StrokeThickness" Value="{TemplateBinding BorderThickness}" />-->
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="StateStates">
<VisualState x:Name="Default" />
<VisualState x:Name="Error">
<VisualState.Setters>
<Setter Target="ContentHolder.Fill" Value="{ThemeResource InfoBarErrorSeverityBackgroundBrush}" />
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource InfoBarErrorSeverityIconBackground}" />
<Setter Target="ContentHolder.Stroke" Value="{ThemeResource InfoBarErrorSeverityIconBackground}" />
<Setter Target="ContentHolder.StrokeThickness" Value="2" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="SmallTextKeyVisualStyle"
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
TargetType="local:KeyVisual">
<Setter Property="MinWidth" Value="40" />
<Setter Property="Height" Value="36" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="12,0,12,2" />
<Setter Property="FontSize" Value="14" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style
x:Key="LargeOutlineTextKeyVisualStyle"
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
TargetType="local:KeyVisual">
<Setter Property="Background" Value="{ThemeResource ControlFillColorDefaultBrush}" />
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
<Setter Property="BorderBrush" Value="{ThemeResource ControlStrongStrokeColorDefaultBrush}" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style
x:Key="SmallOutlineTextKeyVisualStyle"
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
TargetType="local:KeyVisual">
<Setter Property="MinWidth" Value="40" />
<Setter Property="Background" Value="{ThemeResource ControlFillColorDefaultBrush}" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource ControlStrongStrokeColorDefaultBrush}" />
<Setter Property="Height" Value="36" />
<Setter Property="BorderThickness" Value="1,1,1,1" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="8,0,8,2" />
<Setter Property="FontSize" Value="13" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style
x:Key="DefaultIconKeyVisualStyle"
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
TargetType="local:KeyVisual">
<Setter Property="MinWidth" Value="56" />
<Setter Property="MinHeight" Value="48" />
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
<Setter Property="Padding" Value="16,8,16,8" />
<Setter Property="FontSize" Value="14" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style
x:Key="SmallIconKeyVisualStyle"
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
TargetType="local:KeyVisual">
<Setter Property="MinWidth" Value="40" />
<Setter Property="Height" Value="36" />
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="Padding" Value="0" />
<Setter Property="FontSize" Value="10" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style
x:Key="SmallOutlineIconKeyVisualStyle"
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
TargetType="local:KeyVisual">
<Setter Property="MinWidth" Value="40" />
<Setter Property="Background" Value="{ThemeResource ButtonBackground}" />
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
<Setter Property="BorderBrush" Value="{ThemeResource ButtonBorderBrush}" />
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
<Setter Property="Height" Value="36" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="0" />
<Setter Property="FontSize" Value="9" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="KeyboardManagerEditorUI.Styles.TextPageInputControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:KeyboardManagerEditorUI.Styles"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Loaded="UserControl_Loaded"
mc:Ignorable="d">
<StackPanel
Width="360"
Height="360"
Orientation="Vertical">
<!-- Shortcut section -->
<TextBlock
Margin="0,12,0,8"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Text="Shortcut key(s)" />
<ToggleButton
x:Name="ShortcutToggleBtn"
Padding="0,24,0,24"
HorizontalAlignment="Stretch"
Checked="ShortcutToggleBtn_Checked"
Style="{StaticResource CustomShortcutToggleButtonStyle}">
<ToggleButton.Content>
<ItemsControl x:Name="ShortcutKeys">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel HorizontalSpacing="4" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:KeyVisual Content="{Binding}" VisualType="SmallOutline" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ToggleButton.Content>
</ToggleButton>
<!-- Text section -->
<TextBox
x:Name="TextContentBox"
Height="120"
Margin="0,8,0,0"
AcceptsReturn="True"
Background="{ThemeResource TextControlBackgroundFocused}"
BorderBrush="{ThemeResource ControlStrokeColorDefaultBrush}"
FontSize="13"
Header="Text to insert"
PlaceholderText="Enter text that will be inserted when the shortcut is pressed"
TextWrapping="Wrap" />
<!-- App specific section -->
<CheckBox
x:Name="AllAppsCheckBox"
Margin="0,8,0,0"
Content="Only apply this text shortcut to a specific application"
Foreground="{ThemeResource TextFillColorPrimaryBrush}" />
<TextBox
x:Name="AppNameTextBox"
Background="{ThemeResource TextControlBackgroundFocused}"
BorderBrush="{ThemeResource ControlStrokeColorDefaultBrush}"
Header="Application name"
IsEnabled="{Binding ElementName=AllAppsCheckBox, Path=IsChecked}"
PlaceholderText="e.g.: outlook.exe" />
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,252 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using KeyboardManagerEditorUI.Helpers;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.System;
namespace KeyboardManagerEditorUI.Styles
{
public sealed partial class TextPageInputControl : UserControl, IKeyboardHookTarget
{
private ObservableCollection<string> _shortcutKeys = new ObservableCollection<string>();
private TeachingTip? currentNotification;
private DispatcherTimer? notificationTimer;
private bool _internalUpdate;
public TextPageInputControl()
{
this.InitializeComponent();
this.ShortcutKeys.ItemsSource = _shortcutKeys;
ShortcutToggleBtn.IsChecked = true;
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
KeyboardHookHelper.Instance.ActivateHook(this);
TextContentBox.GotFocus += TextContentBox_GotFocus;
AllAppsCheckBox.Checked += AllAppsCheckBox_Changed;
AllAppsCheckBox.Unchecked += AllAppsCheckBox_Changed;
AppNameTextBox.GotFocus += AppNameTextBox_GotFocus;
AppNameTextBox.Visibility = AllAppsCheckBox.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
}
private void ShortcutToggleBtn_Checked(object sender, RoutedEventArgs e)
{
if (ShortcutToggleBtn.IsChecked == true)
{
KeyboardHookHelper.Instance.ActivateHook(this);
}
else
{
KeyboardHookHelper.Instance.CleanupHook();
}
}
public void OnKeyDown(VirtualKey key, List<string> formattedKeys)
{
_shortcutKeys.Clear();
foreach (var keyName in formattedKeys)
{
_shortcutKeys.Add(keyName);
}
UpdateAllAppsCheckBoxState();
}
private void TextContentBox_GotFocus(object sender, RoutedEventArgs e)
{
// Clean up the keyboard hook when the text box gains focus
KeyboardHookHelper.Instance.CleanupHook();
if (ShortcutToggleBtn != null && ShortcutToggleBtn.IsChecked == true)
{
ShortcutToggleBtn.IsChecked = false;
}
}
public void OnInputLimitReached()
{
ShowNotificationTip("Shortcuts can only have up to 4 modifier keys");
}
public void UpdateAllAppsCheckBoxState()
{
// Only enable app-specific remapping for shortcuts (multiple keys)
bool isShortcut = _shortcutKeys.Count > 1;
AllAppsCheckBox.IsEnabled = isShortcut;
// If it's not a shortcut, ensure the checkbox is unchecked and app textbox is hidden
try
{
if (!isShortcut)
{
_internalUpdate = true;
AllAppsCheckBox.IsChecked = false;
AppNameTextBox.Visibility = Visibility.Collapsed;
}
else if (AllAppsCheckBox.IsChecked == true)
{
AppNameTextBox.Visibility = Visibility.Visible;
}
}
finally
{
_internalUpdate = false;
}
}
private void AllAppsCheckBox_Changed(object sender, RoutedEventArgs e)
{
if (_internalUpdate)
{
return;
}
KeyboardHookHelper.Instance.CleanupHook();
if (ShortcutToggleBtn != null && ShortcutToggleBtn.IsChecked == true)
{
ShortcutToggleBtn.IsChecked = false;
}
AppNameTextBox.Visibility = AllAppsCheckBox.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
}
private void AppNameTextBox_GotFocus(object sender, RoutedEventArgs e)
{
if (_internalUpdate)
{
return;
}
KeyboardHookHelper.Instance.CleanupHook();
if (ShortcutToggleBtn != null && ShortcutToggleBtn.IsChecked == true)
{
ShortcutToggleBtn.IsChecked = false;
}
}
public void ShowNotificationTip(string message)
{
CloseExistingNotification();
currentNotification = new TeachingTip
{
Title = "Input Limit",
Subtitle = message,
IsLightDismissEnabled = true,
PreferredPlacement = TeachingTipPlacementMode.Top,
XamlRoot = this.XamlRoot,
IconSource = new SymbolIconSource { Symbol = Symbol.Important },
Target = ShortcutToggleBtn,
};
if (this.Content is Panel rootPanel)
{
rootPanel.Children.Add(currentNotification);
currentNotification.IsOpen = true;
notificationTimer = new DispatcherTimer();
notificationTimer.Interval = TimeSpan.FromMilliseconds(EditorConstants.DefaultNotificationTimeout);
notificationTimer.Tick += (s, e) =>
{
CloseExistingNotification();
};
notificationTimer.Start();
}
}
private void CloseExistingNotification()
{
if (notificationTimer != null)
{
notificationTimer.Stop();
notificationTimer = null;
}
if (currentNotification != null && currentNotification.IsOpen)
{
currentNotification.IsOpen = false;
if (this.Content is Panel rootPanel && rootPanel.Children.Contains(currentNotification))
{
rootPanel.Children.Remove(currentNotification);
}
currentNotification = null;
}
}
public void ClearKeys()
{
_shortcutKeys.Clear();
UpdateAllAppsCheckBoxState();
}
public List<string> GetShortcutKeys()
{
List<string> keys = new List<string>();
foreach (var key in _shortcutKeys)
{
keys.Add(key);
}
return keys;
}
public string GetTextContent()
{
return TextContentBox.Text;
}
public bool GetIsAppSpecific()
{
return AllAppsCheckBox.IsChecked ?? false;
}
public string GetAppName()
{
return AllAppsCheckBox.IsChecked == true ? AppNameTextBox.Text : string.Empty;
}
public void SetShortcutKeys(List<string> keys)
{
if (keys != null)
{
_shortcutKeys.Clear();
foreach (var key in keys)
{
_shortcutKeys.Add(key);
}
}
UpdateAllAppsCheckBoxState();
}
public void SetTextContent(string text)
{
TextContentBox.Text = text;
}
public void SetAppSpecific(bool isAppSpecific, string appName)
{
AllAppsCheckBox.IsChecked = isAppSpecific;
if (isAppSpecific)
{
AppNameTextBox.Text = appName;
}
}
}
}

View File

@@ -297,7 +297,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Logger.LogError("Failed to launch the new WinUI3 Editor", e);
}
string path = Path.Combine(Environment.CurrentDirectory, editorPath);
string path = Path.Combine(Directory.GetParent(Environment.CurrentDirectory).ToString(), editorPath);
Logger.LogInfo($"Starting {PowerToyName} editor from {path}");
// InvariantCulture: type represents the KeyboardManagerEditorType enum value