[KBM]Launch apps / URI with keyboard shortcuts, support chords (#30121)

* Working UI update with just runProgram Path and isRunProgram

* First working, basic. no args or path, or setting change detections.

* Revert and fixed.

* Some clean up, working with config file monitor

* Args and Start-in should be working.

* File monitor, quotes, xaml screens one

* Fixed enable/disable toogle from XAML

* Code cleanup.

* Betting logging.

* Cleanup, start of RunProgramDescriptor and usage of run_non_elevated/run_elevated

* Code moved to KeyboardEventHandlers, but not enabled since it won't build as is, needs elevation.h. Other testing..

* Key chords working, pretty much

* Added gui for elevation level, need to refresh on change...

* f: include shellapi.h and reference wil in KBMEL

* run_elevated/run_non_elevated sorted out. Working!

* Removed lots of old temp code.

* Fix some speling errors.

* Cleanup before trying to add a UI for the chord

* Added "DifferentUser" option

* Closer on UI for chords.

* Better UI, lots working.

* Clean up

* Text for “Allow chords” – needs to look better…

* Bugs and clean-up

* Cleanup

* Refactor and clean up.

* More clean up

* Some localization.

* Don’t show “Allow chords“ to the “to” shortcut

* Maybe better foreground after opening new app

* Better chord matching.

* Runprogram fix for stealing existing shortcut.

* Better runProgram stuff

* Temp commit

* Working well

* Toast test

* More toast

* Added File and Folder picker UI

* Pre-check on run program file exists.

* Refactor to SetupRunProgramControls

* Open URI UI is going.

* Open URI working well

* Open URI stuff working well

* Allowed AppSpecific shortcut and fixed backup/restore shortcut dups

* Fixed settings screen

* Start of code to find by name...

* UI fixed

* Small fixes

* Some single edit code working.

* UI getting better.

* Fixes

* Fixed and merge from main

* UI updates

* UI updates.

* UI stuff

* Fixed crash from move ui item locations.

* Fixed crash from move ui item locations.

* Added delete confirm

* Basic sound working.

* Localized some stuff

* Added sounds

* Better experiance when shortcut is in use.

* UI tweaks

* Fixed KBM ui for unicode shortcut not having ","

* Some clean up

* Cleanup

* Cleanup

* Fixed applyXamlStyling

* Added back stuff lost in merge

* applyXamlStyling, again

* Fixed crash on change from non shortcut to shortcut

* Update src/modules/keyboardmanager/KeyboardManagerEngineTest/KeyboardManagerEngineTest.vcxproj

* Fixed some spelling type issues.

* ImplementationLibrary 231216

* Comment bump to see if the Microsoft.Windows.ImplementationLibrary version thing gets picked up

* Correct, Microsoft.Windows.ImplementationLibrary, finally?

* Fixed two test that failed because we now allow key-chords.

* Removed shortcut sounds.

* use original behavior when "allow chords" is off in shortcut window

* fix crash when editing a shortcut that has apps specified for it

* split KBM chords with comma on dashboard page

* Fix some spelling items.

* More "spelling"

* Fix XAML styling

* align TextBlock and ToggleSwitch

* fix cutoff issue at the top

* increase ComboBox width

* Added *Unsupported* for backwards compat on config of KBM

* fix spellcheck

* Fix crash on Remap key screen

* Fixed Remap Keys ComboBox width too short.

* Removed KBM Single Edit mode, fixed crash.

* Fix Xaml with xaml cops

* Fix crash on setting "target app" for some types of shortcuts.

* Space to toggle chord, combobox back

* fix spellcheck

* fix some code nits

* Code review updates.

* Add exclusions to the bug report tool

* Code review and kill CloseAndEndTask

* Fix alignment / 3 comboboxes per row

* Fix daily telemetry events to exclude start app and open URI

* Add chords and remove app start and open uri from config telemetry

* comma instead of plus in human readable shortcut telemetry data

* Code review, restore default-old state when new row added in KBM

* Code review, restore default-old state when new row added in KBM, part 2

* Still show target app on Settings

* Only allow enabling chords for origin shortcuts

---------

Co-authored-by: Andrey Nekrasov <yuyoyuppe@users.noreply.github.com>
Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
This commit is contained in:
Jeff Lord 2024-02-27 18:12:05 -05:00 committed by GitHub
parent 1a5349bf1e
commit 725c8e8c19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 4373 additions and 1571 deletions

View File

@ -84,6 +84,7 @@ Ctrls
EXSEL
HOLDENTER
HOLDESC
HOLDSPACE
KBDLLHOOKSTRUCT
keyevent
LAlt

View File

@ -99,6 +99,7 @@ Pooja
Quriz
randyrants
ricardosantos
ritchielawrence
robmikh
Rutkas
ryanbodrug
@ -128,6 +129,7 @@ Zykova
# OTHERS
cmdow
Controlz
cortana
fancymouse

View File

@ -99,6 +99,7 @@ bbwe
bck
BESTEFFORT
bhid
BIF
bigbar
bigobj
binlog
@ -125,6 +126,7 @@ BPBF
bpmf
bpp
Browsable
BROWSEINFO
bsd
bthprops
bti
@ -212,6 +214,7 @@ cominterop
commandline
COMMANDTITLE
commctrl
commdlg
compmgmt
COMPOSITIONFULL
comsupp
@ -473,6 +476,7 @@ FILELOCKSMITHCONTEXTMENU
FILELOCKSMITHEXT
FILELOCKSMITHLIB
FILELOCKSMITHLIBINTEROP
FILEMUSTEXIST
FILEOP
FILEOS
FILESUBTYPE
@ -801,6 +805,7 @@ LPCTSTR
lpdw
lpfn
LPINPUT
LPITEMIDLIST
lpmi
LPMINMAXINFO
LPMONITORINFO
@ -808,6 +813,7 @@ LPOSVERSIONINFOEXW
LPQUERY
lprc
LPSAFEARRAY
lpstr
lpsz
lpt
LPTHREAD
@ -971,6 +977,7 @@ netsetup
netsh
newcolor
newdev
NEWDIALOGSTYLE
newitem
newpath
newrow
@ -1041,6 +1048,7 @@ ocr
Ocrsettings
odbccp
officehubintl
OFN
ofs
oldcolor
olditem
@ -1051,6 +1059,7 @@ OLECHAR
onebranch
OOBEPT
opencode
OPENFILENAME
opensource
openxmlformats
OPTIMIZEFORINVOKE
@ -1085,6 +1094,7 @@ parray
PARTIALCONFIRMATIONDIALOGTITLE
PATCOPY
pathcch
PATHMUSTEXIST
Pathto
PATINVERT
PATPAINT
@ -1173,6 +1183,7 @@ PRINTCLIENT
printmanagement
prm
proactively
PROCESSENTRY
PROCESSKEY
processthreadsapi
PRODEXT
@ -1292,6 +1303,7 @@ RESTORETOMAXIMIZED
restrictedcapabilities
restrictederrorinfo
resultlist
RETURNONLYFSDIRS
RGBQUAD
rgbs
rgelt
@ -1436,6 +1448,7 @@ sln
SMALLICON
smartphone
SMTO
SNAPPROCESS
snwprintf
softline
SOURCECLIENTAREAONLY
@ -1578,6 +1591,7 @@ tlbimp
TMPVAR
TNP
toggleswitch
Toolhelp
toolkitconverters
Toolset
toolwindow
@ -1634,6 +1648,7 @@ Updatelayout
UPGRADINGPRODUCTCODE
Uptool
urld
urlmon
Usb
USEDEFAULT
USEFILEATTRIBUTES
@ -1709,6 +1724,7 @@ wcautil
WCE
wcex
WClass
wcsicmp
wcsnicmp
WDA
wdp

View File

@ -27,6 +27,17 @@ std::wstring LayoutMap::GetKeyName(DWORD key)
return impl->GetKeyName(key);
}
DWORD LayoutMap::GetKeyFromName(const std::wstring& name)
{
auto list = impl->GetKeyNameList(false);
for (const auto& [value, key] : list)
{
if (key == name)
return value;
}
return {};
}
std::vector<DWORD> LayoutMap::GetKeyCodeList(const bool isShortcut)
{
return impl->GetKeyCodeList(isShortcut);

View File

@ -11,6 +11,7 @@ public:
~LayoutMap();
void UpdateLayout();
std::wstring GetKeyName(DWORD key);
DWORD GetKeyFromName(const std::wstring& name);
std::vector<DWORD> GetKeyCodeList(const bool isShortcut = false);
std::vector<std::pair<DWORD, std::wstring>> GetKeyNameList(const bool isShortcut = false);

View File

@ -71,7 +71,7 @@ namespace
}
result = spView->QueryInterface(riid, ppv);
if (result != S_OK || ppv == nullptr || *ppv == nullptr )
if (result != S_OK || ppv == nullptr || *ppv == nullptr)
{
Logger::warn(L"Failed to query interface. {}", GetErrorString(result));
return false;
@ -83,7 +83,7 @@ namespace
inline bool GetDesktopAutomationObject(REFIID riid, void** ppv)
{
CComPtr<IShellView> spsv;
// Desktop may not be available on startup
auto attempts = 5;
for (auto i = 1; i <= attempts; i++)
@ -207,8 +207,34 @@ inline bool drop_elevated_privileges()
return result;
}
// Run command as different user, returns true if succeeded
inline HANDLE run_as_different_user(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true)
{
Logger::info(L"run_elevated with params={}", params);
SHELLEXECUTEINFOW exec_info = { 0 };
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
exec_info.lpVerb = L"runAsUser";
exec_info.lpFile = file.c_str();
exec_info.lpParameters = params.c_str();
exec_info.hwnd = 0;
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
exec_info.lpDirectory = workingDir;
exec_info.hInstApp = 0;
if (showWindow)
{
exec_info.nShow = SW_SHOWDEFAULT;
}
else
{
// might have limited success, but only option with ShellExecuteExW
exec_info.nShow = SW_HIDE;
}
return ShellExecuteExW(&exec_info) ? exec_info.hProcess : nullptr;
}
// Run command as elevated user, returns true if succeeded
inline HANDLE run_elevated(const std::wstring& file, const std::wstring& params)
inline HANDLE run_elevated(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true)
{
Logger::info(L"run_elevated with params={}", params);
SHELLEXECUTEINFOW exec_info = { 0 };
@ -218,15 +244,24 @@ inline HANDLE run_elevated(const std::wstring& file, const std::wstring& params)
exec_info.lpParameters = params.c_str();
exec_info.hwnd = 0;
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
exec_info.lpDirectory = 0;
exec_info.lpDirectory = workingDir;
exec_info.hInstApp = 0;
exec_info.nShow = SW_SHOWDEFAULT;
if (showWindow)
{
exec_info.nShow = SW_SHOWDEFAULT;
}
else
{
// might have limited success, but only option with ShellExecuteExW
exec_info.nShow = SW_HIDE;
}
return ShellExecuteExW(&exec_info) ? exec_info.hProcess : nullptr;
}
// Run command as non-elevated user, returns true if succeeded, puts the process id into returnPid if returnPid != NULL
inline bool run_non_elevated(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr)
inline bool run_non_elevated(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr, const bool showWindow = true)
{
Logger::info(L"run_non_elevated with params={}", params);
auto executable_args = L"\"" + file + L"\"";
@ -292,15 +327,22 @@ inline bool run_non_elevated(const std::wstring& file, const std::wstring& param
STARTUPINFOEX siex = { 0 };
siex.lpAttributeList = pptal;
siex.StartupInfo.cb = sizeof(siex);
PROCESS_INFORMATION pi = { 0 };
auto dwCreationFlags = EXTENDED_STARTUPINFO_PRESENT;
if (!showWindow)
{
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
siex.StartupInfo.wShowWindow = SW_HIDE;
dwCreationFlags = CREATE_NO_WINDOW;
}
auto succeeded = CreateProcessW(file.c_str(),
&executable_args[0],
nullptr,
nullptr,
FALSE,
EXTENDED_STARTUPINFO_PRESENT,
dwCreationFlags,
nullptr,
workingDir,
&siex.StartupInfo,
@ -341,7 +383,10 @@ inline bool RunNonElevatedEx(const std::wstring& file, const std::wstring& param
catch (...)
{
}
if (SUCCEEDED(co_init)) CoUninitialize();
if (SUCCEEDED(co_init))
{
CoUninitialize();
}
return success;
}
@ -372,7 +417,7 @@ inline std::optional<ProcessInfo> RunNonElevatedFailsafe(const std::wstring& fil
}
}
auto handles = getProcessHandlesByName(std::filesystem::path{ file }.filename().wstring(), PROCESS_QUERY_INFORMATION | SYNCHRONIZE | handleAccess );
auto handles = getProcessHandlesByName(std::filesystem::path{ file }.filename().wstring(), PROCESS_QUERY_INFORMATION | SYNCHRONIZE | handleAccess);
if (handles.empty())
return std::nullopt;
@ -385,7 +430,7 @@ inline std::optional<ProcessInfo> RunNonElevatedFailsafe(const std::wstring& fil
}
// Run command with the same elevation, returns true if succeeded
inline bool run_same_elevation(const std::wstring& file, const std::wstring& params, DWORD* returnPid)
inline bool run_same_elevation(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr)
{
auto executable_args = L"\"" + file + L"\"";
if (!params.empty())
@ -403,7 +448,7 @@ inline bool run_same_elevation(const std::wstring& file, const std::wstring& par
FALSE,
0,
nullptr,
nullptr,
workingDir,
&si,
&pi);

View File

@ -77,8 +77,26 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
type = static_cast<KeyboardManagerEditorType>(_wtoi(cmdArgs[1]));
}
if (numArgs == 3)
std::wstring keysForShortcutToEdit = L"";
std::wstring action = L"";
// do some parsing of the cmdline arg to see if we need to behave different
// like, single edit mode, or "delete" mode.
// These extra args are from "OpenEditor" in the KeyboardManagerViewModel
if (numArgs >= 3)
{
if (numArgs >= 4)
{
keysForShortcutToEdit = std::wstring(cmdArgs[3]);
}
if (numArgs >= 5)
{
action = std::wstring(cmdArgs[4]);
}
std::wstring pid = std::wstring(cmdArgs[2]);
Logger::trace(L"Editor started from the settings with pid {}", pid);
if (!pid.empty())
@ -108,7 +126,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
return -1;
}
editor->OpenEditorWindow(type);
editor->OpenEditorWindow(type, keysForShortcutToEdit, action);
editor = nullptr;
@ -149,7 +167,7 @@ bool KeyboardManagerEditor::StartLowLevelKeyboardHook()
return (hook != nullptr);
}
void KeyboardManagerEditor::OpenEditorWindow(KeyboardManagerEditorType type)
void KeyboardManagerEditor::OpenEditorWindow(KeyboardManagerEditorType type, std::wstring keysForShortcutToEdit, std::wstring action)
{
switch (type)
{
@ -157,7 +175,7 @@ void KeyboardManagerEditor::OpenEditorWindow(KeyboardManagerEditorType type)
CreateEditKeyboardWindow(hInstance, keyboardManagerState, mappingConfiguration);
break;
case KeyboardManagerEditorType::ShortcutEditor:
CreateEditShortcutsWindow(hInstance, keyboardManagerState, mappingConfiguration);
CreateEditShortcutsWindow(hInstance, keyboardManagerState, mappingConfiguration, keysForShortcutToEdit, action);
}
}

View File

@ -23,20 +23,20 @@ public:
}
bool StartLowLevelKeyboardHook();
void OpenEditorWindow(KeyboardManagerEditorType type);
void OpenEditorWindow(KeyboardManagerEditorType type, std::wstring keysForShortcutToEdit, std::wstring action);
// Function called by the hook procedure to handle the events. This is the starting point function for remapping
intptr_t HandleKeyboardHookEvent(LowlevelKeyboardEvent* data) noexcept;
private:
static LRESULT CALLBACK KeyHookProc(int nCode, WPARAM wParam, LPARAM lParam);
inline static HHOOK hook;
HINSTANCE hInstance;
KBMEditor::KeyboardManagerState keyboardManagerState;
MappingConfiguration mappingConfiguration;
// Object of class which implements InputInterface. Required for calling library functions while enabling testing
KeyboardManagerInput::Input inputHandler;
};

View File

@ -154,7 +154,9 @@
<Manifest Include="KeyboardManagerEditor.exe.manifest" />
</ItemGroup>
<ItemGroup>
<None Include="Resources.resx" />
<None Include="Resources.resx">
<SubType>Designer</SubType>
</None>
</ItemGroup>
<ItemGroup>
<Image Include="Keyboard.ico" />

View File

@ -144,6 +144,15 @@
<data name="EditShortcuts_WindowName" xml:space="preserve">
<value>Remap shortcuts</value>
</data>
<data name="Edit_This_Shortcut_WindowName" xml:space="preserve">
<value>Edit shortcut</value>
</data>
<data name="Browse_For_Program_Button" xml:space="preserve">
<value>Select program</value>
</data>
<data name="Browse_For_Path_Button" xml:space="preserve">
<value>Select path</value>
</data>
<data name="Ok_Button" xml:space="preserve">
<value>OK</value>
</data>
@ -161,12 +170,60 @@
<value>To send:</value>
</data>
<data name="EditShortcuts_TargetHeader" xml:space="preserve">
<value>To send:</value>
<value>To:</value>
</data>
<data name="EditShortcuts_TargetAppHeader" xml:space="preserve">
<value>Target app:</value>
</data>
<data name="EditKeyboard_OrphanedDialogTitle" xml:space="preserve">
<data name="EditShortcuts_Label_Args" xml:space="preserve">
<value>Args:</value>
</data>
<data name="EditShortcuts_Label_Elevation" xml:space="preserve">
<value>Elevation:</value>
</data>
<data name="EditShortcuts_Label_Start_As" xml:space="preserve">
<value>Visibility:</value>
</data>
<data name="EditShortcuts_Label_Program" xml:space="preserve">
<value>App:</value>
</data>
<data name="EditShortcuts_Label_Start_In" xml:space="preserve">
<value>Start in:</value>
</data>
<data name="EditShortcuts_Label_Action" xml:space="preserve">
<value>Action:</value>
</data>
<data name="EditShortcuts_Label_Path_URI" xml:space="preserve">
<value>Path/URI:</value>
</data>
<data name="EditShortcuts_Label_Shortcut" xml:space="preserve">
<value>Shortcut:</value>
</data>
<data name="EditShortcuts_Label_Keys" xml:space="preserve">
<value>Keys:</value>
</data>
<data name="EditShortcuts_Visibility_Normal" xml:space="preserve">
<value>Normal</value>
</data>
<data name="EditShortcuts_Visibility_Hidden" xml:space="preserve">
<value>Hidden</value>
</data>
<data name="EditShortcuts_Label_If_Running" xml:space="preserve">
<value>If running:</value>
</data>
<data name="EditShortcuts_Label_App_Running_Sound" xml:space="preserve">
<value>Running sound:</value>
</data>
<data name="EditShortcuts_LabelApp_Not_Running_Sound" xml:space="preserve">
<value>Start sound:</value>
</data>
<data name="EditShortcuts_Bad_Key" xml:space="preserve">
<value>Key in use</value>
</data>
<data name="EditShortcuts_BtnSelectFile" xml:space="preserve">
<value>Select file</value>
</data>
<data name="EditKeyboard_OrphanedDialogTitle" xml:space="preserve">
<value>Warning: The following keys do not have assignments:</value>
<comment>Key on a keyboard</comment>
</data>
@ -184,7 +241,11 @@
<comment>Key on a keyboard</comment>
</data>
<data name="EditShortcuts_Info" xml:space="preserve">
<value>Select the shortcut you want to change ("Select") and then configure the key, shortcut or text you want it to send ("To send").</value>
<value>Edit shortcuts to activate and then configure the actions to happen when activated.</value>
<comment>Key on a keyboard</comment>
</data>
<data name="Edit_Single_Shortcut_Info" xml:space="preserve">
<value>Edit shortcut to activate and then configure the action to happen when activated.</value>
<comment>Key on a keyboard</comment>
</data>
<data name="EditShortcuts_InfoExample" xml:space="preserve">
@ -211,6 +272,9 @@
<value>Keys selected:</value>
<comment>Keys on a keyboard</comment>
</data>
<data name="Type_HoldSpace" xml:space="preserve">
<value>Hold Space to toggle allow chord</value>
</data>
<data name="Type_HoldEnter" xml:space="preserve">
<value>Hold Enter to continue</value>
</data>
@ -220,6 +284,24 @@
<data name="EditShortcuts_AllApps" xml:space="preserve">
<value>All apps</value>
</data>
<data name="EditShortcuts_Allow_Chords" xml:space="preserve">
<value>Allow chords</value>
</data>
<data name="EditShortcuts_Path_To_Program" xml:space="preserve">
<value>Path to program</value>
</data>
<data name="EditShortcuts_Uri_Example" xml:space="preserve">
<value>E.g.: https://website.url</value>
</data>
<data name="EditShortcuts_What_Can_I_Use_Link" xml:space="preserve">
<value>What can I use?</value>
</data>
<data name="EditShortcuts_Args_For_Program" xml:space="preserve">
<value>Arguments for program</value>
</data>
<data name="EditShortcuts_Start_In_Dir_For_Program" xml:space="preserve">
<value>Start in directory</value>
</data>
<data name="ErrorMessage_RemapSuccessful" xml:space="preserve">
<value>Remapping successful</value>
</data>
@ -288,12 +370,20 @@
<comment>Key on a keyboard</comment>
</data>
<data name="Mapping_Type_DropDown_Text" xml:space="preserve">
<value>Text</value>
<value>Send Text</value>
</data>
<data name="Mapping_Type_DropDown_Key_Shortcut" xml:space="preserve">
<value>Key/Shortcut</value>
<value>Send Key/Shortcut</value>
<comment>Key on a keyboard</comment>
</data>
<data name="Mapping_Type_DropDown_Run_Program" xml:space="preserve">
<value>Run Program</value>
<comment>Run Program</comment>
</data>
<data name="Mapping_Type_DropDown_Open_URI" xml:space="preserve">
<value>Open URI</value>
<comment>URI to open.</comment>
</data>
<data name="Add_Key_Remap_Button" xml:space="preserve">
<value>Add key remapping</value>
<comment>Key on a keyboard</comment>
@ -304,6 +394,33 @@
<data name="Delete_Remapping_Button" xml:space="preserve">
<value>Delete remapping</value>
</data>
<data name="Already_Running_Do_Nothing" xml:space="preserve">
<value>Do nothing</value>
</data>
<data name="Already_Running_Show_Window" xml:space="preserve">
<value>Show window</value>
</data>
<data name="Already_Running_Start_Another" xml:space="preserve">
<value>Start another</value>
</data>
<data name="Already_Running_Close" xml:space="preserve">
<value>Close</value>
</data>
<data name="Already_Running_Terminate" xml:space="preserve">
<value>End task</value>
</data>
<data name="Already_Running_Close_And_Terminate" xml:space="preserve">
<value>Close and end task</value>
</data>
<data name="Elevation_Type_Normal" xml:space="preserve">
<value>Normal</value>
</data>
<data name="Elevation_Type_Elevated" xml:space="preserve">
<value>Elevated</value>
</data>
<data name="Elevation_Type_Different_User" xml:space="preserve">
<value>Different User</value>
</data>
<data name="Delete_Remapping_Event" xml:space="preserve">
<value>Remapping deleted</value>
</data>

View File

@ -204,8 +204,9 @@ namespace BufferValidationHelpers
}
else
{
// warn and reset the drop down
errorType = ShortcutErrorType::ShortcutNotMoreThanOneActionKey;
// this used to "warn and reset the drop down" but for now, since we will allow Chords, we do allow this
// leaving the here and commented out for posterity, for now.
// errorType = ShortcutErrorType::ShortcutNotMoreThanOneActionKey;
}
}
else

View File

@ -69,7 +69,7 @@ static IAsyncAction OnClickAccept(
}
// Function to create the Edit Shortcuts Window
inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardManagerState& keyboardManagerState, MappingConfiguration& mappingConfiguration)
inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardManagerState& keyboardManagerState, MappingConfiguration& mappingConfiguration, std::wstring keysForShortcutToEdit, std::wstring action)
{
Logger::trace("CreateEditShortcutsWindowImpl()");
auto locker = EventLocker::Get(KeyboardManagerConstants::EditorWindowEventName.c_str());
@ -107,12 +107,24 @@ inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardMa
isEditShortcutsWindowRegistrationCompleted = true;
}
// we might be passed via cmdline some keysForShortcutToEdit, this means we're editing just one shortcut
if (!keysForShortcutToEdit.empty())
{
}
// Find coordinates of the screen where the settings window is placed.
RECT desktopRect = UIHelpers::GetForegroundWindowDesktopRect();
// Calculate DPI dependent window size
float windowWidth = EditorConstants::DefaultEditShortcutsWindowWidth;
float windowHeight = EditorConstants::DefaultEditShortcutsWindowHeight;
if (!keysForShortcutToEdit.empty())
{
windowWidth = EditorConstants::DefaultEditSingleShortcutsWindowWidth;
windowHeight = EditorConstants::DefaultEditSingleShortcutsWindowHeight;
}
DPIAware::ConvertByCursorPosition(windowWidth, windowHeight);
DPIAware::GetScreenDPIForCursor(g_currentDPI);
@ -129,13 +141,13 @@ inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardMa
NULL,
hInst,
NULL);
if (_hWndEditShortcutsWindow == NULL)
{
MessageBox(NULL, GET_RESOURCE_STRING(IDS_CREATEWINDOWFAILED_ERRORMESSAGE).c_str(), GET_RESOURCE_STRING(IDS_CREATEWINDOWFAILED_ERRORTITLE).c_str(), NULL);
return;
}
// Ensures the window is in foreground on first startup. If this is not done, the window appears behind because the thread is not on the foreground.
if (_hWndEditShortcutsWindow)
{
@ -157,7 +169,7 @@ inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardMa
// Create the xaml bridge object
XamlBridge2 xamlBridge(_hWndEditShortcutsWindow);
// Create the desktop window xaml source object and set its content
hWndXamlIslandEditShortcutsWindow = xamlBridge.InitBridge();
@ -227,15 +239,15 @@ inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardMa
// Store handle of edit shortcuts window
ShortcutControl::editShortcutsWindowHandle = _hWndEditShortcutsWindow;
// Store keyboard manager state
ShortcutControl::keyboardManagerState = &keyboardManagerState;
KeyDropDownControl::keyboardManagerState = &keyboardManagerState;
KeyDropDownControl::mappingConfiguration = &mappingConfiguration;
// Clear the shortcut remap buffer
ShortcutControl::shortcutRemapBuffer.clear();
// Vector to store dynamically allocated control objects to avoid early destruction
std::vector<std::vector<std::unique_ptr<ShortcutControl>>> keyboardRemapControlObjects;
@ -246,27 +258,9 @@ inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardMa
// Create copy of the remaps to avoid concurrent access
ShortcutRemapTable osLevelShortcutReMapCopy = mappingConfiguration.osLevelShortcutReMap;
for (const auto& it : osLevelShortcutReMapCopy)
{
ShortcutControl::AddNewShortcutControlRow(shortcutTable, keyboardRemapControlObjects, it.first, it.second.targetShortcut);
}
// Load existing app-specific shortcuts into UI
// Create copy of the remaps to avoid concurrent access
AppSpecificShortcutRemapTable appSpecificShortcutReMapCopy = mappingConfiguration.appSpecificShortcutReMap;
// Iterate through all the apps
for (const auto& itApp : appSpecificShortcutReMapCopy)
{
// Iterate through shortcuts for each app
for (const auto& itShortcut : itApp.second)
{
ShortcutControl::AddNewShortcutControlRow(shortcutTable, keyboardRemapControlObjects, itShortcut.first, itShortcut.second.targetShortcut, itApp.first);
}
}
// Apply button
Button applyButton;
applyButton.Name(L"applyButton");
applyButton.Content(winrt::box_value(GET_RESOURCE_STRING(IDS_OK_BUTTON)));
applyButton.Style(AccentButtonStyle());
applyButton.MinWidth(EditorConstants::HeaderButtonWidth);
@ -284,7 +278,44 @@ inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardMa
OnClickAccept(keyboardManagerState, applyButton.XamlRoot(), ApplyRemappings);
});
auto OnClickAcceptNoCheckFn = ApplyRemappings;
for (const auto& it : osLevelShortcutReMapCopy)
{
auto isHidden = false;
// check to see if this should be hidden because it's NOT the one we are looking for.
// It will still be there for backward compatability, just not visible
if (!keysForShortcutToEdit.empty())
{
isHidden = (keysForShortcutToEdit != it.first.ToHstringVK());
}
ShortcutControl::AddNewShortcutControlRow(shortcutTable, keyboardRemapControlObjects, it.first, it.second.targetShortcut, L"");
}
// Load existing app-specific shortcuts into UI
// Create copy of the remaps to avoid concurrent access
AppSpecificShortcutRemapTable appSpecificShortcutReMapCopy = mappingConfiguration.appSpecificShortcutReMap;
// Iterate through all the apps
for (const auto& itApp : appSpecificShortcutReMapCopy)
{
// Iterate through shortcuts for each app
for (const auto& itShortcut : itApp.second)
{
auto isHidden = false;
if (!keysForShortcutToEdit.empty())
{
isHidden = (keysForShortcutToEdit != itShortcut.first.ToHstringVK());
}
ShortcutControl::AddNewShortcutControlRow(shortcutTable, keyboardRemapControlObjects, itShortcut.first, itShortcut.second.targetShortcut, itApp.first);
}
}
header.Children().Append(headerText);
header.Children().Append(applyButton);
header.Children().Append(cancelButton);
@ -299,17 +330,41 @@ inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardMa
addShortcut.Margin({ 10, 10, 0, 25 });
addShortcut.Style(AccentButtonStyle());
addShortcut.Click([&](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
ShortcutControl::AddNewShortcutControlRow(shortcutTable, keyboardRemapControlObjects);
ShortcutControl& newShortcut = ShortcutControl::AddNewShortcutControlRow(shortcutTable, keyboardRemapControlObjects);
// Whenever a remap is added move to the bottom of the screen
scrollViewer.ChangeView(nullptr, scrollViewer.ScrollableHeight(), nullptr);
// Set focus to the first Type Button in the newly added row
UIHelpers::SetFocusOnTypeButtonInLastRow(shortcutTable, EditorConstants::ShortcutTableColCount);
//newShortcut.OpenNewShortcutControlRow(shortcutTable, shortcutTable.Children().GetAt(shortcutTable.Children().Size() - 1).as<StackPanel>());
});
// if this is a delete action we just want to quick load the screen to delete the shortcut and close
// this is so we can delete from the KBM settings screen
if (action == L"isDelete")
{
auto indexToDelete = -1;
for (int i = 0; i < ShortcutControl::shortcutRemapBuffer.size(); i++)
{
auto tempShortcut = std::get<Shortcut>(ShortcutControl::shortcutRemapBuffer[i].first[0]);
if (tempShortcut.ToHstringVK() == keysForShortcutToEdit)
{
indexToDelete = i;
}
}
if (indexToDelete >= 0)
{
ShortcutControl::shortcutRemapBuffer.erase(ShortcutControl::shortcutRemapBuffer.begin() + indexToDelete);
}
OnClickAcceptNoCheckFn();
return;
}
// Remap shortcut button content
StackPanel addShortcutContent;
addShortcutContent.Orientation(Orientation::Horizontal);
addShortcutContent.Spacing(10);
addShortcutContent.Children().Append(SymbolIcon(Symbol::Add));
@ -333,6 +388,7 @@ inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardMa
mappingsPanel.Children().Append(addShortcut);
// Remapping table should be scrollable
scrollViewer.Content(mappingsPanel);
RelativePanel xamlContainer;
@ -347,6 +403,7 @@ inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardMa
xamlContainer.Children().Append(header);
xamlContainer.Children().Append(helperText);
xamlContainer.Children().Append(scrollViewer);
try
{
// If a layout update has been triggered by other methods (e.g.: adapting to zoom level), this may throw an exception.
@ -368,6 +425,7 @@ inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardMa
// Mica isn't available
xamlContainer.Background(Application::Current().Resources().Lookup(box_value(L"ApplicationPageBackgroundThemeBrush")).as<Media::SolidColorBrush>());
}
Window::Current().Content(xamlContent);
////End XAML Island section
@ -389,10 +447,10 @@ inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardMa
keyboardManagerState.ClearRegisteredKeyDelays();
}
void CreateEditShortcutsWindow(HINSTANCE hInst, KBMEditor::KeyboardManagerState& keyboardManagerState, MappingConfiguration& mappingConfiguration)
void CreateEditShortcutsWindow(HINSTANCE hInst, KBMEditor::KeyboardManagerState& keyboardManagerState, MappingConfiguration& mappingConfiguration, std::wstring keysForShortcutToEdit, std::wstring action)
{
// Move implementation into the separate method so resources get destroyed correctly
CreateEditShortcutsWindowImpl(hInst, keyboardManagerState, mappingConfiguration);
CreateEditShortcutsWindowImpl(hInst, keyboardManagerState, mappingConfiguration, keysForShortcutToEdit, action);
// Calling ClearXamlIslands() outside of the message loop is not enough to prevent
// Microsoft.UI.XAML.dll from crashing during deinitialization, see https://github.com/microsoft/PowerToys/issues/10906
@ -420,7 +478,9 @@ LRESULT CALLBACK EditShortcutsWindowProc(HWND hWnd, UINT messageCode, WPARAM wPa
LPMINMAXINFO mmi = reinterpret_cast<LPMINMAXINFO>(lParam);
float minWidth = EditorConstants::MinimumEditShortcutsWindowWidth;
float minHeight = EditorConstants::MinimumEditShortcutsWindowHeight;
DPIAware::Convert(MonitorFromWindow(hWnd, MONITOR_DEFAULTTONULL), minWidth, minHeight);
mmi->ptMinTrackSize.x = static_cast<LONG>(minWidth);
mmi->ptMinTrackSize.y = static_cast<LONG>(minHeight);
}
@ -453,8 +513,7 @@ LRESULT CALLBACK EditShortcutsWindowProc(HWND hWnd, UINT messageCode, WPARAM wPa
rect->top,
rect->right - rect->left,
rect->bottom - rect->top,
SWP_NOZORDER | SWP_NOACTIVATE
);
SWP_NOZORDER | SWP_NOACTIVATE);
Logger::trace(L"WM_DPICHANGED: new dpi {} rect {} {} ", newDPI, rect->right - rect->left, rect->bottom - rect->top);
}

View File

@ -8,7 +8,7 @@ namespace KBMEditor
class MappingConfiguration;
// Function to create the Edit Shortcuts Window
void CreateEditShortcutsWindow(HINSTANCE hInst, KBMEditor::KeyboardManagerState& keyboardManagerState, MappingConfiguration& mappingConfiguration);
void CreateEditShortcutsWindow(HINSTANCE hInst, KBMEditor::KeyboardManagerState& keyboardManagerState, MappingConfiguration& mappingConfiguration, std::wstring keysForShortcutToEdit, std::wstring action);
// Function to check if there is already a window active if yes bring to foreground
bool CheckEditShortcutsWindowActive();

View File

@ -10,8 +10,13 @@ namespace EditorConstants
inline const int EditKeyboardTableMinWidth = 700;
inline const int DefaultEditShortcutsWindowWidth = 1410;
inline const int DefaultEditShortcutsWindowHeight = 600;
inline const int DefaultEditSingleShortcutsWindowWidth = 1080;
inline const int DefaultEditSingleShortcutsWindowHeight = 400;
inline const int MinimumEditShortcutsWindowWidth = 500;
inline const int MinimumEditShortcutsWindowHeight = 500;
inline const int MinimumEditSingleShortcutsWindowWidth = 500;
inline const int MinimumEditSingleShortcutsWindowHeight = 600;
inline const int EditShortcutsTableMinWidth = 1000;
// Key Remap table constants

View File

@ -52,6 +52,16 @@ namespace EditorHelpers
// Function to return true if the shortcut is valid. A valid shortcut has atleast one modifier, as well as an action key
bool IsValidShortcut(Shortcut shortcut)
{
if (shortcut.operationType == Shortcut::OperationType::RunProgram && shortcut.runProgramFilePath.length() > 0)
{
return true;
}
if (shortcut.operationType == Shortcut::OperationType::OpenURI && shortcut.uriToOpen.length() > 0)
{
return true;
}
if (shortcut.actionKey != NULL)
{
if (shortcut.winKey != ModifierKey::Disabled || shortcut.ctrlKey != ModifierKey::Disabled || shortcut.altKey != ModifierKey::Disabled || shortcut.shiftKey != ModifierKey::Disabled)

View File

@ -30,6 +30,11 @@ DWORD KeyDropDownControl::GetSelectedValue(ComboBox comboBox)
return stoul(std::wstring(value));
}
DWORD KeyDropDownControl::GetSelectedValue(TextBlock text)
{
return keyboardManagerState->keyboardMap.GetKeyFromName(std::wstring{ text.Text() });
}
void KeyDropDownControl::SetSelectedValue(std::wstring value)
{
this->dropDown.as<ComboBox>().SelectedValue(winrt::box_value(value));
@ -87,7 +92,7 @@ void KeyDropDownControl::SetDefaultProperties(bool isShortcut, bool renderDisabl
auto child0 = Media::VisualTreeHelper::GetChild(combo, 0);
if (!child0)
return;
auto grid = child0.as<Grid>();
if (!grid)
return;
@ -95,7 +100,7 @@ void KeyDropDownControl::SetDefaultProperties(bool isShortcut, bool renderDisabl
auto& gridChildren = grid.Children();
if (!gridChildren)
return;
gridChildren.Append(warningTip);
});
@ -287,10 +292,8 @@ void KeyDropDownControl::SetSelectionHandler(StackPanel& table, StackPanel row,
}
else
{
Shortcut tempShortcut;
tempShortcut.SetKeyCodes(selectedKeyCodes);
// Assign instead of setting the value in the buffer since the previous value may not be a Shortcut
shortcutRemapBuffer[validationResult.second].first[colIndex] = tempShortcut;
shortcutRemapBuffer[validationResult.second].first[colIndex] = Shortcut(selectedKeyCodes);
}
}
@ -364,10 +367,31 @@ std::vector<int32_t> KeyDropDownControl::GetSelectedCodesFromStackPanel(Variable
std::vector<int32_t> selectedKeyCodes;
// Get selected indices for each drop down
for (int i = 0; i < (int)parent.Children().Size(); i++)
for (uint32_t i = 0; i < parent.Children().Size(); i++)
{
ComboBox ItDropDown = parent.Children().GetAt(i).as<ComboBox>();
selectedKeyCodes.push_back(GetSelectedValue(ItDropDown));
if (auto ItDropDown = parent.Children().GetAt(i).try_as<ComboBox>(); ItDropDown)
{
selectedKeyCodes.push_back(GetSelectedValue(ItDropDown));
}
// If it's a ShortcutControl -> use its layout, see KeyboardManagerState::AddKeyToLayout
else if (auto sp = parent.Children().GetAt(i).try_as<StackPanel>(); sp)
{
for (uint32_t j = 0; j < sp.Children().Size(); ++j)
{
auto border = sp.Children().GetAt(j).try_as<Border>();
// if this is null then this is a different layout
// likely because this not a shortcut to another shortcut but rather
// run app or open uri
if (border != nullptr)
{
auto textBlock = border.Child().try_as<TextBlock>();
selectedKeyCodes.push_back(GetSelectedValue(textBlock));
}
}
}
}
return selectedKeyCodes;
@ -432,27 +456,36 @@ void KeyDropDownControl::AddShortcutToControl(Shortcut shortcut, StackPanel tabl
// Remove references to the old drop down objects to destroy them
keyDropDownControlObjects.clear();
std::vector<DWORD> shortcutKeyCodes = shortcut.GetKeyCodes();
if (shortcutKeyCodes.size() != 0)
auto secondKey = shortcut.GetSecondKey();
bool ignoreWarning = false;
// If more than one key is to be added, ignore a shortcut to key warning on partially entering the remapping
if (shortcutKeyCodes.size() > 1)
{
bool ignoreWarning = false;
ignoreWarning = true;
}
// If more than one key is to be added, ignore a shortcut to key warning on partially entering the remapping
if (shortcutKeyCodes.size() > 1)
KeyDropDownControl::AddDropDown(table, row, parent, colIndex, remapBuffer, keyDropDownControlObjects, targetApp, isHybridControl, isSingleKeyWindow, ignoreWarning);
for (int i = 0; i < shortcutKeyCodes.size(); i++)
{
// New drop down gets added automatically when the SelectedValue(key code) is set
if (i < (int)parent.Children().Size())
{
ignoreWarning = true;
ComboBox currentDropDown = parent.Children().GetAt(i).as<ComboBox>();
currentDropDown.SelectedValue(winrt::box_value(std::to_wstring(shortcutKeyCodes[i])));
}
}
if (shortcut.HasChord())
{
// if this has a chord, render it last
KeyDropDownControl::AddDropDown(table, row, parent, colIndex, remapBuffer, keyDropDownControlObjects, targetApp, isHybridControl, isSingleKeyWindow, ignoreWarning);
for (int i = 0; i < shortcutKeyCodes.size(); i++)
{
// New drop down gets added automatically when the SelectedValue(key code) is set
if (i < (int)parent.Children().Size())
{
ComboBox currentDropDown = parent.Children().GetAt(i).as<ComboBox>();
currentDropDown.SelectedValue(winrt::box_value(std::to_wstring(shortcutKeyCodes[i])));
}
}
auto nextI = static_cast<int>(shortcutKeyCodes.size());
ComboBox currentDropDown = parent.Children().GetAt(nextI).as<ComboBox>();
currentDropDown.SelectedValue(winrt::box_value(std::to_wstring(shortcut.GetSecondKey())));
}
}

View File

@ -63,6 +63,7 @@ private:
// Get selected value of dropdown or -1 if nothing is selected
static DWORD GetSelectedValue(ComboBox comboBox);
static DWORD GetSelectedValue(TextBlock text);
// Function to set accessible name for combobox
static void SetAccessibleNameForComboBox(ComboBox dropDown, int index);
@ -110,7 +111,7 @@ public:
static void AddShortcutToControl(Shortcut shortcut, StackPanel table, VariableSizedWrapGrid parent, KBMEditor::KeyboardManagerState& keyboardManagerState, const int colIndex, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, RemapBuffer& remapBuffer, StackPanel row, TextBox targetApp, bool isHybridControl, bool isSingleKeyWindow);
// Get keys name list depending if Disable is in dropdown
static std::vector<std::pair<DWORD,std::wstring>> GetKeyList(bool isShortcut, bool renderDisable);
static std::vector<std::pair<DWORD, std::wstring>> GetKeyList(bool isShortcut, bool renderDisable);
// Get number of selected keys. Do not count -1 and 0 values as they stand for Not selected and None
static int GetNumberOfSelectedKeys(std::vector<int32_t> keys);

View File

@ -22,6 +22,16 @@ namespace KeyboardManagerEditorStrings
return GET_RESOURCE_STRING(IDS_MAPPING_TYPE_DROPDOWN_KEY_SHORTCUT);
}
inline std::wstring MappingTypeRunProgram()
{
return GET_RESOURCE_STRING(IDS_MAPPING_TYPE_DROPDOWN_RUN_PROGRAM);
}
inline std::wstring MappingTypeOpenUri()
{
return GET_RESOURCE_STRING(IDS_MAPPING_TYPE_DROPDOWN_OPEN_URI);
}
// Function to return the error message
winrt::hstring GetErrorMessage(ShortcutErrorType errorType);
}

View File

@ -110,13 +110,13 @@ void KeyboardManagerState::ConfigureDetectSingleKeyRemapUI(const StackPanel& tex
currentSingleKeyUI = textBlock.as<winrt::Windows::Foundation::IInspectable>();
}
void KeyboardManagerState::AddKeyToLayout(const StackPanel& panel, const hstring& key)
TextBlock KeyboardManagerState::AddKeyToLayout(const StackPanel& panel, const hstring& key)
{
// Textblock to display the detected key
TextBlock remapKey;
Border border;
border.Padding({ 20, 10, 20, 10 });
border.Padding({ 10, 5, 10, 5 });
border.Margin({ 0, 0, 10, 0 });
// Based on settings-ui\Settings.UI\SettingsXAML\Controls\KeyVisual\KeyVisual.xaml
@ -127,12 +127,15 @@ void KeyboardManagerState::AddKeyToLayout(const StackPanel& panel, const hstring
remapKey.Foreground(Application::Current().Resources().Lookup(box_value(L"ButtonForeground")).as<Media::Brush>());
remapKey.FontWeight(Text::FontWeights::SemiBold());
remapKey.FontSize(20);
remapKey.FontSize(14);
border.HorizontalAlignment(HorizontalAlignment::Left);
border.Child(remapKey);
remapKey.Text(key);
panel.Children().Append(border);
return remapKey;
}
// Function to update the detect shortcut UI based on the entered keys
@ -167,17 +170,39 @@ void KeyboardManagerState::UpdateDetectShortcutUI()
currentShortcutUI2.as<StackPanel>().Visibility(Visibility::Collapsed);
}
auto lastStackPanel = currentShortcutUI2.as<StackPanel>();
for (int i = 0; i < shortcut.size(); i++)
{
if (i < 3)
{
AddKeyToLayout(currentShortcutUI1.as<StackPanel>(), shortcut[i]);
lastStackPanel = currentShortcutUI1.as<StackPanel>();
}
else
{
AddKeyToLayout(currentShortcutUI2.as<StackPanel>(), shortcut[i]);
lastStackPanel = currentShortcutUI2.as<StackPanel>();
}
}
if (!AllowChord)
{
detectedShortcut.secondKey = NULL;
}
// add a TextBlock, to show what shortcut in text, e.g.: "CTRL+j, k" OR "CTRL+j, CTRL+k".
if (detectedShortcut.HasChord())
{
TextBlock txtComma;
txtComma.Text(L",");
txtComma.FontSize(20);
txtComma.Padding({ 0, 0, 10, 0 });
txtComma.VerticalAlignment(VerticalAlignment::Bottom);
txtComma.TextAlignment(TextAlignment::Left);
lastStackPanel.Children().Append(txtComma);
AddKeyToLayout(lastStackPanel, EditorHelpers::GetKeyVector(Shortcut(detectedShortcutCopy.secondKey), keyboardMap)[0]);
}
try
{
// If a layout update has been triggered by other methods (e.g.: adapting to zoom level), this may throw an exception.
@ -228,6 +253,12 @@ Shortcut KeyboardManagerState::GetDetectedShortcut()
return currentShortcut;
}
void KeyboardManagerState::SetDetectedShortcut(Shortcut shortcut)
{
detectedShortcut = shortcut;
UpdateDetectShortcutUI();
}
// Function to return the currently detected remap key which is displayed on the UI
DWORD KeyboardManagerState::GetDetectedSingleRemapKey()
{
@ -246,9 +277,57 @@ void KeyboardManagerState::SelectDetectedRemapKey(DWORD key)
void KeyboardManagerState::SelectDetectedShortcut(DWORD key)
{
// Set the new key and store if a change occurred
std::unique_lock<std::mutex> lock(detectedShortcut_mutex);
bool updateUI = detectedShortcut.SetKey(key);
lock.unlock();
bool updateUI = false;
if (AllowChord)
{
// Code to determine if we're building/updating a chord.
auto currentFirstKey = detectedShortcut.GetActionKey();
auto currentSecondKey = detectedShortcut.GetSecondKey();
Shortcut tempShortcut = Shortcut(key);
bool isKeyActionTypeKey = (tempShortcut.actionKey != NULL);
if (isKeyActionTypeKey)
{
// we want a chord and already have the first key set
std::unique_lock<std::mutex> lock(detectedShortcut_mutex);
if (currentFirstKey == NULL)
{
Logger::trace(L"AllowChord AND no first");
updateUI = detectedShortcut.SetKey(key);
}
else if (currentSecondKey == NULL)
{
// we don't have the second key, set it now
Logger::trace(L"AllowChord AND we have first key of {}, will use {}", currentFirstKey, key);
updateUI = detectedShortcut.SetSecondKey(key);
}
else
{
// we already have the second key, swap it to first, and use new as second
Logger::trace(L"DO have secondKey, will make first {} and second {}", currentSecondKey, key);
detectedShortcut.actionKey = currentSecondKey;
detectedShortcut.secondKey = key;
updateUI = true;
}
updateUI = true;
lock.unlock();
}
else
{
std::unique_lock<std::mutex> lock(detectedShortcut_mutex);
updateUI = detectedShortcut.SetKey(key);
lock.unlock();
}
}
else
{
std::unique_lock<std::mutex> lock(detectedShortcut_mutex);
updateUI = detectedShortcut.SetKey(key);
lock.unlock();
}
if (updateUI)
{
@ -261,9 +340,12 @@ void KeyboardManagerState::SelectDetectedShortcut(DWORD key)
void KeyboardManagerState::ResetDetectedShortcutKey(DWORD key)
{
std::lock_guard<std::mutex> lock(detectedShortcut_mutex);
detectedShortcut.ResetKey(key);
// only clear if mod, not if action, since we need to keek actionKey and secondKey for chord
if (Shortcut::IsModifier(key))
{
detectedShortcut.ResetKey(key);
}
}
// Function which can be used in HandleKeyboardHookEvent before the single key remap event to use the UI and suppress events while the remap window is active.
Helpers::KeyboardHookDecision KeyboardManagerState::DetectSingleRemapKeyUIBackend(LowlevelKeyboardEvent* data)
{
@ -372,6 +454,12 @@ void KeyboardManagerState::ClearRegisteredKeyDelays()
keyDelays.clear();
}
void KBMEditor::KeyboardManagerState::ClearStoredShortcut()
{
std::scoped_lock<std::mutex> detectedShortcut_lock(detectedShortcut_mutex);
detectedShortcut.Reset();
}
bool KeyboardManagerState::HandleKeyDelayEvent(LowlevelKeyboardEvent* ev)
{
if (currentUIWindow != GetForegroundWindow())

View File

@ -22,6 +22,7 @@ namespace Helpers
namespace winrt::Windows::UI::Xaml::Controls
{
struct StackPanel;
struct TextBlock;
}
namespace KBMEditor
@ -80,10 +81,15 @@ namespace KBMEditor
std::map<DWORD, std::unique_ptr<KeyDelay>> keyDelays;
std::mutex keyDelays_mutex;
// Display a key by appending a border Control as a child of the panel.
void AddKeyToLayout(const winrt::Windows::UI::Xaml::Controls::StackPanel& panel, const winrt::hstring& key);
public:
// Display a key by appending a border Control as a child of the panel.
winrt::Windows::UI::Xaml::Controls::TextBlock AddKeyToLayout(const winrt::Windows::UI::Xaml::Controls::StackPanel& panel, const winrt::hstring& key);
// flag to set if we want to allow building a chord
bool AllowChord = false;
// Stores the keyboard layout
LayoutMap keyboardMap;
@ -120,6 +126,9 @@ namespace KBMEditor
// Function to return the currently detected shortcut which is displayed on the UI
Shortcut GetDetectedShortcut();
// Function to SetDetectedShortcut and also UpdateDetectShortcutUI
void KeyboardManagerState::SetDetectedShortcut(Shortcut shortcut);
// Function to return the currently detected remap key which is displayed on the UI
DWORD GetDetectedSingleRemapKey();
@ -146,6 +155,8 @@ namespace KBMEditor
// Function to clear all the registered key delays
void ClearRegisteredKeyDelays();
void ClearStoredShortcut();
// Handle a key event, for a delayed key.
bool HandleKeyDelayEvent(LowlevelKeyboardEvent* ev);

View File

@ -1,6 +1,8 @@
#include "pch.h"
#include "ShortcutControl.h"
#include <Windows.h>
#include <commdlg.h>
#include <ShlObj.h>
#include <common/interop/shared_constants.h>
#include "KeyboardManagerState.h"
@ -19,8 +21,9 @@ RemapBuffer ShortcutControl::shortcutRemapBuffer;
ShortcutControl::ShortcutControl(StackPanel table, StackPanel row, const int colIndex, TextBox targetApp)
{
shortcutDropDownVariableSizedWrapGrid = VariableSizedWrapGrid();
typeShortcut = Button();
btnPickShortcut = Button();
shortcutControlLayout = StackPanel();
const bool isHybridControl = colIndex == 1;
// TODO: Check if there is a VariableSizedWrapGrid equivalent.
@ -28,28 +31,44 @@ ShortcutControl::ShortcutControl(StackPanel table, StackPanel row, const int col
shortcutDropDownVariableSizedWrapGrid.as<VariableSizedWrapGrid>().Orientation(Windows::UI::Xaml::Controls::Orientation::Horizontal);
shortcutDropDownVariableSizedWrapGrid.as<VariableSizedWrapGrid>().MaximumRowsOrColumns(3);
typeShortcut.as<Button>().Content(winrt::box_value(GET_RESOURCE_STRING(IDS_TYPE_BUTTON)));
typeShortcut.as<Button>().Width(EditorConstants::ShortcutTableDropDownWidth);
typeShortcut.as<Button>().Click([&, table, row, colIndex, isHybridControl, targetApp](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
btnPickShortcut.as<Button>().Content(winrt::box_value(GET_RESOURCE_STRING(IDS_TYPE_BUTTON)));
btnPickShortcut.as<Button>().Width(EditorConstants::ShortcutTableDropDownWidth / 2);
btnPickShortcut.as<Button>().Click([&, table, row, colIndex, isHybridControl, targetApp](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
keyboardManagerState->SetUIState(KBMEditor::KeyboardManagerUIState::DetectShortcutWindowActivated, editShortcutsWindowHandle);
// Using the XamlRoot of the typeShortcut to get the root of the XAML host
CreateDetectShortcutWindow(sender, sender.as<Button>().XamlRoot(), *keyboardManagerState, colIndex, table, keyDropDownControlObjects, row, targetApp, isHybridControl, false, editShortcutsWindowHandle, shortcutRemapBuffer);
});
FontIcon fontIcon;
fontIcon.Glyph(L"\uE70F"); // Unicode for the accept icon
fontIcon.FontFamily(Media::FontFamily(L"Segoe MDL2 Assets")); // Set the font family to Segoe MDL2 Assets
// Set the FontIcon as the content of the button
btnPickShortcut.as<Button>().Content(fontIcon);
uint32_t rowIndex;
UIElementCollection children = table.Children();
bool indexFound = children.IndexOf(row, rowIndex);
auto nameX = L"btnPickShortcut_" + std::to_wstring(colIndex);
btnPickShortcut.as<Button>().Name(nameX);
// Set an accessible name for the type shortcut button
typeShortcut.as<Button>().SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_TYPE_BUTTON)));
btnPickShortcut.as<Button>().SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_TYPE_BUTTON)));
shortcutControlLayout.as<StackPanel>().Spacing(EditorConstants::ShortcutTableDropDownSpacing);
keyComboAndSelectStackPanel = StackPanel();
keyComboAndSelectStackPanel.as<StackPanel>().Orientation(Windows::UI::Xaml::Controls::Orientation::Horizontal);
keyComboAndSelectStackPanel.as<StackPanel>().Spacing(EditorConstants::ShortcutTableDropDownSpacing);
keyComboStackPanel = StackPanel();
keyComboStackPanel.as<StackPanel>().Orientation(Windows::UI::Xaml::Controls::Orientation::Horizontal);
keyComboStackPanel.as<StackPanel>().Spacing(EditorConstants::ShortcutTableDropDownSpacing);
keyComboAndSelectStackPanel.as<StackPanel>().Children().Append(typeShortcut.as<Button>());
shortcutControlLayout.as<StackPanel>().Children().InsertAt(0, keyComboAndSelectStackPanel.as<StackPanel>());
shortcutControlLayout.as<StackPanel>().Children().Append(keyComboStackPanel.as<StackPanel>());
spBtnPickShortcut = UIHelpers::GetLabelWrapped(btnPickShortcut.as<Button>(), GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_LABEL_SHORTCUT), 80).as<StackPanel>();
shortcutControlLayout.as<StackPanel>().Children().Append(spBtnPickShortcut);
shortcutControlLayout.as<StackPanel>().Children().Append(shortcutDropDownVariableSizedWrapGrid.as<VariableSizedWrapGrid>());
KeyDropDownControl::AddDropDown(table, row, shortcutDropDownVariableSizedWrapGrid.as<VariableSizedWrapGrid>(), colIndex, shortcutRemapBuffer, keyDropDownControlObjects, targetApp, isHybridControl, false);
try
{
// If a layout update has been triggered by other methods (e.g.: adapting to zoom level), this may throw an exception.
@ -60,6 +79,13 @@ ShortcutControl::ShortcutControl(StackPanel table, StackPanel row, const int col
}
}
void ShortcutControl::OpenNewShortcutControlRow(StackPanel table, StackPanel row)
{
keyboardManagerState->SetUIState(KBMEditor::KeyboardManagerUIState::DetectShortcutWindowActivated, editShortcutsWindowHandle);
// Using the XamlRoot of the typeShortcut to get the root of the XAML host
CreateDetectShortcutWindow(btnPickShortcut, btnPickShortcut.XamlRoot(), *keyboardManagerState, 0, table, keyDropDownControlObjects, row, nullptr, false, false, editShortcutsWindowHandle, shortcutRemapBuffer);
}
// Function to set the accessible name of the target App text box
void ShortcutControl::SetAccessibleNameForTextBox(TextBox targetAppTextBox, int rowIndex)
{
@ -82,18 +108,36 @@ void ShortcutControl::UpdateAccessibleNames(StackPanel sourceColumn, StackPanel
deleteButton.SetValue(Automation::AutomationProperties::NameProperty(), box_value(GET_RESOURCE_STRING(IDS_AUTOMATIONPROPERTIES_ROW) + std::to_wstring(rowIndex) + L", " + GET_RESOURCE_STRING(IDS_DELETE_REMAPPING_BUTTON)));
}
void ShortcutControl::DeleteShortcutControl(StackPanel& parent, std::vector<std::vector<std::unique_ptr<ShortcutControl>>>& keyboardRemapControlObjects, int rowIndex)
{
UIElementCollection children = parent.Children();
children.RemoveAt(rowIndex);
shortcutRemapBuffer.erase(shortcutRemapBuffer.begin() + rowIndex);
// delete the SingleKeyRemapControl objects so that they get destructed
keyboardRemapControlObjects.erase(keyboardRemapControlObjects.begin() + rowIndex);
}
// Function to add a new row to the shortcut table. If the originalKeys and newKeys args are provided, then the displayed shortcuts are set to those values.
void ShortcutControl::AddNewShortcutControlRow(StackPanel& parent, std::vector<std::vector<std::unique_ptr<ShortcutControl>>>& keyboardRemapControlObjects, const Shortcut& originalKeys, const KeyShortcutTextUnion& newKeys, const std::wstring& targetAppName)
ShortcutControl& ShortcutControl::AddNewShortcutControlRow(StackPanel& parent, std::vector<std::vector<std::unique_ptr<ShortcutControl>>>& keyboardRemapControlObjects, const Shortcut& originalKeys, const KeyShortcutTextUnion& newKeys, const std::wstring& targetAppName)
{
// Textbox for target application
TextBox targetAppTextBox;
int runProgramLabelWidth = 80;
// Create new ShortcutControl objects dynamically so that we does not get destructed
std::vector<std::unique_ptr<ShortcutControl>> newrow;
StackPanel row = StackPanel();
row.Name(L"row");
parent.Children().Append(row);
newrow.emplace_back(std::make_unique<ShortcutControl>(parent, row, 0, targetAppTextBox));
ShortcutControl& newShortcutToRemap = *(newrow.back());
newrow.emplace_back(std::make_unique<ShortcutControl>(parent, row, 1, targetAppTextBox));
keyboardRemapControlObjects.push_back(std::move(newrow));
row.Padding({ 10, 15, 10, 5 });
@ -105,7 +149,9 @@ void ShortcutControl::AddNewShortcutControlRow(StackPanel& parent, std::vector<s
// ShortcutControl for the original shortcut
auto origin = keyboardRemapControlObjects.back()[0]->GetShortcutControl();
origin.Width(EditorConstants::ShortcutOriginColumnWidth);
row.Children().Append(origin);
// Arrow icon
@ -122,26 +168,46 @@ void ShortcutControl::AddNewShortcutControlRow(StackPanel& parent, std::vector<s
auto target = keyboardRemapControlObjects.back()[1]->GetShortcutControl();
target.Width(EditorConstants::ShortcutTargetColumnWidth);
auto typeCombo = ComboBox();
typeCombo.Width(EditorConstants::RemapTableDropDownWidth);
typeCombo.Items().Append(winrt::box_value(KeyboardManagerEditorStrings::MappingTypeKeyShortcut()));
typeCombo.Items().Append(winrt::box_value(KeyboardManagerEditorStrings::MappingTypeText()));
uint32_t rowIndex = -1;
if (!parent.Children().IndexOf(row, rowIndex))
{
return newShortcutToRemap;
}
// add shortcut type choice
auto actionTypeCombo = ComboBox();
actionTypeCombo.Name(L"actionTypeCombo_" + std::to_wstring(rowIndex));
actionTypeCombo.Width(EditorConstants::RemapTableDropDownWidth);
actionTypeCombo.Items().Append(winrt::box_value(KeyboardManagerEditorStrings::MappingTypeKeyShortcut()));
actionTypeCombo.Items().Append(winrt::box_value(KeyboardManagerEditorStrings::MappingTypeText()));
actionTypeCombo.Items().Append(winrt::box_value(KeyboardManagerEditorStrings::MappingTypeRunProgram()));
actionTypeCombo.Items().Append(winrt::box_value(KeyboardManagerEditorStrings::MappingTypeOpenUri()));
auto controlStackPanel = keyboardRemapControlObjects.back()[1]->shortcutControlLayout.as<StackPanel>();
auto firstLineStackPanel = keyboardRemapControlObjects.back()[1]->keyComboAndSelectStackPanel.as<StackPanel>();
firstLineStackPanel.Children().InsertAt(0, typeCombo);
auto firstLineStackPanel = keyboardRemapControlObjects.back()[1]->keyComboStackPanel.as<StackPanel>();
firstLineStackPanel.Children().InsertAt(0, UIHelpers::GetLabelWrapped(actionTypeCombo, GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_LABEL_ACTION), runProgramLabelWidth).as<StackPanel>());
// add textbox for when it's a text input
auto unicodeTextKeysInput = TextBox();
unicodeTextKeysInput.Name(L"unicodeTextKeysInput_" + std::to_wstring(rowIndex));
auto textInput = TextBox();
auto textInputMargin = Windows::UI::Xaml::Thickness();
textInputMargin.Top = -EditorConstants::ShortcutTableDropDownSpacing;
textInputMargin.Bottom = EditorConstants::ShortcutTableDropDownSpacing; // compensate for a collapsed UIElement
textInput.Margin(textInputMargin);
unicodeTextKeysInput.Margin(textInputMargin);
textInput.AcceptsReturn(false);
textInput.Visibility(Visibility::Collapsed);
textInput.Width(EditorConstants::TableDropDownHeight);
controlStackPanel.Children().Append(textInput);
textInput.HorizontalAlignment(HorizontalAlignment::Left);
textInput.TextChanged([parent, row](winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::Controls::TextChangedEventArgs const& e) mutable {
unicodeTextKeysInput.AcceptsReturn(false);
//unicodeTextKeysInput.Visibility(Visibility::Collapsed);
unicodeTextKeysInput.Width(EditorConstants::TableDropDownHeight);
StackPanel spUnicodeTextKeysInput = UIHelpers::GetLabelWrapped(unicodeTextKeysInput, GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_LABEL_KEYS), runProgramLabelWidth).as<StackPanel>();
controlStackPanel.Children().Append(spUnicodeTextKeysInput);
unicodeTextKeysInput.HorizontalAlignment(HorizontalAlignment::Left);
unicodeTextKeysInput.TextChanged([parent, row](winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::Controls::TextChangedEventArgs const& e) mutable {
auto textbox = sender.as<TextBox>();
auto text = textbox.Text();
uint32_t rowIndex = -1;
@ -154,29 +220,93 @@ void ShortcutControl::AddNewShortcutControlRow(StackPanel& parent, std::vector<s
shortcutRemapBuffer[rowIndex].first[1] = text.c_str();
});
auto grid = keyboardRemapControlObjects.back()[1]->shortcutDropDownVariableSizedWrapGrid.as<VariableSizedWrapGrid>();
const bool textSelected = newKeys.index() == 2;
bool isRunProgram = false;
bool isOpenUri = false;
Shortcut shortCut;
if (!textSelected && newKeys.index() == 1)
{
shortCut = std::get<Shortcut>(newKeys);
isRunProgram = (shortCut.operationType == Shortcut::OperationType::RunProgram);
isOpenUri = (shortCut.operationType == Shortcut::OperationType::OpenURI);
}
// add TextBoxes for when it's a runProgram fields
auto runProgramStackPanel = SetupRunProgramControls(parent, row, shortCut, textInputMargin, controlStackPanel);
runProgramStackPanel.Margin({ 0, -30, 0, 0 });
auto openURIStackPanel = SetupOpenURIControls(parent, row, shortCut, textInputMargin, controlStackPanel);
// add grid for when it's a key/shortcut
auto shortcutItemsGrid = keyboardRemapControlObjects.back()[1]->shortcutDropDownVariableSizedWrapGrid.as<VariableSizedWrapGrid>();
auto gridMargin = Windows::UI::Xaml::Thickness();
gridMargin.Bottom = -EditorConstants::ShortcutTableDropDownSpacing; // compensate for a collapsed textInput
grid.Margin(gridMargin);
auto button = keyboardRemapControlObjects.back()[1]->typeShortcut.as<Button>();
shortcutItemsGrid.Margin(gridMargin);
auto shortcutButton = keyboardRemapControlObjects.back()[1]->btnPickShortcut.as<Button>();
auto spBtnPickShortcut = keyboardRemapControlObjects.back()[1]->spBtnPickShortcut.as<StackPanel>();
typeCombo.SelectionChanged([typeCombo, grid, button, textInput](winrt::Windows::Foundation::IInspectable const&, SelectionChangedEventArgs const&) {
const bool textSelected = typeCombo.SelectedIndex() == 1;
// event code for when type changes
actionTypeCombo.SelectionChanged([parent, row, controlStackPanel, actionTypeCombo, shortcutItemsGrid, spBtnPickShortcut, spUnicodeTextKeysInput, runProgramStackPanel, openURIStackPanel](winrt::Windows::Foundation::IInspectable const&, SelectionChangedEventArgs const&) {
const auto shortcutType = ShortcutControl::GetShortcutType(actionTypeCombo);
const auto shortcutInputVisibility = textSelected ? Visibility::Collapsed : Visibility::Visible;
grid.Visibility(shortcutInputVisibility);
button.Visibility(shortcutInputVisibility);
const auto textInputVisibility = textSelected ? Visibility::Visible : Visibility::Collapsed;
textInput.Visibility(textInputVisibility);
if (shortcutType == ShortcutControl::ShortcutType::Shortcut)
{
spBtnPickShortcut.Visibility(Visibility::Visible);
shortcutItemsGrid.Visibility(Visibility::Visible);
spUnicodeTextKeysInput.Visibility(Visibility::Collapsed);
runProgramStackPanel.Visibility(Visibility::Collapsed);
openURIStackPanel.Visibility(Visibility::Collapsed);
}
else if (shortcutType == ShortcutControl::ShortcutType::Text)
{
spBtnPickShortcut.Visibility(Visibility::Collapsed);
shortcutItemsGrid.Visibility(Visibility::Collapsed);
spUnicodeTextKeysInput.Visibility(Visibility::Visible);
runProgramStackPanel.Visibility(Visibility::Collapsed);
openURIStackPanel.Visibility(Visibility::Collapsed);
}
else if (shortcutType == ShortcutControl::ShortcutType::RunProgram)
{
spBtnPickShortcut.Visibility(Visibility::Collapsed);
shortcutItemsGrid.Visibility(Visibility::Collapsed);
spUnicodeTextKeysInput.Visibility(Visibility::Collapsed);
runProgramStackPanel.Visibility(Visibility::Visible);
openURIStackPanel.Visibility(Visibility::Collapsed);
}
else
{
spBtnPickShortcut.Visibility(Visibility::Collapsed);
shortcutItemsGrid.Visibility(Visibility::Collapsed);
spUnicodeTextKeysInput.Visibility(Visibility::Collapsed);
runProgramStackPanel.Visibility(Visibility::Collapsed);
openURIStackPanel.Visibility(Visibility::Visible);
}
});
const bool textSelected = newKeys.index() == 2;
typeCombo.SelectedIndex(textSelected);
row.Children().Append(target);
if (textSelected)
{
actionTypeCombo.SelectedIndex(1);
}
else
{
if (shortCut.operationType == Shortcut::OperationType::RunProgram)
{
actionTypeCombo.SelectedIndex(2);
}
else if (shortCut.operationType == Shortcut::OperationType::OpenURI)
{
actionTypeCombo.SelectedIndex(3);
}
else
{
actionTypeCombo.SelectedIndex(0);
}
}
targetAppTextBox.Width(EditorConstants::ShortcutTableDropDownWidth);
targetAppTextBox.PlaceholderText(KeyboardManagerEditorStrings::DefaultAppName());
targetAppTextBox.Text(targetAppName);
@ -189,7 +319,7 @@ void ShortcutControl::AddNewShortcutControlRow(StackPanel& parent, std::vector<s
});
// LostFocus handler will be called whenever text is updated by a user and then they click something else or tab to another control. Does not get called if Text is updated while the TextBox isn't in focus (i.e. from code)
targetAppTextBox.LostFocus([&keyboardRemapControlObjects, parent, row, targetAppTextBox, typeCombo, textInput](auto const& sender, auto const& e) {
targetAppTextBox.LostFocus([&keyboardRemapControlObjects, parent, row, targetAppTextBox, actionTypeCombo, unicodeTextKeysInput](auto const& sender, auto const& e) {
// Get index of targetAppTextBox button
uint32_t rowIndex;
if (!parent.Children().IndexOf(row, rowIndex))
@ -211,28 +341,60 @@ void ShortcutControl::AddNewShortcutControlRow(StackPanel& parent, std::vector<s
std::get<Shortcut>(shortcutRemapBuffer[rowIndex].first[0]).SetKeyCodes(KeyDropDownControl::GetSelectedCodesFromStackPanel(keyboardRemapControlObjects[rowIndex][0]->shortcutDropDownVariableSizedWrapGrid.as<VariableSizedWrapGrid>()));
// second column is a hybrid column
const bool textSelected = typeCombo.SelectedIndex() == 1;
const bool regularShortcut = actionTypeCombo.SelectedIndex() == 0;
const bool textSelected = actionTypeCombo.SelectedIndex() == 1;
const bool runProgram = actionTypeCombo.SelectedIndex() == 2;
const bool openUri = actionTypeCombo.SelectedIndex() == 3;
if (textSelected)
{
shortcutRemapBuffer[rowIndex].first[1] = textInput.Text().c_str();
shortcutRemapBuffer[rowIndex].first[1] = unicodeTextKeysInput.Text().c_str();
}
else
{
std::vector<int32_t> selectedKeyCodes = KeyDropDownControl::GetSelectedCodesFromStackPanel(keyboardRemapControlObjects[rowIndex][1]->shortcutDropDownVariableSizedWrapGrid.as<VariableSizedWrapGrid>());
if (regularShortcut)
{
std::vector<int32_t> selectedKeyCodes = KeyDropDownControl::GetSelectedCodesFromStackPanel(keyboardRemapControlObjects[rowIndex][1]->shortcutDropDownVariableSizedWrapGrid.as<VariableSizedWrapGrid>());
// If exactly one key is selected consider it to be a key remap
if (selectedKeyCodes.size() == 1)
{
shortcutRemapBuffer[rowIndex].first[1] = (DWORD)selectedKeyCodes[0];
// If exactly one key is selected consider it to be a key remap
if (selectedKeyCodes.size() == 1)
{
shortcutRemapBuffer[rowIndex].first[1] = (DWORD)selectedKeyCodes[0];
}
else
{
Shortcut tempShortcut;
tempShortcut.SetKeyCodes(selectedKeyCodes);
// Assign instead of setting the value in the buffer since the previous value may not be a Shortcut
shortcutRemapBuffer[rowIndex].first[1] = tempShortcut;
}
}
else
else if (runProgram)
{
auto runProgramPathInput = row.FindName(L"runProgramPathInput_" + std::to_wstring(rowIndex)).as<TextBox>();
auto runProgramArgsForProgramInput = row.FindName(L"runProgramArgsForProgramInput_" + std::to_wstring(rowIndex)).as<TextBox>();
auto runProgramStartInDirInput = row.FindName(L"runProgramStartInDirInput_" + std::to_wstring(rowIndex)).as<TextBox>();
auto runProgramElevationTypeCombo = row.FindName(L"runProgramElevationTypeCombo_" + std::to_wstring(rowIndex)).as<ComboBox>();
auto runProgramAlreadyRunningAction = row.FindName(L"runProgramAlreadyRunningAction_" + std::to_wstring(rowIndex)).as<ComboBox>();
Shortcut tempShortcut;
tempShortcut.SetKeyCodes(selectedKeyCodes);
tempShortcut.operationType = Shortcut::OperationType::RunProgram;
tempShortcut.runProgramFilePath = ShortcutControl::RemoveExtraQuotes(runProgramPathInput.Text().c_str());
tempShortcut.runProgramArgs = (runProgramArgsForProgramInput.Text().c_str());
tempShortcut.runProgramStartInDir = (runProgramStartInDirInput.Text().c_str());
tempShortcut.elevationLevel = static_cast<Shortcut::ElevationLevel>(runProgramElevationTypeCombo.SelectedIndex());
tempShortcut.alreadyRunningAction = static_cast<Shortcut::ProgramAlreadyRunningAction>(runProgramAlreadyRunningAction.SelectedIndex());
// Assign instead of setting the value in the buffer since the previous value may not be a Shortcut
shortcutRemapBuffer[rowIndex].first[1] = tempShortcut;
}
else if (openUri)
{
}
}
std::wstring newText = targetAppTextBox.Text().c_str();
std::wstring lowercaseDefAppName = KeyboardManagerEditorStrings::DefaultAppName();
std::transform(newText.begin(), newText.end(), newText.begin(), towlower);
@ -251,16 +413,19 @@ void ShortcutControl::AddNewShortcutControlRow(StackPanel& parent, std::vector<s
});
// We need two containers in order to align it horizontally and vertically
StackPanel targetAppHorizontal = UIHelpers::GetWrapped(targetAppTextBox, EditorConstants::TableTargetAppColWidth).as<StackPanel>();
targetAppHorizontal.Orientation(Orientation::Horizontal);
targetAppHorizontal.HorizontalAlignment(HorizontalAlignment::Left);
StackPanel targetAppContainer = UIHelpers::GetWrapped(targetAppHorizontal, EditorConstants::TableTargetAppColWidth).as<StackPanel>();
targetAppContainer.Orientation(Orientation::Vertical);
targetAppContainer.VerticalAlignment(VerticalAlignment::Center);
row.Children().Append(targetAppContainer);
// Delete row button
Windows::UI::Xaml::Controls::Button deleteShortcut;
deleteShortcut.Content(SymbolIcon(Symbol::Delete));
deleteShortcut.Background(Media::SolidColorBrush(Colors::Transparent()));
deleteShortcut.HorizontalAlignment(HorizontalAlignment::Center);
@ -312,9 +477,11 @@ void ShortcutControl::AddNewShortcutControlRow(StackPanel& parent, std::vector<s
ToolTipService::SetToolTip(deleteShortcut, deleteShortcuttoolTip);
StackPanel deleteShortcutContainer = StackPanel();
deleteShortcutContainer.Name(L"deleteShortcutContainer");
deleteShortcutContainer.Children().Append(deleteShortcut);
deleteShortcutContainer.Orientation(Orientation::Vertical);
deleteShortcutContainer.VerticalAlignment(VerticalAlignment::Center);
row.Children().Append(deleteShortcutContainer);
// Set accessible names
@ -324,12 +491,25 @@ void ShortcutControl::AddNewShortcutControlRow(StackPanel& parent, std::vector<s
if (EditorHelpers::IsValidShortcut(originalKeys) && !(newKeys.index() == 0 && std::get<DWORD>(newKeys) == NULL) && !(newKeys.index() == 1 && !EditorHelpers::IsValidShortcut(std::get<Shortcut>(newKeys))))
{
// change to load app name
shortcutRemapBuffer.push_back(std::make_pair<RemapBufferItem, std::wstring>(RemapBufferItem{ Shortcut(), Shortcut() }, std::wstring(targetAppName)));
if (isRunProgram || isOpenUri)
{
// not sure why by we need to add the current item in here, so we have it even if does not change.
auto newShortcut = std::get<Shortcut>(newKeys);
shortcutRemapBuffer.push_back(std::make_pair<RemapBufferItem, std::wstring>(RemapBufferItem{ Shortcut(), newShortcut }, std::wstring(targetAppName)));
}
else
{
shortcutRemapBuffer.push_back(std::make_pair<RemapBufferItem, std::wstring>(RemapBufferItem{ Shortcut(), Shortcut() }, std::wstring(targetAppName)));
}
KeyDropDownControl::AddShortcutToControl(originalKeys, parent, keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][0]->shortcutDropDownVariableSizedWrapGrid.as<VariableSizedWrapGrid>(), *keyboardManagerState, 0, keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][0]->keyDropDownControlObjects, shortcutRemapBuffer, row, targetAppTextBox, false, false);
if (newKeys.index() == 0)
{
keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][1]->keyDropDownControlObjects[0]->SetSelectedValue(std::to_wstring(std::get<DWORD>(newKeys)));
auto shortcut = new Shortcut;
shortcut->SetKey(std::get<DWORD>(newKeys));
KeyDropDownControl::AddShortcutToControl(*shortcut, parent, keyboardRemapControlObjects.back()[1]->shortcutDropDownVariableSizedWrapGrid.as<VariableSizedWrapGrid>(), *keyboardManagerState, 1, keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][1]->keyDropDownControlObjects, shortcutRemapBuffer, row, targetAppTextBox, true, false);
}
else if (newKeys.index() == 1)
{
@ -339,16 +519,404 @@ void ShortcutControl::AddNewShortcutControlRow(StackPanel& parent, std::vector<s
{
shortcutRemapBuffer.back().first[1] = std::get<std::wstring>(newKeys);
const auto& remapControl = keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][1];
const auto& controlChildren = remapControl->GetShortcutControl().Children();
const auto& topLineChildren = controlChildren.GetAt(0).as<StackPanel>();
topLineChildren.Children().GetAt(0).as<ComboBox>().SelectedIndex(1);
controlChildren.GetAt(2).as<TextBox>().Text(std::get<std::wstring>(newKeys));
actionTypeCombo.SelectedIndex(1);
unicodeTextKeysInput.Text(std::get<std::wstring>(newKeys));
}
}
else
{
// Initialize both shortcuts as empty shortcuts
shortcutRemapBuffer.push_back(std::make_pair<RemapBufferItem, std::wstring>(RemapBufferItem{ Shortcut(), Shortcut() }, std::wstring(targetAppName)));
KeyDropDownControl::AddShortcutToControl(originalKeys, parent, keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][0]->shortcutDropDownVariableSizedWrapGrid.as<VariableSizedWrapGrid>(), *keyboardManagerState, 0, keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][0]->keyDropDownControlObjects, shortcutRemapBuffer, row, targetAppTextBox, false, false);
KeyDropDownControl::AddShortcutToControl(std::get<Shortcut>(newKeys), parent, keyboardRemapControlObjects.back()[1]->shortcutDropDownVariableSizedWrapGrid.as<VariableSizedWrapGrid>(), *keyboardManagerState, 1, keyboardRemapControlObjects[keyboardRemapControlObjects.size() - 1][1]->keyDropDownControlObjects, shortcutRemapBuffer, row, targetAppTextBox, true, false);
}
return newShortcutToRemap;
}
StackPanel SetupOpenURIControls(StackPanel& parent, StackPanel& row, Shortcut& shortCut, winrt::Windows::UI::Xaml::Thickness& textInputMargin, ::StackPanel& _controlStackPanel)
{
StackPanel openUriStackPanel;
auto uriTextBox = TextBox();
int runProgramLabelWidth = 80;
uriTextBox.Text(shortCut.uriToOpen);
uriTextBox.PlaceholderText(GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_URI_EXAMPLE));
uriTextBox.Margin(textInputMargin);
uriTextBox.Width(EditorConstants::TableDropDownHeight);
uriTextBox.HorizontalAlignment(HorizontalAlignment::Left);
winrt::Windows::UI::Xaml::Controls::HyperlinkButton hyperlinkButton;
hyperlinkButton.NavigateUri(Windows::Foundation::Uri(L"https://learn.microsoft.com/windows/uwp/launch-resume/launch-app-with-uri"));
hyperlinkButton.Content(winrt::box_value(GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_WHAT_CAN_I_USE_LINK)));
hyperlinkButton.Margin(textInputMargin);
StackPanel boxAndLink;
boxAndLink.Orientation(Orientation::Horizontal);
boxAndLink.Children().Append(uriTextBox);
boxAndLink.Children().Append(hyperlinkButton);
openUriStackPanel.Children().Append(UIHelpers::GetLabelWrapped(boxAndLink, GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_LABEL_PATH_URI), runProgramLabelWidth).as<StackPanel>());
uriTextBox.TextChanged([parent, row, uriTextBox](winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::Controls::TextChangedEventArgs const& e) mutable {
uint32_t rowIndex = -1;
if (!parent.Children().IndexOf(row, rowIndex))
{
return;
}
Shortcut tempShortcut;
tempShortcut.operationType = Shortcut::OperationType::OpenURI;
tempShortcut.uriToOpen = ShortcutControl::RemoveExtraQuotes(uriTextBox.Text().c_str());
ShortcutControl::shortcutRemapBuffer[rowIndex].first[1] = tempShortcut;
});
_controlStackPanel.Children().Append(openUriStackPanel);
return openUriStackPanel;
}
StackPanel SetupRunProgramControls(StackPanel& parent, StackPanel& row, Shortcut& shortCut, winrt::Windows::UI::Xaml::Thickness& textInputMargin, ::StackPanel& _controlStackPanel)
{
uint32_t rowIndex;
// Get index of delete button
UIElementCollection children = parent.Children();
children.IndexOf(row, rowIndex);
StackPanel controlStackPanel;
controlStackPanel.Name(L"RunProgramControls_" + std::to_wstring(rowIndex));
auto runProgramPathInput = TextBox();
runProgramPathInput.Name(L"runProgramPathInput_" + std::to_wstring(rowIndex));
auto runProgramArgsForProgramInput = TextBox();
runProgramArgsForProgramInput.Name(L"runProgramArgsForProgramInput_" + std::to_wstring(rowIndex));
auto runProgramStartInDirInput = TextBox();
runProgramStartInDirInput.Name(L"runProgramStartInDirInput_" + std::to_wstring(rowIndex));
Button pickFileBtn;
Button pickPathBtn;
auto runProgramElevationTypeCombo = ComboBox();
runProgramElevationTypeCombo.Name(L"runProgramElevationTypeCombo_" + std::to_wstring(rowIndex));
auto runProgramAlreadyRunningAction = ComboBox();
runProgramAlreadyRunningAction.Name(L"runProgramAlreadyRunningAction_" + std::to_wstring(rowIndex));
_controlStackPanel.Children().Append(controlStackPanel);
StackPanel stackPanelForRunProgramPath;
StackPanel stackPanelRunProgramStartInDir;
runProgramPathInput.PlaceholderText(GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_PATH_TO_PROGRAM));
runProgramPathInput.Margin(textInputMargin);
runProgramPathInput.AcceptsReturn(false);
runProgramPathInput.IsSpellCheckEnabled(false);
runProgramPathInput.Width(EditorConstants::TableDropDownHeight);
runProgramPathInput.HorizontalAlignment(HorizontalAlignment::Left);
runProgramArgsForProgramInput.PlaceholderText(GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_ARGS_FOR_PROGRAM));
runProgramArgsForProgramInput.Margin(textInputMargin);
runProgramArgsForProgramInput.AcceptsReturn(false);
runProgramArgsForProgramInput.IsSpellCheckEnabled(false);
runProgramArgsForProgramInput.Width(EditorConstants::TableDropDownHeight);
runProgramArgsForProgramInput.HorizontalAlignment(HorizontalAlignment::Left);
runProgramStartInDirInput.IsSpellCheckEnabled(false);
runProgramStartInDirInput.PlaceholderText(GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_START_IN_DIR_FOR_PROGRAM));
runProgramStartInDirInput.Margin(textInputMargin);
runProgramStartInDirInput.AcceptsReturn(false);
runProgramStartInDirInput.Width(EditorConstants::TableDropDownHeight);
runProgramStartInDirInput.HorizontalAlignment(HorizontalAlignment::Left);
stackPanelForRunProgramPath.Orientation(Orientation::Horizontal);
stackPanelRunProgramStartInDir.Orientation(Orientation::Horizontal);
pickFileBtn.Content(winrt::box_value(GET_RESOURCE_STRING(IDS_BROWSE_FOR_PROGRAM_BUTTON)));
pickPathBtn.Content(winrt::box_value(GET_RESOURCE_STRING(IDS_BROWSE_FOR_PATH_BUTTON)));
pickFileBtn.Margin(textInputMargin);
pickPathBtn.Margin(textInputMargin);
stackPanelForRunProgramPath.Children().Append(runProgramPathInput);
stackPanelForRunProgramPath.Children().Append(pickFileBtn);
stackPanelRunProgramStartInDir.Children().Append(runProgramStartInDirInput);
stackPanelRunProgramStartInDir.Children().Append(pickPathBtn);
int runProgramLabelWidth = 90;
controlStackPanel.Children().Append(UIHelpers::GetLabelWrapped(stackPanelForRunProgramPath, GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_LABEL_PROGRAM), runProgramLabelWidth).as<StackPanel>());
controlStackPanel.Children().Append(UIHelpers::GetLabelWrapped(runProgramArgsForProgramInput, GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_LABEL_ARGS), runProgramLabelWidth).as<StackPanel>());
controlStackPanel.Children().Append(UIHelpers::GetLabelWrapped(stackPanelRunProgramStartInDir, GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_LABEL_START_IN), runProgramLabelWidth).as<StackPanel>());
// add shortcut type choice
runProgramElevationTypeCombo.Width(EditorConstants::TableDropDownHeight);
runProgramElevationTypeCombo.Items().Append(winrt::box_value(GET_RESOURCE_STRING(IDS_ELEVATION_TYPE_NORMAL)));
runProgramElevationTypeCombo.Items().Append(winrt::box_value(GET_RESOURCE_STRING(IDS_ELEVATION_TYPE_ELEVATED)));
runProgramElevationTypeCombo.Items().Append(winrt::box_value(GET_RESOURCE_STRING(IDS_ELEVATION_TYPE_DIFFERENT_USER)));
runProgramElevationTypeCombo.SelectedIndex(0);
// runProgramAlreadyRunningAction
runProgramAlreadyRunningAction.Width(EditorConstants::TableDropDownHeight);
runProgramAlreadyRunningAction.Items().Append(winrt::box_value(GET_RESOURCE_STRING(IDS_ALREADY_RUNNING_SHOW_WINDOW)));
runProgramAlreadyRunningAction.Items().Append(winrt::box_value(GET_RESOURCE_STRING(IDS_ALREADY_RUNNING_START_ANOTHER)));
runProgramAlreadyRunningAction.Items().Append(winrt::box_value(GET_RESOURCE_STRING(IDS_ALREADY_RUNNING_DO_NOTHING)));
runProgramAlreadyRunningAction.Items().Append(winrt::box_value(GET_RESOURCE_STRING(IDS_ALREADY_RUNNING_CLOSE)));
runProgramAlreadyRunningAction.Items().Append(winrt::box_value(GET_RESOURCE_STRING(IDS_ALREADY_RUNNING_TERMINATE)));
runProgramAlreadyRunningAction.SelectedIndex(0);
controlStackPanel.Children().Append(UIHelpers::GetLabelWrapped(runProgramElevationTypeCombo, GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_LABEL_ELEVATION), runProgramLabelWidth).as<StackPanel>());
controlStackPanel.Children().Append(UIHelpers::GetLabelWrapped(runProgramAlreadyRunningAction, GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_LABEL_IF_RUNNING), runProgramLabelWidth).as<StackPanel>());
auto runProgramStartWindow = ComboBox();
runProgramStartWindow.Name(L"runProgramStartWindow_" + std::to_wstring(rowIndex));
runProgramStartWindow.Width(EditorConstants::TableDropDownHeight);
runProgramStartWindow.Items().Append(winrt::box_value(GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_VISIBILITY_NORMAL)));
runProgramStartWindow.Items().Append(winrt::box_value(GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_VISIBILITY_HIDDEN)));
runProgramStartWindow.SelectedIndex(0);
controlStackPanel.Children().Append(UIHelpers::GetLabelWrapped(runProgramStartWindow, GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_LABEL_START_AS), runProgramLabelWidth).as<StackPanel>());
// add events to TextBoxes for runProgram fields.
runProgramPathInput.TextChanged([parent, row](winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::Controls::TextChangedEventArgs const& e) mutable {
uint32_t rowIndex = -1;
if (!parent.Children().IndexOf(row, rowIndex))
{
return;
}
Shortcut tempShortcut;
CreateNewTempShortcut(row, tempShortcut, rowIndex);
ShortcutControl::shortcutRemapBuffer[rowIndex].first[1] = tempShortcut;
});
runProgramArgsForProgramInput.TextChanged([parent, row](winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::Controls::TextChangedEventArgs const& e) mutable {
uint32_t rowIndex = -1;
if (!parent.Children().IndexOf(row, rowIndex))
{
return;
}
Shortcut tempShortcut;
CreateNewTempShortcut(row, tempShortcut, rowIndex);
ShortcutControl::shortcutRemapBuffer[rowIndex].first[1] = tempShortcut;
});
runProgramStartInDirInput.TextChanged([parent, row](winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::Controls::TextChangedEventArgs const& e) mutable {
uint32_t rowIndex = -1;
if (!parent.Children().IndexOf(row, rowIndex))
{
return;
}
if (ShortcutControl::shortcutRemapBuffer.size() <= rowIndex)
{
return;
}
Shortcut tempShortcut;
CreateNewTempShortcut(row, tempShortcut, rowIndex);
ShortcutControl::shortcutRemapBuffer[rowIndex].first[1] = tempShortcut;
});
runProgramAlreadyRunningAction.SelectionChanged([parent, row](winrt::Windows::Foundation::IInspectable const&, SelectionChangedEventArgs const&) {
uint32_t rowIndex;
if (!parent.Children().IndexOf(row, rowIndex))
{
return;
}
if (ShortcutControl::shortcutRemapBuffer.size() <= rowIndex)
{
return;
}
Shortcut tempShortcut;
CreateNewTempShortcut(static_cast<StackPanel>(row), tempShortcut, rowIndex);
ShortcutControl::shortcutRemapBuffer[rowIndex].first[1] = tempShortcut;
});
runProgramElevationTypeCombo.SelectionChanged([parent, row](winrt::Windows::Foundation::IInspectable const&, SelectionChangedEventArgs const&) {
uint32_t rowIndex;
if (!parent.Children().IndexOf(row, rowIndex))
{
return;
}
if (ShortcutControl::shortcutRemapBuffer.size() <= rowIndex)
{
return;
}
Shortcut tempShortcut;
CreateNewTempShortcut(static_cast<StackPanel>(row), tempShortcut, rowIndex);
ShortcutControl::shortcutRemapBuffer[rowIndex].first[1] = tempShortcut;
});
runProgramStartWindow.SelectionChanged([parent, row](winrt::Windows::Foundation::IInspectable const&, SelectionChangedEventArgs const&) {
uint32_t rowIndex;
if (!parent.Children().IndexOf(row, rowIndex))
{
return;
}
if (ShortcutControl::shortcutRemapBuffer.size() <= rowIndex)
{
return;
}
Shortcut tempShortcut;
CreateNewTempShortcut(static_cast<StackPanel>(row), tempShortcut, rowIndex);
ShortcutControl::shortcutRemapBuffer[rowIndex].first[1] = tempShortcut;
});
pickFileBtn.Click([&, parent, row](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
Button currentButton = sender.as<Button>();
uint32_t rowIndex;
UIElementCollection children = parent.Children();
bool indexFound = children.IndexOf(row, rowIndex);
if (!indexFound)
{
return;
}
OPENFILENAME openFileName;
TCHAR szFile[260] = { 0 };
ZeroMemory(&openFileName, sizeof(openFileName));
openFileName.lStructSize = sizeof(openFileName);
openFileName.hwndOwner = NULL;
openFileName.lpstrFile = szFile;
openFileName.nMaxFile = sizeof(szFile);
openFileName.lpstrFilter = TEXT("All Files (*.*)\0*.*\0");
openFileName.nFilterIndex = 1;
openFileName.lpstrFileTitle = NULL;
openFileName.nMaxFileTitle = 0;
openFileName.lpstrInitialDir = NULL;
openFileName.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
auto runProgramPathInput = row.FindName(L"runProgramPathInput_" + std::to_wstring(rowIndex)).as<TextBox>();
if (GetOpenFileName(&openFileName) == TRUE)
{
runProgramPathInput.Text(szFile);
}
});
pickPathBtn.Click([&, parent, row](winrt::Windows::Foundation::IInspectable const& sender, RoutedEventArgs const&) {
Button currentButton = sender.as<Button>();
uint32_t rowIndex;
UIElementCollection children = parent.Children();
bool indexFound = children.IndexOf(row, rowIndex);
if (!indexFound)
{
return;
}
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
if (!FAILED(hr))
{
// Create a buffer to store the selected folder path
wchar_t path[MAX_PATH];
ZeroMemory(path, sizeof(path));
// Initialize the BROWSEINFO structure
BROWSEINFO browseInfo = { 0 };
browseInfo.hwndOwner = NULL; // Use NULL if there's no owner window
browseInfo.pidlRoot = NULL; // Use NULL to start from the desktop
browseInfo.pszDisplayName = path; // Buffer to store the display name
browseInfo.lpszTitle = L"Select a folder"; // Title of the dialog
browseInfo.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE; // Show only file system directories
// Show the dialog
LPITEMIDLIST pidl = SHBrowseForFolder(&browseInfo);
if (pidl != NULL)
{
// Get the selected folder's path
if (SHGetPathFromIDList(pidl, path))
{
auto runProgramStartInDirInput = row.FindName(L"runProgramStartInDirInput_" + std::to_wstring(rowIndex)).as<TextBox>();
runProgramStartInDirInput.Text(path);
}
// Free the PIDL
CoTaskMemFree(pidl);
}
// Release COM
CoUninitialize();
// Uninitialize COM
CoUninitialize();
}
});
// this really should not be here, it just works because SelectionChanged is always changed?
runProgramPathInput.Text(shortCut.runProgramFilePath);
runProgramArgsForProgramInput.Text(shortCut.runProgramArgs);
runProgramStartInDirInput.Text(shortCut.runProgramStartInDir);
runProgramElevationTypeCombo.SelectedIndex(shortCut.elevationLevel);
runProgramAlreadyRunningAction.SelectedIndex(shortCut.alreadyRunningAction);
runProgramStartWindow.SelectedIndex(shortCut.startWindowType);
return controlStackPanel;
}
void CreateNewTempShortcut(StackPanel& row, Shortcut& tempShortcut, const uint32_t& rowIndex)
{
tempShortcut.operationType = Shortcut::OperationType::RunProgram;
//tempShortcut.isRunProgram = true;
auto runProgramPathInput = row.FindName(L"runProgramPathInput_" + std::to_wstring(rowIndex)).as<TextBox>();
auto runProgramArgsForProgramInput = row.FindName(L"runProgramArgsForProgramInput_" + std::to_wstring(rowIndex)).as<TextBox>();
auto runProgramStartInDirInput = row.FindName(L"runProgramStartInDirInput_" + std::to_wstring(rowIndex)).as<TextBox>();
auto runProgramElevationTypeCombo = row.FindName(L"runProgramElevationTypeCombo_" + std::to_wstring(rowIndex)).as<ComboBox>();
auto runProgramAlreadyRunningAction = row.FindName(L"runProgramAlreadyRunningAction_" + std::to_wstring(rowIndex)).as<ComboBox>();
auto runProgramStartWindow = row.FindName(L"runProgramStartWindow_" + std::to_wstring(rowIndex)).as<ComboBox>();
tempShortcut.runProgramFilePath = ShortcutControl::RemoveExtraQuotes(runProgramPathInput.Text().c_str());
tempShortcut.runProgramArgs = (runProgramArgsForProgramInput.Text().c_str());
tempShortcut.runProgramStartInDir = (runProgramStartInDirInput.Text().c_str());
// Assign instead of setting the value in the buffer since the previous value may not be a Shortcut
tempShortcut.elevationLevel = static_cast<Shortcut::ElevationLevel>(runProgramElevationTypeCombo.SelectedIndex());
tempShortcut.alreadyRunningAction = static_cast<Shortcut::ProgramAlreadyRunningAction>(runProgramAlreadyRunningAction.SelectedIndex());
tempShortcut.startWindowType = static_cast<Shortcut::StartWindowType>(runProgramStartWindow.SelectedIndex());
}
std::wstring ShortcutControl::RemoveExtraQuotes(const std::wstring& str)
{
if (!str.empty() && str.front() == L'"' && str.back() == L'"')
{
return str.substr(1, str.size() - 2);
}
return str;
}
ShortcutControl::ShortcutType ShortcutControl::GetShortcutType(const winrt::Windows::UI::Xaml::Controls::ComboBox& typeCombo)
{
if (typeCombo.SelectedIndex() == 0)
{
return ShortcutControl::ShortcutType::Shortcut;
}
else if (typeCombo.SelectedIndex() == 1)
{
return ShortcutControl::ShortcutType::Text;
}
else if (typeCombo.SelectedIndex() == 2)
{
return ShortcutControl::ShortcutType::RunProgram;
}
else
{
return ShortcutControl::ShortcutType::OpenURI;
}
}
@ -361,14 +929,53 @@ StackPanel ShortcutControl::GetShortcutControl()
// Function to create the detect shortcut UI window
void ShortcutControl::CreateDetectShortcutWindow(winrt::Windows::Foundation::IInspectable const& sender, XamlRoot xamlRoot, KBMEditor::KeyboardManagerState& keyboardManagerState, const int colIndex, StackPanel table, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, StackPanel row, TextBox targetApp, bool isHybridControl, bool isSingleKeyWindow, HWND parentWindow, RemapBuffer& remapBuffer)
{
// check to see if this orig or map-to shortcut;
bool isOrigShortcut = (colIndex == 0);
uint32_t rowIndex;
UIElementCollection children = table.Children();
bool indexFound = children.IndexOf(row, rowIndex);
Shortcut shortcut;
if (shortcutRemapBuffer.size() > 0)
{
if (colIndex == 0)
{
shortcut = std::get<Shortcut>(shortcutRemapBuffer[rowIndex].first[0]);
}
else
{
if (shortcutRemapBuffer[rowIndex].first[1].index() != 1)
{
// not a shortcut, let's fix that.
Shortcut newShortcut;
shortcutRemapBuffer[rowIndex].first[1] = newShortcut;
}
shortcut = std::get<Shortcut>(shortcutRemapBuffer[rowIndex].first[1]);
}
if (!shortcut.IsEmpty() && shortcut.HasChord())
{
keyboardManagerState.AllowChord = true;
} else {
keyboardManagerState.AllowChord = false;
}
}
//remapBuffer[rowIndex].first.
// ContentDialog for detecting shortcuts. This is the parent UI element.
ContentDialog detectShortcutBox;
ToggleSwitch allowChordSwitch;
// ContentDialog requires manually setting the XamlRoot (https://learn.microsoft.com/uwp/api/windows.ui.xaml.controls.contentdialog#contentdialog-in-appwindow-or-xaml-islands)
detectShortcutBox.XamlRoot(xamlRoot);
detectShortcutBox.Title(box_value(GET_RESOURCE_STRING(IDS_TYPESHORTCUT_TITLE)));
// Get the parent linked stack panel for the "Type shortcut" button that was clicked
VariableSizedWrapGrid linkedShortcutVariableSizedWrapGrid = UIHelpers::GetSiblingElement(sender.as<FrameworkElement>().Parent()).as<VariableSizedWrapGrid>();
auto unregisterKeys = [&keyboardManagerState]() {
@ -424,6 +1031,13 @@ void ShortcutControl::CreateDetectShortcutWindow(winrt::Windows::Foundation::IIn
unregisterKeys();
};
auto onReleaseSpace = [&keyboardManagerState,
allowChordSwitch] {
keyboardManagerState.AllowChord = !keyboardManagerState.AllowChord;
allowChordSwitch.IsOn(keyboardManagerState.AllowChord);
};
auto onAccept = [onPressEnter,
onReleaseEnter] {
onPressEnter();
@ -481,6 +1095,22 @@ void ShortcutControl::CreateDetectShortcutWindow(winrt::Windows::Foundation::IIn
unregisterKeys();
};
if (isOrigShortcut)
{
// Hold space to allow chords. Chords are only available for origin shortcuts.
keyboardManagerState.RegisterKeyDelay(
VK_SPACE,
selectDetectedShortcutAndResetKeys,
[onReleaseSpace, detectShortcutBox](DWORD) {
detectShortcutBox.Dispatcher().RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
[onReleaseSpace] {
onReleaseSpace();
});
},
nullptr);
}
// Cancel button
detectShortcutBox.CloseButtonText(GET_RESOURCE_STRING(IDS_CANCEL_BUTTON));
detectShortcutBox.CloseButtonClick([onCancel](winrt::Windows::Foundation::IInspectable const& sender, ContentDialogButtonClickEventArgs const& args) {
@ -525,6 +1155,42 @@ void ShortcutControl::CreateDetectShortcutWindow(winrt::Windows::Foundation::IIn
keyStackPanel2.Visibility(Visibility::Collapsed);
stackPanel.Children().Append(keyStackPanel2);
// Detect Chord
Windows::UI::Xaml::Controls::StackPanel chordStackPanel;
if (isOrigShortcut)
{
constexpr double verticalMargin = 20.f;
TextBlock allowChordText;
allowChordText.Text(GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_ALLOW_CHORDS));
allowChordText.FontSize(12);
allowChordText.Margin({ 0, 12 + verticalMargin, 0, 0 });
chordStackPanel.VerticalAlignment(VerticalAlignment::Center);
allowChordText.TextAlignment(TextAlignment::Center);
chordStackPanel.Orientation(Orientation::Horizontal);
allowChordSwitch.OnContent(nullptr);
allowChordSwitch.OffContent(nullptr);
allowChordSwitch.Margin({ 12, verticalMargin, 0, 0 });
chordStackPanel.Children().Append(allowChordText);
chordStackPanel.Children().Append(allowChordSwitch);
stackPanel.Children().Append(chordStackPanel);
allowChordSwitch.IsOn(keyboardManagerState.AllowChord);
auto toggleHandler = [allowChordSwitch, &keyboardManagerState](auto const& sender, auto const& e) {
keyboardManagerState.AllowChord = allowChordSwitch.IsOn();
if (!allowChordSwitch.IsOn())
{
keyboardManagerState.ClearStoredShortcut();
}
};
allowChordSwitch.Toggled(toggleHandler);
}
TextBlock holdEscInfo;
holdEscInfo.Text(GET_RESOURCE_STRING(IDS_TYPE_HOLDESC));
holdEscInfo.FontSize(12);
@ -537,6 +1203,16 @@ void ShortcutControl::CreateDetectShortcutWindow(winrt::Windows::Foundation::IIn
holdEnterInfo.Margin({ 0, 0, 0, 0 });
stackPanel.Children().Append(holdEnterInfo);
if (isOrigShortcut)
{
// Hold space to allow chords. Chords are only available for origin shortcuts.
TextBlock holdSpaceInfo;
holdSpaceInfo.Text(GET_RESOURCE_STRING(IDS_TYPE_HOLDSPACE));
holdSpaceInfo.FontSize(12);
holdSpaceInfo.Margin({ 0, 0, 0, 0 });
stackPanel.Children().Append(holdSpaceInfo);
}
try
{
// If a layout update has been triggered by other methods (e.g.: adapting to zoom level), this may throw an exception.
@ -551,4 +1227,9 @@ void ShortcutControl::CreateDetectShortcutWindow(winrt::Windows::Foundation::IIn
// Show the dialog
detectShortcutBox.ShowAsync();
if (!shortcut.IsEmpty() && keyboardManagerState.AllowChord)
{
keyboardManagerState.SetDetectedShortcut(shortcut);
}
}

View File

@ -26,13 +26,16 @@ private:
winrt::Windows::Foundation::IInspectable shortcutDropDownVariableSizedWrapGrid;
// Button to type the shortcut
winrt::Windows::Foundation::IInspectable typeShortcut;
Button btnPickShortcut;
// StackPanel to hold the shortcut
StackPanel spBtnPickShortcut;
// StackPanel to parent the above controls
winrt::Windows::Foundation::IInspectable shortcutControlLayout;
// StackPanel to parent the first line of "To" Column
winrt::Windows::Foundation::IInspectable keyComboAndSelectStackPanel;
winrt::Windows::Foundation::IInspectable keyComboStackPanel;
// Function to set the accessible name of the target app text box
static void SetAccessibleNameForTextBox(TextBox targetAppTextBox, int rowIndex);
@ -40,6 +43,15 @@ private:
// Function to set the accessible names for all the controls in a row
static void UpdateAccessibleNames(StackPanel sourceColumn, StackPanel mappedToColumn, TextBox targetAppTextBox, Button deleteButton, int rowIndex);
// enum for the type of shortcut, to make it easier to switch on and read
enum class ShortcutType
{
Shortcut,
Text,
RunProgram,
OpenURI
};
public:
// Handle to the current Edit Shortcuts Window
static HWND editShortcutsWindowHandle;
@ -56,8 +68,20 @@ public:
// constructor
ShortcutControl(StackPanel table, StackPanel row, const int colIndex, TextBox targetApp);
// Function to that will CreateDetectShortcutWindow, created here to it can be done automatically when "new shortcut" is clicked.
void OpenNewShortcutControlRow(StackPanel table, StackPanel row);
// Function to add a new row to the shortcut table. If the originalKeys and newKeys args are provided, then the displayed shortcuts are set to those values.
static void AddNewShortcutControlRow(StackPanel& parent, std::vector<std::vector<std::unique_ptr<ShortcutControl>>>& keyboardRemapControlObjects, const Shortcut& originalKeys = Shortcut(), const KeyShortcutTextUnion& newKeys = Shortcut(), const std::wstring& targetAppName = L"");
static ShortcutControl& AddNewShortcutControlRow(StackPanel& parent, std::vector<std::vector<std::unique_ptr<ShortcutControl>>>& keyboardRemapControlObjects, const Shortcut& originalKeys = Shortcut(), const KeyShortcutTextUnion& newKeys = Shortcut(), const std::wstring& targetAppName = L"");
// Function to delete the shortcut control
static void ShortcutControl::DeleteShortcutControl(StackPanel& parent, std::vector<std::vector<std::unique_ptr<ShortcutControl>>>& keyboardRemapControlObjects, int index);
// Function to get the shortcut type
static ShortcutType GetShortcutType(const Controls::ComboBox& typeCombo);
// Function to remove extra quotes from the start and end of the string (used where we will add them as needed later)
static std::wstring ShortcutControl::RemoveExtraQuotes(const std::wstring& str);
// Function to return the stack panel element of the ShortcutControl. This is the externally visible UI element which can be used to add it to other layouts
StackPanel GetShortcutControl();
@ -65,3 +89,9 @@ public:
// Function to create the detect shortcut UI window
static void CreateDetectShortcutWindow(winrt::Windows::Foundation::IInspectable const& sender, XamlRoot xamlRoot, KBMEditor::KeyboardManagerState& keyboardManagerState, const int colIndex, StackPanel table, std::vector<std::unique_ptr<KeyDropDownControl>>& keyDropDownControlObjects, StackPanel controlLayout, TextBox targetApp, bool isHybridControl, bool isSingleKeyWindow, HWND parentWindow, RemapBuffer& remapBuffer);
};
StackPanel SetupRunProgramControls(StackPanel& parent, StackPanel& row, Shortcut& shortCut, winrt::Windows::UI::Xaml::Thickness& textInputMargin, ::StackPanel& _controlStackPanel);
void CreateNewTempShortcut(StackPanel& row, Shortcut& tempShortcut, const uint32_t& rowIndex);
StackPanel SetupOpenURIControls(StackPanel& parent, StackPanel& row, Shortcut& shortCut, winrt::Windows::UI::Xaml::Thickness& textInputMargin, ::StackPanel& _controlStackPanel);

View File

@ -3,6 +3,9 @@
#include <common/monitor_utils.h>
using namespace Windows::UI::Xaml::Media;
using namespace Windows::UI::Xaml::Automation::Peers;
namespace UIHelpers
{
// This method sets focus to the first Type button on the last row of the Grid
@ -19,9 +22,11 @@ namespace UIHelpers
// Get Type Button from the first line
Button typeButton = firstLineIntoColumn.Children().GetAt(1).as<Button>();
// Set programmatic focus on the button
typeButton.Focus(FocusState::Programmatic);
if (typeButton != nullptr)
{
// Set programmatic focus on the button
typeButton.Focus(FocusState::Programmatic);
}
}
RECT GetForegroundWindowDesktopRect()
@ -53,6 +58,34 @@ namespace UIHelpers
return parentElement.Children().GetAt(index + 1);
}
winrt::Windows::Foundation::IInspectable GetLabelWrapped(const winrt::Windows::Foundation::IInspectable& element, std::wstring label, double textWidth, HorizontalAlignment horizontalAlignment)
{
StackPanel sp = StackPanel();
try
{
sp.Name(L"Wrapped_" + element.as<FrameworkElement>().Name());
}
catch (...)
{
}
sp.Orientation(Orientation::Horizontal);
sp.HorizontalAlignment(horizontalAlignment);
TextBlock text;
text.FontWeight(Text::FontWeights::Bold());
text.Text(label);
if (textWidth >= 0)
{
text.Width(textWidth);
}
sp.Children().Append(text);
sp.Children().Append(element.as<FrameworkElement>());
return sp;
}
winrt::Windows::Foundation::IInspectable GetWrapped(const winrt::Windows::Foundation::IInspectable& element, double width)
{
StackPanel sp = StackPanel();

View File

@ -27,6 +27,9 @@ namespace UIHelpers
winrt::Windows::Foundation::IInspectable GetWrapped(const winrt::Windows::Foundation::IInspectable& element, double width);
// Function to return a StackPanel with an element and a TextBlock label.
winrt::Windows::Foundation::IInspectable GetLabelWrapped(const winrt::Windows::Foundation::IInspectable& element, std::wstring label, double width, HorizontalAlignment horizontalAlignment = HorizontalAlignment::Left);
// Function to return the list of key name in the order for the drop down based on the key codes
winrt::Windows::Foundation::Collections::IVector<winrt::Windows::Foundation::IInspectable> ToBoxValue(const std::vector<std::pair<DWORD, std::wstring>>& list);

View File

@ -1,12 +1,31 @@
#include "pch.h"
#include <shellapi.h>
#include "KeyboardEventHandlers.h"
#include <common/interop/shared_constants.h>
#include <common/utils/elevation.h>
#include <keyboardmanager/common/InputInterface.h>
#include <keyboardmanager/common/Helpers.h>
#include <keyboardmanager/KeyboardManagerEngineLibrary/trace.h>
#include <TlHelp32.h>
#include <thread>
#include <future>
#include <chrono>
#include <winrt/Windows.UI.Notifications.h>
#include <winrt/Windows.Data.Xml.Dom.h>
#include <windows.h>
#include <string>
#include <urlmon.h>
#include <mmsystem.h>
using namespace winrt;
using namespace Windows::UI::Notifications;
using namespace Windows::Data::Xml::Dom;
namespace
{
bool GeneratedByKBM(const LowlevelKeyboardEvent* data)
@ -203,6 +222,8 @@ namespace KeyboardEventHandlers
// Function to a handle a shortcut remap
intptr_t HandleShortcutRemapEvent(KeyboardManagerInput::InputInterface& ii, LowlevelKeyboardEvent* data, State& state, const std::optional<std::wstring>& activatedApp) noexcept
{
auto resetChordsResults = ResetChordsIfNeeded(data, state, activatedApp);
// Check if any shortcut is currently in the invoked state
bool isShortcutInvoked = state.CheckShortcutRemapInvoked(activatedApp);
@ -224,15 +245,53 @@ namespace KeyboardEventHandlers
const bool remapToKey = it->second.targetShortcut.index() == 0;
const bool remapToShortcut = it->second.targetShortcut.index() == 1;
const bool remapToText = it->second.targetShortcut.index() == 2;
const bool isRunProgram = (remapToShortcut && std::get<Shortcut>(it->second.targetShortcut).IsRunProgram());
const bool isOpenUri = (remapToShortcut && std::get<Shortcut>(it->second.targetShortcut).IsOpenURI());
const size_t src_size = it->first.Size();
const size_t dest_size = remapToShortcut ? std::get<Shortcut>(it->second.targetShortcut).Size() : 1;
bool isMatchOnChordEnd = false;
bool isMatchOnChordStart = false;
// If the shortcut has been pressed down
if (!it->second.isShortcutInvoked && it->first.CheckModifiersKeyboardState(ii))
{
if (data->lParam->vkCode == it->first.GetActionKey() && (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN))
// if not a mod key, check for chord stuff
if (!resetChordsResults.CurrentKeyIsModifierKey && (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN))
{
if (itShortcut.HasChord())
{
if (!resetChordsResults.AnyChordStarted && data->lParam->vkCode == itShortcut.GetActionKey() && !itShortcut.IsChordStarted() && itShortcut.HasChord())
{
// start new chord
// Logger::trace(L"ChordKeyboardHandler:new chord started for {}", data->lParam->vkCode);
isMatchOnChordStart = true;
ResetAllOtherStartedChords(state, activatedApp, data->lParam->vkCode);
itShortcut.SetChordStarted(true);
continue;
}
if (data->lParam->vkCode == itShortcut.GetSecondKey() && itShortcut.IsChordStarted() && itShortcut.HasChord())
{
Logger::trace(L"ChordKeyboardHandler:found chord match {}, {}", itShortcut.GetActionKey(), itShortcut.GetSecondKey());
isMatchOnChordEnd = true;
}
if (resetChordsResults.AnyChordStarted && !isMatchOnChordEnd)
{
// Logger::trace(L"ChordKeyboardHandler:waiting on second key of chord, checked {} for {}", itShortcut.GetSecondKey(), data->lParam->vkCode);
// this is a key and there is a mod, but it's not the second key of a chord.
// we can't do anything with this key, we're waiting.
continue;
}
}
}
if (isMatchOnChordEnd || (!resetChordsResults.AnyChordStarted && !itShortcut.HasChord() && (data->lParam->vkCode == it->first.GetActionKey() && (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN))))
{
ResetAllStartedChords(state, activatedApp);
resetChordsResults.AnyChordStarted = false;
// Check if any other keys have been pressed apart from the shortcut. If true, then check for the next shortcut. This is to be done only for shortcut to shortcut remaps
if (!it->first.IsKeyboardStateClearExceptShortcut(ii) && (remapToShortcut || (remapToKey && std::get<DWORD>(it->second.targetShortcut) == CommonSharedConstants::VK_DISABLED)))
{
@ -252,9 +311,75 @@ namespace KeyboardEventHandlers
it->second.winKeyInvoked = ModifierKey::Left;
}
if (remapToShortcut)
if (isRunProgram)
{
// Get the common keys between the two shortcuts
auto threadFunction = [it]() {
CreateOrShowProcessForShortcut(std::get<Shortcut>(it->second.targetShortcut));
};
std::thread myThread(threadFunction);
if (myThread.joinable())
{
myThread.detach();
}
Logger::trace(L"ChordKeyboardHandler:returning..");
return 1;
}
else if (isOpenUri)
{
auto shortcut = std::get<Shortcut>(it->second.targetShortcut);
auto uri = shortcut.uriToOpen;
auto newUri = uri;
if (!PathIsURL(uri.c_str()))
{
WCHAR url[1024];
DWORD bufferSize = 1024;
if (UrlCreateFromPathW(uri.c_str(), url, &bufferSize, 0) == S_OK)
{
newUri = url;
Logger::trace(L"ChordKeyboardHandler:ConvertPathToURI from {} to {}", uri, url);
}
else
{
// need access to text resources, maybe "convert-resx-to-rc.ps1" is not working to get
// text from KeyboardManagerEditor to here in KeyboardManagerEngineLibrary land?
toast(L"Error", L"Could not understand the Path or URI");
return 1;
}
}
auto threadFunction = [newUri]() {
HINSTANCE result = ShellExecute(NULL, L"open", newUri.c_str(), NULL, NULL, SW_SHOWNORMAL);
if (result == reinterpret_cast<HINSTANCE>(HINSTANCE_ERROR))
{
// need access to text resources, maybe "convert-resx-to-rc.ps1" is not working to get
// text from KeyboardManagerEditor to here in KeyboardManagerEngineLibrary land?
toast(L"Error", L"Could not understand the Path or URI");
}
};
std::thread myThread(threadFunction);
if (myThread.joinable())
{
myThread.detach();
}
Logger::trace(L"ChordKeyboardHandler:returning..");
return 1;
}
else if (remapToShortcut)
{
// Get the common keys between the two shortcuts if this is not a runProgram shortcut
int commonKeys = it->first.GetCommonModifiersCount(std::get<Shortcut>(it->second.targetShortcut));
// If the original shortcut modifiers are a subset of the new shortcut
@ -367,6 +492,8 @@ namespace KeyboardEventHandlers
state.SetActivatedApp(*activatedApp);
}
Logger::trace(L"ChordKeyboardHandler:key_count:{}", key_count);
UINT res = ii.SendVirtualInput(static_cast<UINT>(key_count), keyEventList, sizeof(INPUT));
delete[] keyEventList;
@ -383,7 +510,7 @@ namespace KeyboardEventHandlers
dayWeLastSentAppSpecificShortcutToKeyTelemetryOn = currentDay;
}
}
else if (remapToShortcut)
else if (remapToShortcut && (!isRunProgram) && (!isOpenUri))
{
static int dayWeLastSentAppSpecificShortcutToShortcutTelemetryOn = -1;
auto currentDay = std::chrono::duration_cast<std::chrono::days>(std::chrono::system_clock::now().time_since_epoch()).count();
@ -406,7 +533,7 @@ namespace KeyboardEventHandlers
dayWeLastSentShortcutToKeyTelemetryOn = currentDay;
}
}
else if (remapToShortcut)
else if (remapToShortcut && (!isRunProgram) && (!isOpenUri))
{
static int dayWeLastSentShortcutToShortcutTelemetryOn = -1;
auto currentDay = std::chrono::duration_cast<std::chrono::days>(std::chrono::system_clock::now().time_since_epoch()).count();
@ -433,7 +560,7 @@ namespace KeyboardEventHandlers
// 6. The user releases any key apart from original modifier or original action key - This can't happen since the key down would have to happen first, which is handled above
// Get the common keys between the two shortcuts
int commonKeys = remapToShortcut ? it->first.GetCommonModifiersCount(std::get<Shortcut>(it->second.targetShortcut)) : 0;
int commonKeys = (remapToShortcut && !isRunProgram) ? it->first.GetCommonModifiersCount(std::get<Shortcut>(it->second.targetShortcut)) : 0;
// Case 1: If any of the modifier keys of the original shortcut are released before the action key
if ((it->first.CheckWinKey(data->lParam->vkCode) || it->first.CheckCtrlKey(data->lParam->vkCode) || it->first.CheckAltKey(data->lParam->vkCode) || it->first.CheckShiftKey(data->lParam->vkCode)) && (data->wParam == WM_KEYUP || data->wParam == WM_SYSKEYUP))
@ -441,7 +568,7 @@ namespace KeyboardEventHandlers
// Release new shortcut, and set original shortcut keys except the one released
size_t key_count = 0;
LPINPUT keyEventList = nullptr;
if (remapToShortcut)
if (remapToShortcut && !isRunProgram)
{
// if the released key is present in both shortcuts' modifiers (i.e part of the common modifiers)
if (std::get<Shortcut>(it->second.targetShortcut).CheckWinKey(data->lParam->vkCode) || std::get<Shortcut>(it->second.targetShortcut).CheckCtrlKey(data->lParam->vkCode) || std::get<Shortcut>(it->second.targetShortcut).CheckAltKey(data->lParam->vkCode) || std::get<Shortcut>(it->second.targetShortcut).CheckShiftKey(data->lParam->vkCode))
@ -472,6 +599,7 @@ namespace KeyboardEventHandlers
Helpers::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, static_cast<WORD>(std::get<Shortcut>(it->second.targetShortcut).GetActionKey()), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
}
Helpers::SetModifierKeyEvents(std::get<Shortcut>(it->second.targetShortcut), it->second.winKeyInvoked, keyEventList, i, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, it->first, data->lParam->vkCode);
// Set original shortcut key down state except the action key and the released modifier since the original action key may or may not be held down. If it is held down it will generate it's own key message
@ -892,6 +1020,667 @@ namespace KeyboardEventHandlers
return 0;
}
std::wstring URL_encode(const std::wstring& filepath)
{
std::wostringstream escaped;
escaped.fill('0');
escaped << std::hex;
for (wchar_t ch : filepath)
{
// Encode special characters except for colon after drive letter
if (!iswalnum(ch) && ch != L'-' && ch != L'_' && ch != L'.' && ch != L'~' && !(ch == L':' && std::isalpha(filepath[0])))
{
escaped << std::uppercase;
//escaped << '%' << std::setw(2) << int((unsigned char)ch);
escaped << '%' << std::setw(2) << static_cast<int>((static_cast<unsigned char>(ch)));
escaped << std::nouppercase;
}
else
{
escaped << ch;
}
}
return escaped.str();
}
std::wstring ConvertPathToURI(const std::wstring& filePath)
{
std::wstring fileUri = std::filesystem::absolute(filePath).wstring();
std::replace(fileUri.begin(), fileUri.end(), L'\\', L'/');
fileUri = L"file:///" + URL_encode(fileUri);
return fileUri;
}
void ResetAllOtherStartedChords(State& state, const std::optional<std::wstring>& activatedApp, DWORD keyToKeep)
{
for (auto& itShortcut_2 : state.GetSortedShortcutRemapVector(activatedApp))
{
if (keyToKeep == NULL || itShortcut_2.actionKey != keyToKeep)
{
itShortcut_2.SetChordStarted(false);
}
}
}
void ResetAllStartedChords(State& state, const std::optional<std::wstring>& activatedApp)
{
ResetAllOtherStartedChords(state, activatedApp, NULL);
}
ResetChordsResults ResetChordsIfNeeded(LowlevelKeyboardEvent* data, State& state, const std::optional<std::wstring>& activatedApp)
{
ResetChordsResults result;
result.AnyChordStarted = false;
result.CurrentKeyIsModifierKey = false;
bool isNewControlKey = false;
bool anyChordStarted = false;
if (VK_LWIN == data->lParam->vkCode || VK_RWIN == data->lParam->vkCode)
{
isNewControlKey = true;
}
if (VK_LSHIFT == data->lParam->vkCode || VK_RSHIFT == data->lParam->vkCode)
{
isNewControlKey = true;
}
if (VK_LMENU == data->lParam->vkCode || VK_RMENU == data->lParam->vkCode)
{
isNewControlKey = true;
}
if (VK_LCONTROL == data->lParam->vkCode || VK_RCONTROL == data->lParam->vkCode)
{
isNewControlKey = true;
}
if (isNewControlKey)
{
//Logger::trace(L"ChordKeyboardHandler:reset");
for (auto& itShortcut : state.GetSortedShortcutRemapVector(activatedApp))
{
itShortcut.SetChordStarted(false);
}
result.CurrentKeyIsModifierKey = true;
}
else
{
for (auto& itShortcut : state.GetSortedShortcutRemapVector(activatedApp))
{
if (itShortcut.IsChordStarted())
{
result.AnyChordStarted = true;
break;
}
}
}
return result;
}
struct handle_data
{
unsigned long process_id;
HWND window_handle;
};
// used for reactivating a window for a program we already started.
HWND FindMainWindow(unsigned long process_id, const bool allowNonVisible)
{
handle_data data;
data.process_id = process_id;
data.window_handle = 0;
if (allowNonVisible)
{
EnumWindows(EnumWindowsCallbackAllowNonVisible, reinterpret_cast<LPARAM>(&data));
}
else
{
EnumWindows(EnumWindowsCallback, reinterpret_cast<LPARAM>(&data));
}
return data.window_handle;
}
// used by FindMainWindow
BOOL CALLBACK EnumWindowsCallbackAllowNonVisible(HWND handle, LPARAM lParam)
{
handle_data& data = *reinterpret_cast<handle_data*>(lParam);
unsigned long process_id = 0;
GetWindowThreadProcessId(handle, &process_id);
if (data.process_id == process_id)
{
data.window_handle = handle;
return FALSE;
}
return TRUE;
}
// used by FindMainWindow
BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam)
{
handle_data& data = *reinterpret_cast<handle_data*>(lParam);
unsigned long process_id = 0;
GetWindowThreadProcessId(handle, &process_id);
if (data.process_id != process_id || !(GetWindow(handle, GW_OWNER) == static_cast<HWND>(0) && IsWindowVisible(handle)))
{
return TRUE;
}
data.window_handle = handle;
return FALSE;
}
// GetProcessIdByName also used by HandleCreateProcessHotKeysAndChords
std::vector<DWORD> GetProcessesIdByName(const std::wstring& processName)
{
std::vector<DWORD> processIds;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(snapshot, &processEntry))
{
do
{
if (_wcsicmp(processEntry.szExeFile, processName.c_str()) == 0)
{
processIds.push_back(processEntry.th32ProcessID);
}
} while (Process32Next(snapshot, &processEntry));
}
CloseHandle(snapshot);
}
return processIds;
}
DWORD GetProcessIdByName(const std::wstring& processName)
{
DWORD pid = 0;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(snapshot, &processEntry))
{
do
{
if (_wcsicmp(processEntry.szExeFile, processName.c_str()) == 0)
{
pid = processEntry.th32ProcessID;
break;
}
} while (Process32Next(snapshot, &processEntry));
}
CloseHandle(snapshot);
}
return pid;
}
// Use to find a process by its name
std::wstring GetFileNameFromPath(const std::wstring& fullPath)
{
size_t found = fullPath.find_last_of(L"\\");
if (found != std::wstring::npos)
{
return fullPath.substr(found + 1);
}
return fullPath;
}
void toast(param::hstring const& message1, param::hstring const& message2) noexcept
{
try
{
// Alternatively can build DOM from code:
XmlDocument toastXml;
XmlElement toastElement = toastXml.CreateElement(L"toast");
XmlElement visualElement = toastXml.CreateElement(L"visual");
XmlElement bindingElement = toastXml.CreateElement(L"binding");
XmlElement textElement1 = toastXml.CreateElement(L"text");
XmlElement textElement2 = toastXml.CreateElement(L"text");
toastXml.AppendChild(toastElement);
toastElement.AppendChild(visualElement);
visualElement.AppendChild(bindingElement);
bindingElement.AppendChild(textElement1);
bindingElement.AppendChild(textElement2);
bindingElement.SetAttribute(L"template", L"ToastGeneric");
textElement1.InnerText(message1);
textElement2.InnerText(message2);
Logger::trace(L"ChordKeyboardHandler:toastXml {}", toastXml.GetXml());
std::wstring APPLICATION_ID = L"Microsoft.PowerToysWin32";
const auto notifier = ToastNotificationManager::ToastNotificationManager::CreateToastNotifier(APPLICATION_ID);
ToastNotification notification{ toastXml };
notifier.Show(notification);
}
catch (...)
{
}
/*std::thread{ [message] {
} }.detach();*/
}
void CreateOrShowProcessForShortcut(Shortcut shortcut) noexcept
{
WCHAR fullExpandedFilePath[MAX_PATH];
DWORD result = ExpandEnvironmentStrings(shortcut.runProgramFilePath.c_str(), fullExpandedFilePath, MAX_PATH);
auto fileNamePart = GetFileNameFromPath(fullExpandedFilePath);
Logger::trace(L"ChordKeyboardHandler:{}, trying to run {}", fileNamePart, fullExpandedFilePath);
//lastKeyInChord = 0;
DWORD targetPid = GetProcessIdByName(fileNamePart);
/*if (fileNamePart != L"explorer.exe" && fileNamePart != L"powershell.exe" && fileNamePart != L"cmd.exe" && fileNamePart != L"msedge.exe")
{
targetPid = GetProcessIdByName(fileNamePart);
}*/
Logger::trace(L"ChordKeyboardHandler:{}, already running, pid:{}, alreadyRunningAction:{}", fileNamePart, targetPid, shortcut.alreadyRunningAction);
if (targetPid != 0 && shortcut.alreadyRunningAction != Shortcut::ProgramAlreadyRunningAction::StartAnother)
{
if (shortcut.alreadyRunningAction == Shortcut::ProgramAlreadyRunningAction::EndTask)
{
TerminateProcessesByName(fileNamePart);
return;
}
else if (shortcut.alreadyRunningAction == Shortcut::ProgramAlreadyRunningAction::Close)
{
CloseProcessByName(fileNamePart);
Logger::trace(L"ChordKeyboardHandler:{}, CloseProcessByName returning 3", fileNamePart);
return;
}
else if (shortcut.alreadyRunningAction == Shortcut::ProgramAlreadyRunningAction::ShowWindow)
{
auto processIds = GetProcessesIdByName(fileNamePart);
for (DWORD pid : processIds)
{
ShowProgram(targetPid, fileNamePart, false, false, 0);
}
//if (!ShowProgram(targetPid, fileNamePart, false, false, 0))
//{
// /*auto future = std::async(std::launch::async, [=] {
// std::this_thread::sleep_for(std::chrono::milliseconds(30));
// Logger::trace(L"ChordKeyboardHandler:{}, second try, pid:{}", fileNamePart, targetPid);
// ShowProgram(targetPid, fileNamePart, false, false);
//});*/
//}
return;
}
}
else
{
DWORD dwAttrib = GetFileAttributesW(fullExpandedFilePath);
if (dwAttrib == INVALID_FILE_ATTRIBUTES)
{
std::wstring title = fmt::format(L"Error starting {}", fileNamePart);
std::wstring message = fmt::format(L"The program was not found.");
toast(title, message);
return;
}
std::wstring executable_and_args = fmt::format(L"\"{}\" {}", fullExpandedFilePath, shortcut.runProgramArgs);
WCHAR currentDir[MAX_PATH];
WCHAR* currentDirPtr = currentDir;
DWORD result = ExpandEnvironmentStrings(shortcut.runProgramStartInDir.c_str(), currentDir, MAX_PATH);
if (shortcut.runProgramStartInDir == L"")
{
currentDirPtr = nullptr;
}
else
{
DWORD dwAttrib = GetFileAttributesW(currentDir);
if (dwAttrib == INVALID_FILE_ATTRIBUTES)
{
std::wstring title = fmt::format(L"Error starting {}", fileNamePart);
std::wstring message = fmt::format(L"The start in path was not valid. It could not be used.", currentDir);
currentDirPtr = nullptr;
toast(title, message);
return;
}
}
DWORD processId = 0;
HANDLE newProcessHandle;
if (shortcut.elevationLevel == Shortcut::ElevationLevel::Elevated)
{
newProcessHandle = run_elevated(fullExpandedFilePath, shortcut.runProgramArgs, currentDirPtr, (shortcut.startWindowType == Shortcut::StartWindowType::Normal));
processId = GetProcessId(newProcessHandle);
}
else if (shortcut.elevationLevel == Shortcut::ElevationLevel::NonElevated)
{
run_non_elevated(fullExpandedFilePath, shortcut.runProgramArgs, &processId, currentDirPtr, (shortcut.startWindowType == Shortcut::StartWindowType::Normal));
}
else if (shortcut.elevationLevel == Shortcut::ElevationLevel::DifferentUser)
{
newProcessHandle = run_as_different_user(fullExpandedFilePath, shortcut.runProgramArgs, currentDirPtr, (shortcut.startWindowType == Shortcut::StartWindowType::Normal));
processId = GetProcessId(newProcessHandle);
}
if (processId == 0)
{
std::wstring title = fmt::format(L"Error starting {}", fileNamePart);
std::wstring message = fmt::format(L"The application might not have started.");
toast(title, message);
return;
}
if (shortcut.startWindowType == Shortcut::StartWindowType::Hidden)
{
HideProgram(processId, fileNamePart, 0);
}
//ShowProgram(processId, fileNamePart, true, false, (shortcut.startWindowType == Shortcut::StartWindowType::Hidden), 0);
}
return;
}
void CloseProcessByName(const std::wstring& fileNamePart)
{
auto processIds = GetProcessesIdByName(fileNamePart);
if (processIds.size() == 0)
{
Logger::trace(L"ChordKeyboardHandler:{}, Nothing To WM_CLOSE", fileNamePart);
return;
}
auto threadFunction = [fileNamePart]() {
auto processIds = GetProcessesIdByName(fileNamePart);
auto retryCount = 10;
while (processIds.size() > 0 && retryCount-- > 0)
{
//Logger::trace(L"ChordKeyboardHandler:{}, WM_CLOSE 'ing {}processIds ", fileNamePart, processIds.size());
for (DWORD pid : processIds)
{
//Logger::trace(L"ChordKeyboardHandler:{}, WM_CLOSE ({}) -> pid:{}", fileNamePart, retryCount, pid);
HWND hwnd = FindMainWindow(pid, false);
SendMessage(hwnd, WM_CLOSE, 0, 0);
// small sleep between when there are a lot might help
Sleep(10);
}
processIds = GetProcessesIdByName(fileNamePart);
if (processIds.size() <= 0)
{
Logger::trace(L"ChordKeyboardHandler:{}, WM_CLOSE done", fileNamePart);
break;
}
else
{
Sleep(100);
}
}
};
processIds = GetProcessesIdByName(fileNamePart);
if (processIds.size() > 0)
{
std::thread myThread(threadFunction);
if (myThread.joinable())
{
myThread.detach();
}
}
Logger::trace(L"ChordKeyboardHandler:{}, CloseProcessByName returning", fileNamePart);
}
void TerminateProcessesByName(const std::wstring& fileNamePart)
{
auto processIds = GetProcessesIdByName(fileNamePart);
if (processIds.size() == 0)
{
Logger::trace(L"ChordKeyboardHandler:{}, Nothing To PROCESS_TERMINATE", fileNamePart);
return;
}
for (DWORD pid : processIds)
{
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
Logger::trace(L"ChordKeyboardHandler:{}, PROCESS_TERMINATE (1) -> pid:{}", fileNamePart, pid);
if (hProcess != NULL)
{
if (!TerminateProcess(hProcess, 0))
{
CloseHandle(hProcess);
}
else
{
CloseHandle(hProcess);
}
}
}
}
bool HideProgram(DWORD pid, std::wstring programName, int retryCount)
{
Logger::trace(L"ChordKeyboardHandler:HideProgram starting with {},{}, retryCount:{}", pid, programName, retryCount);
HWND hwnd = FindMainWindow(pid, false);
if (hwnd == NULL)
{
if (retryCount < 20)
{
Logger::trace(L"ChordKeyboardHandler:hwnd not found will retry for pid:{}", pid);
auto future = std::async(std::launch::async, [=] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
auto result = HideProgram(pid, programName, retryCount + 1);
return false;
});
}
}
hwnd = FindWindow(nullptr, nullptr);
auto anyHideResultFailed = false;
Logger::trace(L"ChordKeyboardHandler:{}:{},{}, FindWindow, HideProgram (all)", programName, pid, retryCount);
while (hwnd)
{
DWORD pidForHwnd;
GetWindowThreadProcessId(hwnd, &pidForHwnd);
if (pid == pidForHwnd)
{
if (IsWindowVisible(hwnd))
{
ShowWindow(hwnd, SW_HIDE);
Logger::trace(L"ChordKeyboardHandler:{}, tryToHide {}, {}", programName, reinterpret_cast<uintptr_t>(hwnd), anyHideResultFailed);
}
}
hwnd = FindWindowEx(NULL, hwnd, NULL, NULL);
}
return true;
}
bool ShowProgram(DWORD pid, std::wstring programName, bool isNewProcess, bool minimizeIfVisible, int retryCount)
{
Logger::trace(L"ChordKeyboardHandler:ShowProgram starting with {},{},isNewProcess:{}, tryToHide:{} retryCount:{}", pid, programName, isNewProcess, retryCount);
// a good place to look for this...
// https://github.com/ritchielawrence/cmdow
// try by main window.
auto allowNonVisible = false;
HWND hwnd = FindMainWindow(pid, allowNonVisible);
if (hwnd == NULL)
{
if (retryCount < 20)
{
Logger::trace(L"ChordKeyboardHandler:hwnd not found will retry for pid:{}, allowNonVisible:{}", pid, allowNonVisible);
auto future = std::async(std::launch::async, [=] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
auto result = ShowProgram(pid, programName, isNewProcess, minimizeIfVisible, retryCount + 1);
return false;
});
}
}
else
{
Logger::trace(L"ChordKeyboardHandler:{}, got hwnd from FindMainWindow", programName);
if (hwnd == GetForegroundWindow())
{
// only hide if this was a call from a already open program, don't make small if we just opened it.
if (!isNewProcess && minimizeIfVisible)
{
Logger::trace(L"ChordKeyboardHandler:{}, got GetForegroundWindow, doing SW_MINIMIZE", programName);
return ShowWindow(hwnd, SW_MINIMIZE);
}
return false;
}
else
{
Logger::trace(L"ChordKeyboardHandler:{}, not ForegroundWindow, doing SW_RESTORE", programName);
// Check if the window is minimized
if (IsIconic(hwnd))
{
// Show the window since SetForegroundWindow fails on minimized windows
if (!ShowWindow(hwnd, SW_RESTORE))
{
Logger::error(L"ShowWindow failed");
}
}
INPUT inputs[1] = { { .type = INPUT_MOUSE } };
SendInput(ARRAYSIZE(inputs), inputs, sizeof(INPUT));
if (!SetForegroundWindow(hwnd))
{
auto errorCode = GetLastError();
Logger::warn(L"ChordKeyboardHandler:{}, failed to SetForegroundWindow, {}", programName, errorCode);
return false;
}
else
{
Logger::trace(L"ChordKeyboardHandler:{}, success on SetForegroundWindow", programName);
return true;
}
}
}
if (isNewProcess)
{
return true;
}
if (false)
{
// try by console.
hwnd = FindWindow(nullptr, nullptr);
if (AttachConsole(pid))
{
Logger::trace(L"ChordKeyboardHandler:{}, success on AttachConsole", programName);
// Get the console window handle
hwnd = GetConsoleWindow();
auto showByConsoleSuccess = false;
if (hwnd != NULL)
{
Logger::trace(L"ChordKeyboardHandler:{}, success on GetConsoleWindow, doing SW_RESTORE", programName);
ShowWindow(hwnd, SW_RESTORE);
if (!SetForegroundWindow(hwnd))
{
auto errorCode = GetLastError();
Logger::warn(L"ChordKeyboardHandler:{}, failed to SetForegroundWindow, {}", programName, errorCode);
}
else
{
Logger::trace(L"ChordKeyboardHandler:{}, success on SetForegroundWindow", programName);
showByConsoleSuccess = true;
}
}
// Detach from the console
FreeConsole();
if (showByConsoleSuccess)
{
return true;
}
}
}
// try to just show them all (if they have a title)!.
hwnd = FindWindow(nullptr, nullptr);
auto anyHideResultFailed = false;
if (hwnd)
{
Logger::trace(L"ChordKeyboardHandler:{}:{},{}, FindWindow (show all mode)", programName, pid, retryCount);
while (hwnd)
{
DWORD pidForHwnd;
GetWindowThreadProcessId(hwnd, &pidForHwnd);
if (pid == pidForHwnd)
{
int length = GetWindowTextLength(hwnd);
if (length > 0)
{
ShowWindow(hwnd, SW_RESTORE);
// hwnd is the window handle with targetPid
if (SetForegroundWindow(hwnd))
{
Logger::trace(L"ChordKeyboardHandler:{}, success on SetForegroundWindow", programName);
return true;
}
else
{
auto errorCode = GetLastError();
Logger::warn(L"ChordKeyboardHandler:{}, failed to SetForegroundWindow, {}", programName, errorCode);
}
}
}
hwnd = FindWindowEx(NULL, hwnd, NULL, NULL);
}
}
return false;
}
// Function to a handle an os-level shortcut remap
intptr_t HandleOSLevelShortcutRemapEvent(KeyboardManagerInput::InputInterface& ii, LowlevelKeyboardEvent* data, State& state) noexcept
{

View File

@ -10,6 +10,13 @@ namespace KeyboardManagerInput
namespace KeyboardEventHandlers
{
struct ResetChordsResults
{
bool CurrentKeyIsModifierKey;
bool AnyChordStarted;
};
// Function to a handle a single key remap
intptr_t HandleSingleKeyRemapEvent(KeyboardManagerInput::InputInterface& ii, LowlevelKeyboardEvent* data, State& state) noexcept;
@ -21,6 +28,51 @@ namespace KeyboardEventHandlers
// Function to a handle a shortcut remap
intptr_t HandleShortcutRemapEvent(KeyboardManagerInput::InputInterface& ii, LowlevelKeyboardEvent* data, State& state, const std::optional<std::wstring>& activatedApp = std::nullopt) noexcept;
// Function to reset chord matching
void ResetAllStartedChords(State& state, const std::optional<std::wstring>& activatedApp);
// Function to reset chord matching
void ResetAllOtherStartedChords(State& state, const std::optional<std::wstring>& activatedApp, DWORD keyToKeep);
std::wstring URL_encode(const std::wstring& value);
std::wstring ConvertPathToURI(const std::wstring& filePath);
// Function to reset chord matching if needed
ResetChordsResults ResetChordsIfNeeded(LowlevelKeyboardEvent* data, State& state, const std::optional<std::wstring>& activatedApp);
// Function to handle (start or show) programs for shortcuts
void CreateOrShowProcessForShortcut(Shortcut shortcut) noexcept;
void CloseProcessByName(const std::wstring& fileNamePart);
void TerminateProcessesByName(const std::wstring& fileNamePart);
void toast(winrt::param::hstring const& message1, winrt::param::hstring const& message2) noexcept;
// Function to help FindMainWindow
BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam);
// Function to help FindMainWindow
BOOL CALLBACK EnumWindowsCallbackAllowNonVisible(HWND handle, LPARAM lParam);
// Function to FindMainWindow
HWND FindMainWindow(unsigned long process_id, const bool allowNonVisible);
// Function to GetProcessIdByName
DWORD GetProcessIdByName(const std::wstring& processName);
// Function to GetProcessesIdByName
std::vector<DWORD> GetProcessesIdByName(const std::wstring& processName);
// Function to get just the file name from a fill path
std::wstring GetFileNameFromPath(const std::wstring& fullPath);
// Function to a find and show a running program
bool ShowProgram(DWORD pid, std::wstring programName, bool isNewProcess, bool minimizeIfVisible, int retryCount);
bool HideProgram(DWORD pid, std::wstring programName, int retryCount);
// Function to a handle an os-level shortcut remap
intptr_t HandleOSLevelShortcutRemapEvent(KeyboardManagerInput::InputInterface& ii, LowlevelKeyboardEvent* data, State& state) noexcept;

View File

@ -7,8 +7,6 @@
#include <common/utils/winapi_error.h>
#include <common/logger/logger_settings.h>
#include <keyboardmanager/common/Shortcut.h>
#include <keyboardmanager/common/RemapShortcut.h>
#include <keyboardmanager/common/KeyboardManagerConstants.h>
#include <keyboardmanager/common/Helpers.h>
#include <keyboardmanager/common/KeyboardEventHandlers.h>

View File

@ -11,7 +11,7 @@ public:
// Constructor
KeyboardManager();
~KeyboardManager()
{
if (editorIsRunningEvent)
@ -19,7 +19,7 @@ public:
CloseHandle(editorIsRunningEvent);
}
}
void StartLowlevelKeyboardHook();
void StopLowlevelKeyboardHook();

View File

@ -59,13 +59,14 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
</packages>

View File

@ -3,6 +3,7 @@
#include <windows.h>
#include <ProjectTelemetry.h>
#include <shlwapi.h>
#include <shellapi.h>
#include <stdexcept>
#include <unordered_set>
#include <winrt/base.h>

View File

@ -166,6 +166,10 @@ std::wstring GetShortcutHumanReadableString(Shortcut const & shortcut, LayoutMap
if (shortcut.actionKey != NULL)
{
humanReadableShortcut += keyboardMap.GetKeyName(shortcut.actionKey);
if (shortcut.secondKey != NULL)
{
humanReadableShortcut += L" , " + keyboardMap.GetKeyName(shortcut.secondKey);
}
}
return humanReadableShortcut;
}
@ -227,6 +231,8 @@ void Trace::SendKeyAndShortcutRemapLoadedConfiguration(State& remappings) noexce
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedFrom.ctrlKey), "ModifierRemapFromCtrl"),
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedFrom.altKey), "ModifierRemapFromAlt"),
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedFrom.shiftKey), "ModifierRemapFromShift"),
TraceLoggingBool(shortcutRemappedFrom.HasChord(), "KeyRemapFromHasChord"),
TraceLoggingInt64(shortcutRemappedFrom.secondKey, "KeyRemapFromChordSecondKey"),
TraceLoggingInt64(keyRemappedTo, "KeyRemapTo"),
TraceLoggingWideString(GetShortcutHumanReadableString(shortcutRemappedFrom, keyboardMap).c_str(), "HumanRemapFrom"),
TraceLoggingWideString(keyboardMap.GetKeyName(keyRemappedTo).c_str(), "HumanRemapTo"));
@ -234,6 +240,11 @@ void Trace::SendKeyAndShortcutRemapLoadedConfiguration(State& remappings) noexce
else if (shortcutRemap.second.targetShortcut.index() == 1) // 1 - Remapping to shortcut
{
Shortcut shortcutRemappedTo = std::get<Shortcut>(shortcutRemap.second.targetShortcut);
if (shortcutRemappedTo.IsRunProgram() || shortcutRemappedTo.IsOpenURI())
{
// Don't include Start app or Open URI mappings in this telemetry.
continue;
}
TraceLoggingWrite(
g_hProvider,
"KeyboardManager_ShortcutRemapConfigurationLoaded",
@ -244,6 +255,8 @@ void Trace::SendKeyAndShortcutRemapLoadedConfiguration(State& remappings) noexce
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedFrom.ctrlKey), "ModifierRemapFromCtrl"),
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedFrom.altKey), "ModifierRemapFromAlt"),
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedFrom.shiftKey), "ModifierRemapFromShift"),
TraceLoggingBool(shortcutRemappedFrom.HasChord(), "KeyRemapFromHasChord"),
TraceLoggingInt64(shortcutRemappedFrom.secondKey, "KeyRemapFromChordSecondKey"),
TraceLoggingInt64(shortcutRemappedTo.actionKey, "KeyRemapTo"),
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedTo.winKey), "ModifierRemapToWin"),
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedTo.ctrlKey), "ModifierRemapToCtrl"),
@ -274,6 +287,8 @@ void Trace::SendKeyAndShortcutRemapLoadedConfiguration(State& remappings) noexce
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedFrom.ctrlKey), "ModifierRemapFromCtrl"),
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedFrom.altKey), "ModifierRemapFromAlt"),
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedFrom.shiftKey), "ModifierRemapFromShift"),
TraceLoggingBool(shortcutRemappedFrom.HasChord(), "KeyRemapFromHasChord"),
TraceLoggingInt64(shortcutRemappedFrom.secondKey, "KeyRemapFromChordSecondKey"),
TraceLoggingInt64(keyRemappedTo, "KeyRemapTo"),
TraceLoggingWideString(GetShortcutHumanReadableString(shortcutRemappedFrom, keyboardMap).c_str(), "HumanRemapFrom"),
TraceLoggingWideString(keyboardMap.GetKeyName(keyRemappedTo).c_str(), "HumanRemapTo"),
@ -283,6 +298,11 @@ void Trace::SendKeyAndShortcutRemapLoadedConfiguration(State& remappings) noexce
else if (shortcutRemap.second.targetShortcut.index() == 1) // 1 - Remapping to shortcut
{
Shortcut shortcutRemappedTo = std::get<Shortcut>(shortcutRemap.second.targetShortcut);
if (shortcutRemappedTo.IsRunProgram() || shortcutRemappedTo.IsOpenURI())
{
// Don't include Start app or Open URI mappings in this telemetry.
continue;
}
TraceLoggingWrite(
g_hProvider,
"KeyboardManager_AppSpecificShortcutRemapConfigurationLoaded",
@ -293,6 +313,8 @@ void Trace::SendKeyAndShortcutRemapLoadedConfiguration(State& remappings) noexce
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedFrom.ctrlKey), "ModifierRemapFromCtrl"),
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedFrom.altKey), "ModifierRemapFromAlt"),
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedFrom.shiftKey), "ModifierRemapFromShift"),
TraceLoggingBool(shortcutRemappedFrom.HasChord(), "KeyRemapFromHasChord"),
TraceLoggingInt64(shortcutRemappedFrom.secondKey, "KeyRemapFromChordSecondKey"),
TraceLoggingInt64(shortcutRemappedTo.actionKey, "KeyRemapTo"),
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedTo.winKey), "ModifierRemapToWin"),
TraceLoggingInt8(static_cast<INT8>(shortcutRemappedTo.ctrlKey), "ModifierRemapToCtrl"),

View File

@ -35,6 +35,7 @@
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>

View File

@ -28,6 +28,9 @@ namespace KeyboardManagerConstants
// Name of the property use to store shortcut to text remaps.
inline const std::wstring RemapShortcutsToTextSettingName = L"remapShortcutsToText";
// Name of the property use to store shortcut to run-program remaps.
inline const std::wstring RemapShortcutsToRunProgramSettingName = L"remapShortcutsToRunProgram";
// Name of the property use to store global shortcut remaps array.
inline const std::wstring GlobalRemapShortcutsSettingName = L"global";
@ -43,6 +46,33 @@ namespace KeyboardManagerConstants
// Name of the property use to store new remapped string.
inline const std::wstring NewTextSettingName = L"unicodeText";
// Name of the property use to store runProgramStartInDir.
inline const std::wstring RunProgramStartInDirSettingName = L"runProgramStartInDir";
// Name of the property use to store runProgramStartInDir.
inline const std::wstring RunProgramElevationLevelSettingName = L"runProgramElevationLevel";
// Name of the property use to store runProgramAlreadyRunningAction.
inline const std::wstring RunProgramAlreadyRunningAction = L"runProgramAlreadyRunningAction";
// Name of the property use to store runProgramStartWindowType.
inline const std::wstring RunProgramStartWindowType = L"runProgramStartWindowType";
// Name of the property use to store runProgramArgs.
inline const std::wstring RunProgramArgsSettingName = L"runProgramArgs";
// Name of the property use to store runProgramFilePath.
inline const std::wstring RunProgramFilePathSettingName = L"runProgramFilePath";
// Name of the property use to store secondKeyOfChord.
inline const std::wstring ShortcutSecondKeyOfChordSettingName = L"secondKeyOfChord";
// Name of the property use to store openUri.
inline const std::wstring ShortcutOpenURI = L"openUri";
// Name of the property use to store shortcutOperationType.
inline const std::wstring ShortcutOperationType = L"operationType";
// Name of the property use to store the target application.
inline const std::wstring TargetAppSettingName = L"targetApp";

View File

@ -179,6 +179,13 @@ bool MappingConfiguration::LoadSingleKeyToTextRemaps(const json::JsonObject& jso
{
auto originalKey = it.GetObjectW().GetNamedString(KeyboardManagerConstants::OriginalKeysSettingName);
auto newText = it.GetObjectW().GetNamedString(KeyboardManagerConstants::NewTextSettingName);
// undo dummy data for backwards compatibility
if (newText == L"*Unsupported*")
{
newText == L"";
}
AddSingleKeyToTextRemap(std::stoul(originalKey.c_str()), newText.c_str());
}
catch (...)
@ -212,6 +219,45 @@ bool MappingConfiguration::LoadAppSpecificShortcutRemaps(const json::JsonObject&
auto newRemapKeys = it.GetObjectW().GetNamedString(KeyboardManagerConstants::NewRemapKeysSettingName, {});
auto newRemapText = it.GetObjectW().GetNamedString(KeyboardManagerConstants::NewTextSettingName, {});
auto targetApp = it.GetObjectW().GetNamedString(KeyboardManagerConstants::TargetAppSettingName);
auto operationType = it.GetObjectW().GetNamedNumber(KeyboardManagerConstants::ShortcutOperationType, 0);
// undo dummy data for backwards compatibility
if (newRemapText == L"*Unsupported*")
{
newRemapText == L"";
}
// check Shortcut::OperationType
if (operationType == 1)
{
auto runProgramFilePath = it.GetObjectW().GetNamedString(KeyboardManagerConstants::RunProgramFilePathSettingName, L"");
auto runProgramArgs = it.GetObjectW().GetNamedString(KeyboardManagerConstants::RunProgramArgsSettingName, L"");
auto runProgramStartInDir = it.GetObjectW().GetNamedString(KeyboardManagerConstants::RunProgramStartInDirSettingName, L"");
auto runProgramElevationLevel = it.GetObjectW().GetNamedNumber(KeyboardManagerConstants::RunProgramElevationLevelSettingName, 0);
auto runProgramAlreadyRunningAction = it.GetObjectW().GetNamedNumber(KeyboardManagerConstants::RunProgramAlreadyRunningAction, 0);
auto runProgramStartWindowType = it.GetObjectW().GetNamedNumber(KeyboardManagerConstants::RunProgramStartWindowType, 0);
auto secondKeyOfChord = it.GetObjectW().GetNamedNumber(KeyboardManagerConstants::ShortcutSecondKeyOfChordSettingName, 0);
auto tempShortcut = Shortcut(newRemapKeys.c_str());
tempShortcut.operationType = Shortcut::OperationType::RunProgram;
tempShortcut.runProgramFilePath = runProgramFilePath;
tempShortcut.runProgramArgs = runProgramArgs;
tempShortcut.runProgramStartInDir = runProgramStartInDir;
tempShortcut.elevationLevel = static_cast<Shortcut::ElevationLevel>(runProgramElevationLevel);
tempShortcut.alreadyRunningAction = static_cast<Shortcut::ProgramAlreadyRunningAction>(runProgramAlreadyRunningAction);
tempShortcut.startWindowType = static_cast<Shortcut::StartWindowType>(runProgramStartWindowType);
AddAppSpecificShortcut(targetApp.c_str(), Shortcut(originalKeys.c_str(), static_cast<DWORD>(secondKeyOfChord)), tempShortcut);
}
else if (operationType == 2)
{
auto secondKeyOfChord = it.GetObjectW().GetNamedNumber(KeyboardManagerConstants::ShortcutSecondKeyOfChordSettingName, 0);
auto tempShortcut = Shortcut(newRemapKeys.c_str());
tempShortcut.operationType = Shortcut::OperationType::OpenURI;
tempShortcut.uriToOpen = it.GetObjectW().GetNamedString(KeyboardManagerConstants::ShortcutOpenURI, L"");
AddAppSpecificShortcut(targetApp.c_str(), Shortcut(originalKeys.c_str(), static_cast<DWORD>(secondKeyOfChord)), tempShortcut);
}
if (!newRemapKeys.empty())
{
@ -268,8 +314,47 @@ bool MappingConfiguration::LoadShortcutRemaps(const json::JsonObject& jsonData,
auto originalKeys = it.GetObjectW().GetNamedString(KeyboardManagerConstants::OriginalKeysSettingName);
auto newRemapKeys = it.GetObjectW().GetNamedString(KeyboardManagerConstants::NewRemapKeysSettingName, {});
auto newRemapText = it.GetObjectW().GetNamedString(KeyboardManagerConstants::NewTextSettingName, {});
auto operationType = it.GetObjectW().GetNamedNumber(KeyboardManagerConstants::ShortcutOperationType, 0);
if (!newRemapKeys.empty())
// undo dummy data for backwards compatibility
if (newRemapText == L"*Unsupported*")
{
newRemapText == L"";
}
// check Shortcut::OperationType
if (operationType == 1)
{
auto runProgramFilePath = it.GetObjectW().GetNamedString(KeyboardManagerConstants::RunProgramFilePathSettingName, L"");
auto runProgramArgs = it.GetObjectW().GetNamedString(KeyboardManagerConstants::RunProgramArgsSettingName, L"");
auto runProgramStartInDir = it.GetObjectW().GetNamedString(KeyboardManagerConstants::RunProgramStartInDirSettingName, L"");
auto runProgramElevationLevel = it.GetObjectW().GetNamedNumber(KeyboardManagerConstants::RunProgramElevationLevelSettingName, 0);
auto runProgramStartWindowType = it.GetObjectW().GetNamedNumber(KeyboardManagerConstants::RunProgramStartWindowType, 0);
auto secondKeyOfChord = it.GetObjectW().GetNamedNumber(KeyboardManagerConstants::ShortcutSecondKeyOfChordSettingName, 0);
auto runProgramAlreadyRunningAction = it.GetObjectW().GetNamedNumber(KeyboardManagerConstants::RunProgramAlreadyRunningAction, 0);
auto tempShortcut = Shortcut(newRemapKeys.c_str());
tempShortcut.operationType = Shortcut::OperationType::RunProgram;
tempShortcut.runProgramFilePath = runProgramFilePath;
tempShortcut.runProgramArgs = runProgramArgs;
tempShortcut.runProgramStartInDir = runProgramStartInDir;
tempShortcut.elevationLevel = static_cast<Shortcut::ElevationLevel>(runProgramElevationLevel);
tempShortcut.alreadyRunningAction = static_cast<Shortcut::ProgramAlreadyRunningAction>(runProgramAlreadyRunningAction);
tempShortcut.startWindowType = static_cast<Shortcut::StartWindowType>(runProgramStartWindowType);
AddOSLevelShortcut(Shortcut(originalKeys.c_str(), static_cast<DWORD>(secondKeyOfChord)), tempShortcut);
}
else if (operationType == 2)
{
auto secondKeyOfChord = it.GetObjectW().GetNamedNumber(KeyboardManagerConstants::ShortcutSecondKeyOfChordSettingName, 0);
auto tempShortcut = Shortcut(newRemapKeys.c_str());
tempShortcut.operationType = Shortcut::OperationType::OpenURI;
tempShortcut.uriToOpen = it.GetObjectW().GetNamedString(KeyboardManagerConstants::ShortcutOpenURI, L"");
AddOSLevelShortcut(Shortcut(originalKeys.c_str(), static_cast<DWORD>(secondKeyOfChord)), tempShortcut);
}
else if (!newRemapKeys.empty())
{
// If remapped to a shortcut
if (std::wstring(newRemapKeys).find(L";") != std::string::npos)
@ -416,13 +501,48 @@ bool MappingConfiguration::SaveSettingsToFile()
// For shortcut to shortcut remapping
else if (it.second.targetShortcut.index() == 1)
{
keys.SetNamedValue(KeyboardManagerConstants::NewRemapKeysSettingName, json::value(std::get<Shortcut>(it.second.targetShortcut).ToHstringVK()));
auto targetShortcut = std::get<Shortcut>(it.second.targetShortcut);
if (targetShortcut.operationType == Shortcut::OperationType::RunProgram)
{
keys.SetNamedValue(KeyboardManagerConstants::RunProgramElevationLevelSettingName, json::value(static_cast<unsigned int>(targetShortcut.elevationLevel)));
keys.SetNamedValue(KeyboardManagerConstants::ShortcutSecondKeyOfChordSettingName, json::value(static_cast<unsigned int>(it.first.secondKey)));
keys.SetNamedValue(KeyboardManagerConstants::ShortcutOperationType, json::value(static_cast<unsigned int>(targetShortcut.operationType)));
keys.SetNamedValue(KeyboardManagerConstants::RunProgramAlreadyRunningAction, json::value(static_cast<unsigned int>(targetShortcut.alreadyRunningAction)));
keys.SetNamedValue(KeyboardManagerConstants::RunProgramStartWindowType, json::value(static_cast<unsigned int>(targetShortcut.startWindowType)));
keys.SetNamedValue(KeyboardManagerConstants::RunProgramFilePathSettingName, json::value(targetShortcut.runProgramFilePath));
keys.SetNamedValue(KeyboardManagerConstants::RunProgramArgsSettingName, json::value(targetShortcut.runProgramArgs));
keys.SetNamedValue(KeyboardManagerConstants::RunProgramStartInDirSettingName, json::value(targetShortcut.runProgramStartInDir));
// we need to add this dummy data for backwards compatibility
keys.SetNamedValue(KeyboardManagerConstants::NewTextSettingName, json::value(L"*Unsupported*"));
}
else if (targetShortcut.operationType == Shortcut::OperationType::OpenURI)
{
keys.SetNamedValue(KeyboardManagerConstants::RunProgramElevationLevelSettingName, json::value(static_cast<unsigned int>(targetShortcut.elevationLevel)));
keys.SetNamedValue(KeyboardManagerConstants::ShortcutSecondKeyOfChordSettingName, json::value(static_cast<unsigned int>(it.first.secondKey)));
keys.SetNamedValue(KeyboardManagerConstants::ShortcutOperationType, json::value(static_cast<unsigned int>(targetShortcut.operationType)));
keys.SetNamedValue(KeyboardManagerConstants::ShortcutOpenURI, json::value(targetShortcut.uriToOpen));
// we need to add this dummy data for backwards compatibility
keys.SetNamedValue(KeyboardManagerConstants::NewTextSettingName, json::value(L"*Unsupported*"));
}
else
{
keys.SetNamedValue(KeyboardManagerConstants::ShortcutOperationType, json::value(static_cast<unsigned int>(targetShortcut.operationType)));
keys.SetNamedValue(KeyboardManagerConstants::ShortcutSecondKeyOfChordSettingName, json::value(static_cast<unsigned int>(it.first.secondKey)));
keys.SetNamedValue(KeyboardManagerConstants::NewRemapKeysSettingName, json::value(targetShortcut.ToHstringVK()));
}
}
// For shortcut to text remapping
else if (it.second.targetShortcut.index() == 2)
{
remapToText = true;
keys.SetNamedValue(KeyboardManagerConstants::NewTextSettingName, json::value(std::get<std::wstring>(it.second.targetShortcut)));
keys.SetNamedValue(KeyboardManagerConstants::ShortcutSecondKeyOfChordSettingName, json::value(static_cast<unsigned int>(it.first.secondKey)));
}
if (!remapToText)
@ -450,7 +570,40 @@ bool MappingConfiguration::SaveSettingsToFile()
// For shortcut to shortcut remapping
else if (itKeys.second.targetShortcut.index() == 1)
{
keys.SetNamedValue(KeyboardManagerConstants::NewRemapKeysSettingName, json::value(std::get<Shortcut>(itKeys.second.targetShortcut).ToHstringVK()));
auto targetShortcut = std::get<Shortcut>(itKeys.second.targetShortcut);
if (targetShortcut.operationType == Shortcut::OperationType::RunProgram)
{
keys.SetNamedValue(KeyboardManagerConstants::RunProgramElevationLevelSettingName, json::value(static_cast<unsigned int>(targetShortcut.elevationLevel)));
keys.SetNamedValue(KeyboardManagerConstants::ShortcutSecondKeyOfChordSettingName, json::value(static_cast<unsigned int>(itKeys.first.secondKey)));
keys.SetNamedValue(KeyboardManagerConstants::ShortcutOperationType, json::value(static_cast<unsigned int>(targetShortcut.operationType)));
keys.SetNamedValue(KeyboardManagerConstants::RunProgramAlreadyRunningAction, json::value(static_cast<unsigned int>(targetShortcut.alreadyRunningAction)));
keys.SetNamedValue(KeyboardManagerConstants::RunProgramStartWindowType, json::value(static_cast<unsigned int>(targetShortcut.startWindowType)));
keys.SetNamedValue(KeyboardManagerConstants::RunProgramFilePathSettingName, json::value(targetShortcut.runProgramFilePath));
keys.SetNamedValue(KeyboardManagerConstants::RunProgramArgsSettingName, json::value(targetShortcut.runProgramArgs));
keys.SetNamedValue(KeyboardManagerConstants::RunProgramStartInDirSettingName, json::value(targetShortcut.runProgramStartInDir));
// we need to add this dummy data for backwards compatibility
keys.SetNamedValue(KeyboardManagerConstants::NewTextSettingName, json::value(L"*Unsupported*"));
}
else if (targetShortcut.operationType == Shortcut::OperationType::OpenURI)
{
keys.SetNamedValue(KeyboardManagerConstants::RunProgramElevationLevelSettingName, json::value(static_cast<unsigned int>(targetShortcut.elevationLevel)));
keys.SetNamedValue(KeyboardManagerConstants::ShortcutSecondKeyOfChordSettingName, json::value(static_cast<unsigned int>(itKeys.first.secondKey)));
keys.SetNamedValue(KeyboardManagerConstants::ShortcutOperationType, json::value(static_cast<unsigned int>(targetShortcut.operationType)));
keys.SetNamedValue(KeyboardManagerConstants::ShortcutOpenURI, json::value(targetShortcut.uriToOpen));
// we need to add this dummy data for backwards compatibility
keys.SetNamedValue(KeyboardManagerConstants::NewTextSettingName, json::value(L"*Unsupported*"));
}
else
{
keys.SetNamedValue(KeyboardManagerConstants::ShortcutOperationType, json::value(static_cast<unsigned int>(targetShortcut.operationType)));
keys.SetNamedValue(KeyboardManagerConstants::NewRemapKeysSettingName, json::value(std::get<Shortcut>(itKeys.second.targetShortcut).ToHstringVK()));
}
}
else if (itKeys.second.targetShortcut.index() == 2)
{

View File

@ -26,11 +26,33 @@ Shortcut::Shortcut(const std::wstring& shortcutVK) :
winKey(ModifierKey::Disabled), ctrlKey(ModifierKey::Disabled), altKey(ModifierKey::Disabled), shiftKey(ModifierKey::Disabled), actionKey(NULL)
{
auto keys = splitwstring(shortcutVK, ';');
SetKeyCodes(ConvertToNumbers(keys));
}
std::vector<int32_t> Shortcut::ConvertToNumbers(std::vector<std::wstring>& keys)
{
std::vector<int32_t> keysAsNumbers;
for (auto it : keys)
{
auto vkKeyCode = std::stoul(it);
SetKey(vkKeyCode);
keysAsNumbers.push_back(vkKeyCode);
}
return keysAsNumbers;
}
// Constructor to initialize Shortcut from single key
Shortcut::Shortcut(const DWORD key)
{
SetKey(key);
}
// Constructor to initialize Shortcut from it's virtual key code string representation.
Shortcut::Shortcut(const std::wstring& shortcutVK, const DWORD secondKeyOfChord) :
winKey(ModifierKey::Disabled), ctrlKey(ModifierKey::Disabled), altKey(ModifierKey::Disabled), shiftKey(ModifierKey::Disabled), actionKey(NULL)
{
auto keys = splitwstring(shortcutVK, ';');
SetKeyCodes(ConvertToNumbers(keys));
secondKey = secondKeyOfChord;
}
// Constructor to initialize shortcut from a list of keys
@ -88,6 +110,8 @@ void Shortcut::Reset()
altKey = ModifierKey::Disabled;
shiftKey = ModifierKey::Disabled;
actionKey = NULL;
secondKey = NULL;
chordStarted = false;
}
// Function to return the action key
@ -96,6 +120,36 @@ DWORD Shortcut::GetActionKey() const
return actionKey;
}
bool Shortcut::IsRunProgram() const
{
return operationType == OperationType::RunProgram;
}
bool Shortcut::IsOpenURI() const
{
return operationType == OperationType::OpenURI;
}
DWORD Shortcut::GetSecondKey() const
{
return secondKey;
}
bool Shortcut::HasChord() const
{
return secondKey != NULL;
}
void Shortcut::SetChordStarted(bool started)
{
chordStarted = started;
}
bool Shortcut::IsChordStarted() const
{
return chordStarted;
}
// Function to return the virtual key code of the win key state expected in the shortcut. Argument is used to decide which win key to return in case of both. If the current shortcut doesn't use both win keys then arg is ignored. Return NULL if it is not a part of the shortcut
DWORD Shortcut::GetWinKey(const ModifierKey& input) const
{
@ -281,6 +335,16 @@ bool Shortcut::CheckShiftKey(const DWORD input) const
}
}
bool Shortcut::SetSecondKey(const DWORD input)
{
if (secondKey == input)
{
return false;
}
secondKey = input;
return true;
}
// Function to set a key in the shortcut based on the passed key code argument. Returns false if it is already set to the same value. This can be used to avoid UI refreshing
bool Shortcut::SetKey(const DWORD input)
{
@ -413,10 +477,10 @@ void Shortcut::ResetKey(const DWORD input)
{
shiftKey = ModifierKey::Disabled;
}
else
{
actionKey = {};
}
// we always want to reset these also, I think for now since this got a little weirder when chords
actionKey = {};
secondKey = {};
}
// Function to return the string representation of the shortcut in virtual key codes appended in a string by ";" separator.
@ -444,6 +508,11 @@ winrt::hstring Shortcut::ToHstringVK() const
output = output + winrt::to_hstring(static_cast<unsigned int>(GetActionKey())) + winrt::to_hstring(L";");
}
if (secondKey != NULL)
{
output = output + winrt::to_hstring(static_cast<unsigned int>(GetSecondKey())) + winrt::to_hstring(L";");
}
if (!output.empty())
{
output = winrt::hstring(output.c_str(), output.size() - 1);
@ -479,15 +548,48 @@ std::vector<DWORD> Shortcut::GetKeyCodes()
return keys;
}
bool Shortcut::IsActionKey(const DWORD input)
{
auto shortcut = Shortcut();
shortcut.SetKey(input);
return (shortcut.actionKey != NULL);
}
bool Shortcut::IsModifier(const DWORD input)
{
auto shortcut = Shortcut();
shortcut.SetKey(input);
return (shortcut.actionKey == NULL);
}
// Function to set a shortcut from a vector of key codes
void Shortcut::SetKeyCodes(const std::vector<int32_t>& keys)
{
Reset();
bool foundActionKey = false;
for (int i = 0; i < keys.size(); i++)
{
if (keys[i] != -1 && keys[i] != 0)
{
SetKey(keys[i]);
Shortcut tempShortcut = Shortcut(keys[i]);
if (!foundActionKey && tempShortcut.actionKey != NULL)
{
// last key was an action key, next key is secondKey
foundActionKey = true;
SetKey(keys[i]);
}
else if (foundActionKey && tempShortcut.actionKey != NULL)
{
// already found actionKey, and we found another, add this as the secondKey
secondKey = keys[i];
}
else
{
// just add whatever it is.
SetKey(keys[i]);
}
}
}
}

View File

@ -19,21 +19,75 @@ private:
inline auto comparator() const
{
return std::make_tuple(winKey, ctrlKey, altKey, shiftKey, actionKey);
return std::make_tuple(winKey, ctrlKey, altKey, shiftKey, actionKey, secondKey);
}
// helper to use the same code in more places.
std::vector<int32_t> Shortcut::ConvertToNumbers(std::vector<std::wstring>& keys);
public:
enum ElevationLevel
{
NonElevated = 0,
Elevated = 1,
DifferentUser = 2
};
enum OperationType
{
RemapShortcut = 0,
RunProgram = 1,
OpenURI = 2
};
enum StartWindowType
{
Normal = 0,
Hidden = 1,
Minimized = 2,
Maximized = 2
};
enum ProgramAlreadyRunningAction
{
ShowWindow = 0,
StartAnother = 1,
DoNothing = 2,
Close = 3,
EndTask = 4,
CloseAndEndTask = 5,
};
ModifierKey winKey = ModifierKey::Disabled;
ModifierKey ctrlKey = ModifierKey::Disabled;
ModifierKey altKey = ModifierKey::Disabled;
ModifierKey shiftKey = ModifierKey::Disabled;
std::wstring runProgramFilePath;
std::wstring runProgramArgs;
std::wstring runProgramStartInDir;
std::wstring uriToOpen;
ProgramAlreadyRunningAction alreadyRunningAction = ProgramAlreadyRunningAction::ShowWindow;
ElevationLevel elevationLevel = ElevationLevel::NonElevated;
OperationType operationType = OperationType::RemapShortcut;
StartWindowType startWindowType = StartWindowType::Normal;
DWORD actionKey = {};
DWORD secondKey = {}; // of the chord
bool chordStarted = false;
Shortcut() = default;
// Constructor to initialize Shortcut from single key
Shortcut(const DWORD key);
// Constructor to initialize Shortcut from it's virtual key code string representation.
Shortcut(const std::wstring& shortcutVK);
// Constructor to initialize Shortcut from it's virtual key code string representation.
Shortcut(const std::wstring& shortcutVK, const DWORD _secondKeyOfChord);
// Constructor to initialize shortcut from a list of keys
Shortcut(const std::vector<int32_t>& keys);
@ -47,6 +101,10 @@ public:
return lhs.comparator() == rhs.comparator();
}
static bool Shortcut::IsActionKey(const DWORD input);
static bool Shortcut::IsModifier(const DWORD input);
// Function to return the number of keys in the shortcut
int Size() const;
@ -59,6 +117,24 @@ public:
// Function to return the action key
DWORD GetActionKey() const;
// Function to return IsRunProgram
bool Shortcut::IsRunProgram() const;
// Function to return IsOpenURI
bool Shortcut::IsOpenURI() const;
// Function to return the second key (of the chord)
DWORD Shortcut::GetSecondKey() const;
// Function to check if a chord is started for this shortcut
bool Shortcut::IsChordStarted() const;
// Function to check if this shortcut has a chord
bool Shortcut::HasChord() const;
// Function to set that we started a chord
void Shortcut::SetChordStarted(bool started);
// Function to return the virtual key code of the win key state expected in the shortcut. Argument is used to decide which win key to return in case of both. If the current shortcut doesn't use both win keys then arg is ignored. Return NULL if it is not a part of the shortcut
DWORD GetWinKey(const ModifierKey& input) const;
@ -86,6 +162,9 @@ public:
// Function to set a key in the shortcut based on the passed key code argument. Returns false if it is already set to the same value. This can be used to avoid UI refreshing
bool SetKey(const DWORD input);
// Function to SetSecondKey
bool SetSecondKey(const DWORD input);
// Function to reset the state of a shortcut key based on the passed key code argument
void ResetKey(const DWORD input);

View File

@ -18,9 +18,29 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return base.GetMappedOriginalKeys();
}
public new List<string> GetMappedNewRemapKeys()
public new List<string> GetMappedOriginalKeysWithSplitChord()
{
return base.GetMappedNewRemapKeys();
return base.GetMappedOriginalKeysWithSplitChord();
}
public List<string> GetMappedOriginalKeys(bool ignoreSecondKeyInChord)
{
return base.GetMappedOriginalKeys(ignoreSecondKeyInChord);
}
public List<string> GetMappedOriginalKeysWithoutChord()
{
return base.GetMappedOriginalKeys(true);
}
public new List<string> GetMappedOriginalKeysOnlyChord()
{
return base.GetMappedOriginalKeysOnlyChord();
}
public new List<string> GetMappedNewRemapKeys(int runProgramMaxLength)
{
return base.GetMappedNewRemapKeys(runProgramMaxLength);
}
public bool Compare(AppSpecificKeysDataModel arg)

View File

@ -2,42 +2,312 @@
// 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.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Windows.Input;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class KeysDataModel
public class KeysDataModel : INotifyPropertyChanged
{
[JsonPropertyName("originalKeys")]
public string OriginalKeys { get; set; }
[JsonPropertyName("secondKeyOfChord")]
public int SecondKeyOfChord { get; set; }
[JsonPropertyName("newRemapKeys")]
public string NewRemapKeys { get; set; }
[JsonPropertyName("unicodeText")]
public string NewRemapString { get; set; }
[JsonPropertyName("runProgramFilePath")]
public string RunProgramFilePath { get; set; }
[JsonPropertyName("runProgramArgs")]
public string RunProgramArgs { get; set; }
[JsonPropertyName("openUri")]
public string OpenUri { get; set; }
[JsonPropertyName("operationType")]
public int OperationType { get; set; }
private enum KeyboardManagerEditorType
{
KeyEditor = 0,
ShortcutEditor,
}
public const string CommaSeparator = "<comma>";
private static Process editor;
private ICommand _editShortcutItemCommand;
public ICommand EditShortcutItem => _editShortcutItemCommand ?? (_editShortcutItemCommand = new RelayCommand<object>(OnEditShortcutItem));
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void OnEditShortcutItem(object parameter)
{
OpenEditor((int)KeyboardManagerEditorType.ShortcutEditor);
}
private async void OpenEditor(int type)
{
if (editor != null)
{
BringProcessToFront(editor);
return;
}
const string PowerToyName = KeyboardManagerSettings.ModuleName;
const string KeyboardManagerEditorPath = "KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe";
try
{
if (editor != null && editor.HasExited)
{
Logger.LogInfo($"Previous instance of {PowerToyName} editor exited");
editor = null;
}
if (editor != null)
{
Logger.LogInfo($"The {PowerToyName} editor instance {editor.Id} exists. Bringing the process to the front");
BringProcessToFront(editor);
return;
}
string path = Path.Combine(Environment.CurrentDirectory, KeyboardManagerEditorPath);
Logger.LogInfo($"Starting {PowerToyName} editor from {path}");
// InvariantCulture: type represents the KeyboardManagerEditorType enum value
editor = Process.Start(path, $"{type.ToString(CultureInfo.InvariantCulture)} {Environment.ProcessId}");
await editor.WaitForExitAsync();
editor = null;
}
catch (Exception e)
{
editor = null;
Logger.LogError($"Exception encountered when opening an {PowerToyName} editor", e);
}
}
private static void BringProcessToFront(Process process)
{
if (process == null)
{
return;
}
IntPtr handle = process.MainWindowHandle;
if (NativeMethods.IsIconic(handle))
{
NativeMethods.ShowWindow(handle, NativeMethods.SWRESTORE);
}
NativeMethods.SetForegroundWindow(handle);
}
private static List<string> MapKeysOnlyChord(int secondKeyOfChord)
{
var result = new List<string>();
if (secondKeyOfChord <= 0)
{
return result;
}
result.Add(Helper.GetKeyName((uint)secondKeyOfChord));
return result;
}
private static List<string> MapKeys(string stringOfKeys, int secondKeyOfChord, bool splitChordsWithComma = false)
{
if (stringOfKeys == null)
{
return new List<string>();
}
if (secondKeyOfChord > 0)
{
var keys = stringOfKeys.Split(';');
return keys.Take(keys.Length - 1)
.Select(uint.Parse)
.Select(Helper.GetKeyName)
.ToList();
}
else
{
if (splitChordsWithComma)
{
var keys = stringOfKeys.Split(';')
.Select(uint.Parse)
.Select(Helper.GetKeyName)
.ToList();
keys.Insert(keys.Count - 1, CommaSeparator);
return keys;
}
else
{
return stringOfKeys
.Split(';')
.Select(uint.Parse)
.Select(Helper.GetKeyName)
.ToList();
}
}
}
private static List<string> MapKeys(string stringOfKeys)
{
return stringOfKeys
.Split(';')
.Select(uint.Parse)
.Select(Helper.GetKeyName)
.ToList();
return MapKeys(stringOfKeys, 0);
}
public List<string> GetMappedOriginalKeys(bool ignoreSecondKeyInChord, bool splitChordsWithComma = false)
{
if (ignoreSecondKeyInChord && SecondKeyOfChord > 0)
{
return MapKeys(OriginalKeys, SecondKeyOfChord);
}
else
{
return MapKeys(OriginalKeys, -1, splitChordsWithComma);
}
}
public List<string> GetMappedOriginalKeysOnlyChord()
{
return MapKeysOnlyChord(SecondKeyOfChord);
}
public List<string> GetMappedOriginalKeys()
{
return MapKeys(OriginalKeys);
return GetMappedOriginalKeys(false);
}
public List<string> GetMappedNewRemapKeys()
public List<string> GetMappedOriginalKeysWithSplitChord()
{
return string.IsNullOrEmpty(NewRemapString) ? MapKeys(NewRemapKeys) : new List<string> { NewRemapString };
return GetMappedOriginalKeys(false, true);
}
public bool IsRunProgram
{
get
{
return OperationType == 1;
}
}
public bool IsOpenURI
{
get
{
return OperationType == 2;
}
}
public bool IsOpenUriOrIsRunProgram
{
get
{
return IsOpenURI || IsRunProgram;
}
}
public bool HasChord
{
get
{
return SecondKeyOfChord > 0;
}
}
public List<string> GetMappedNewRemapKeys(int runProgramMaxLength)
{
if (IsRunProgram)
{
// we're going to just pretend this is a "key" if we have a RunProgramFilePath
if (string.IsNullOrEmpty(RunProgramFilePath))
{
return new List<string>();
}
else
{
return new List<string> { FormatFakeKeyForDisplay(runProgramMaxLength) };
}
}
else if (IsOpenURI)
{
// we're going to just pretend this is a "key" if we have a RunProgramFilePath
if (string.IsNullOrEmpty(OpenUri))
{
return new List<string>();
}
else
{
if (OpenUri.Length > runProgramMaxLength)
{
return new List<string> { $"{OpenUri.Substring(0, runProgramMaxLength - 3)}..." };
}
else
{
return new List<string> { OpenUri };
}
}
}
return (string.IsNullOrEmpty(NewRemapString) || NewRemapString == "*Unsupported*") ? MapKeys(NewRemapKeys) : new List<string> { NewRemapString };
}
// Instead of doing something fancy pants, we 'll just display the RunProgramFilePath data when it's IsRunProgram
// It truncates the start of the program to run, if it's long and truncates the end of the args if it's long
// e.g.: c:\MyCool\PathIs\Long\software.exe myArg1 myArg2 myArg3 -> (something like) "...ng\software.exe myArg1..."
// the idea is you get the most important part of the program to run and some of the args in case that the only thing thats different,
// e.g: "...path\software.exe cool1.txt" and "...path\software.exe cool3.txt"
private string FormatFakeKeyForDisplay(int runProgramMaxLength)
{
// was going to use this:
var fakeKey = Environment.ExpandEnvironmentVariables(RunProgramFilePath);
try
{
if (File.Exists(fakeKey))
{
fakeKey = Path.GetFileName(Environment.ExpandEnvironmentVariables(RunProgramFilePath));
}
}
catch
{
}
fakeKey = $"{fakeKey} {RunProgramArgs}".Trim();
if (fakeKey.Length > runProgramMaxLength)
{
fakeKey = $"{fakeKey.Substring(0, runProgramMaxLength - 3)}...";
}
return fakeKey;
}
public string ToJsonString()

View File

@ -324,8 +324,24 @@ namespace Microsoft.PowerToys.Settings.UI.Library
// the settings file needs to be updated, update the real one with non-excluded stuff...
Logger.LogInfo($"Settings file {currentFile.Key} is different and is getting updated from backup");
var newCurrentSettingsFile = JsonMergeHelper.Merge(File.ReadAllText(currentSettingsFiles[currentFile.Key]), settingsToRestoreJson);
File.WriteAllText(currentSettingsFiles[currentFile.Key], newCurrentSettingsFile);
// we needed a new "CustomRestoreSettings" for now, to overwrite because some settings don't merge well (like KBM shortcuts)
var overwrite = false;
if (backupRestoreSettings["CustomRestoreSettings"] != null && backupRestoreSettings["CustomRestoreSettings"][currentFile.Key] != null)
{
var customRestoreSettings = backupRestoreSettings["CustomRestoreSettings"][currentFile.Key];
overwrite = customRestoreSettings["overwrite"] != null && (bool)customRestoreSettings["overwrite"];
}
if (overwrite)
{
File.WriteAllText(currentSettingsFiles[currentFile.Key], settingsToRestoreJson);
}
else
{
var newCurrentSettingsFile = JsonMergeHelper.Merge(File.ReadAllText(currentSettingsFiles[currentFile.Key]), settingsToRestoreJson);
File.WriteAllText(currentSettingsFiles[currentFile.Key], newCurrentSettingsFile);
}
anyFilesUpdated = true;
}
}

View File

@ -26,6 +26,11 @@
]
}
],
"CustomRestoreSettings": {
"\\Keyboard Manager\\default.json": {
"overwrite": true
}
},
"IgnoredSettings": {
"backup-restore_settings.json": [
"RestartAfterRestore"

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 Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Converters
{
internal sealed class KeyVisualTemplateSelector : DataTemplateSelector
{
public DataTemplate KeyVisualTemplate { get; set; }
public DataTemplate CommaTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
var stringValue = item as string;
return stringValue == KeysDataModel.CommaSeparator ? CommaTemplate : KeyVisualTemplate;
}
}
}

View File

@ -15,6 +15,26 @@
mc:Ignorable="d">
<Page.Resources>
<DataTemplate x:Key="KeyVisualTemplate">
<controls:KeyVisual
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Content="{Binding}"
IsTabStop="False"
VisualType="TextOnly" />
</DataTemplate>
<DataTemplate x:Key="CommaTemplate">
<StackPanel Background="{ThemeResource SystemFillColorSolidAttentionBackground}">
<TextBlock
Margin="4,0"
VerticalAlignment="Bottom"
Text="," />
</StackPanel>
</DataTemplate>
<converters:KeyVisualTemplateSelector
x:Key="KeyVisualTemplateSelector"
CommaTemplate="{StaticResource CommaTemplate}"
KeyVisualTemplate="{StaticResource KeyVisualTemplate}" />
<converters:ModuleItemTemplateSelector
x:Key="ModuleItemTemplateSelector"
ButtonTemplate="{StaticResource ModuleItemButtonTemplate}"
@ -156,7 +176,7 @@
<ItemsControl
AutomationProperties.AccessibilityView="Raw"
IsTabStop="False"
ItemsSource="{x:Bind GetMappedNewRemapKeys()}">
ItemsSource="{x:Bind GetMappedNewRemapKeys(15)}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="12" />
@ -190,7 +210,7 @@
<DataTemplate x:DataType="Lib:AppSpecificKeysDataModel">
<StackPanel Orientation="Horizontal">
<Border
Padding="8,4"
Padding="8,0"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
@ -198,29 +218,27 @@
<ItemsControl
AutomationProperties.AccessibilityView="Raw"
IsTabStop="False"
ItemsSource="{x:Bind GetMappedOriginalKeys()}">
ItemTemplateSelector="{StaticResource KeyVisualTemplateSelector}"
ItemsSource="{x:Bind GetMappedOriginalKeysWithSplitChord()}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="12" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:KeyVisual
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Content="{Binding}"
IsTabStop="False"
VisualType="TextOnly" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<controls:IsEnabledTextBlock
x:Uid="To"
Margin="8,0,8,0"
VerticalAlignment="Center"
Style="{StaticResource SecondaryIsEnabledTextBlockStyle}" />
Style="{StaticResource SecondaryIsEnabledTextBlockStyle}"
Visibility="{x:Bind Path=IsOpenUriOrIsRunProgram, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
<controls:IsEnabledTextBlock
x:Uid="Starts"
Margin="8,0,8,0"
VerticalAlignment="Center"
Style="{StaticResource SecondaryIsEnabledTextBlockStyle}"
Visibility="{x:Bind Path=IsOpenUriOrIsRunProgram, Mode=OneWay}" />
<Border
Padding="8,4"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
@ -230,7 +248,7 @@
<ItemsControl
AutomationProperties.AccessibilityView="Raw"
IsTabStop="False"
ItemsSource="{x:Bind GetMappedNewRemapKeys()}">
ItemsSource="{x:Bind GetMappedNewRemapKeys(15)}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="12" />

View File

@ -14,6 +14,12 @@
<Page.Resources>
<tkconverters:CollectionVisibilityConverter x:Key="CollectionVisibilityConverter" />
<tkconverters:BoolToVisibilityConverter
x:Key="BoolToInvertedVisibilityConverter"
FalseValue="Visible"
TrueValue="Collapsed" />
<tkconverters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<Style x:Name="KeysListViewContainerStyle" TargetType="ListViewItem">
<Setter Property="IsTabStop" Value="False" />
</Style>
@ -105,7 +111,7 @@
x:Uid="KeyboardManager_RemappedTo"
IsTabStop="False"
ItemTemplate="{StaticResource RemappedKeyTemplate}"
ItemsSource="{x:Bind GetMappedNewRemapKeys()}">
ItemsSource="{x:Bind GetMappedNewRemapKeys(50)}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
@ -124,7 +130,7 @@
x:Uid="KeyboardManager_RemapShortcutsButton"
ActionIcon="{ui:FontIcon Glyph=&#xE8A7;}"
Command="{Binding Path=EditShortcutCommand}"
HeaderIcon="{ui:FontIcon Glyph=&#xE92E;}"
HeaderIcon="{ui:FontIcon Glyph=&#xE713;}"
IsClickEnabled="True" />
<ListView
@ -136,47 +142,79 @@
Visibility="{x:Bind Path=ViewModel.RemapShortcuts, Mode=OneWay, Converter={StaticResource CollectionVisibilityConverter}}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="Lib:AppSpecificKeysDataModel">
<tkcontrols:SettingsCard ContentAlignment="Left">
<StackPanel Orientation="Horizontal">
<ItemsControl
IsTabStop="False"
ItemTemplate="{StaticResource OriginalKeyTemplate}"
ItemsSource="{x:Bind GetMappedOriginalKeys()}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<controls:IsEnabledTextBlock
x:Uid="To"
Margin="8,0,8,0"
VerticalAlignment="Center"
Style="{StaticResource SecondaryIsEnabledTextBlockStyle}" />
<ItemsControl
Name="KeyboardManager_RemappedTo"
x:Uid="KeyboardManager_RemappedTo"
IsTabStop="False"
ItemTemplate="{StaticResource RemappedKeyTemplate}"
ItemsSource="{x:Bind GetMappedNewRemapKeys()}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<Border
Margin="16,0,0,0"
Padding="12,4,12,6"
VerticalAlignment="Center"
CornerRadius="12">
<Border.Background>
<SolidColorBrush Opacity="0.3" Color="{ThemeResource SystemAccentColor}" />
</Border.Background>
<TextBlock Text="{x:Bind TargetApp}" />
</Border>
</StackPanel>
<tkcontrols:SettingsCard>
<tkcontrols:SettingsCard.Description>
<StackPanel Orientation="Horizontal">
<ItemsControl
IsTabStop="False"
ItemTemplate="{StaticResource OriginalKeyTemplate}"
ItemsSource="{x:Bind GetMappedOriginalKeysWithoutChord()}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<TextBlock
Padding="6,0,6,6"
VerticalAlignment="Bottom"
Text=","
Visibility="{x:Bind Path=HasChord, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<ItemsControl
IsTabStop="False"
ItemTemplate="{StaticResource OriginalKeyTemplate}"
ItemsSource="{x:Bind GetMappedOriginalKeysOnlyChord()}"
Visibility="{x:Bind Path=HasChord, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<controls:IsEnabledTextBlock
x:Uid="To"
Margin="8,0,8,0"
VerticalAlignment="Center"
Style="{StaticResource SecondaryIsEnabledTextBlockStyle}"
Visibility="{x:Bind Path=IsOpenUriOrIsRunProgram, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
<controls:IsEnabledTextBlock
x:Uid="Starts"
Margin="8,0,8,0"
VerticalAlignment="Center"
Style="{StaticResource SecondaryIsEnabledTextBlockStyle}"
Visibility="{x:Bind Path=IsOpenUriOrIsRunProgram, Mode=OneWay}" />
<ItemsControl
Name="KeyboardManager_RemappedTo"
x:Uid="KeyboardManager_RemappedTo"
IsTabStop="False"
ItemTemplate="{StaticResource RemappedKeyTemplate}"
ItemsSource="{x:Bind GetMappedNewRemapKeys(50)}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<Border
Margin="16,0,0,0"
Padding="12,4,12,6"
VerticalAlignment="Center"
CornerRadius="12">
<Border.Background>
<SolidColorBrush Opacity="0.3" Color="{ThemeResource SystemAccentColor}" />
</Border.Background>
<TextBlock Text="{x:Bind TargetApp}" />
</Border>
</StackPanel>
</tkcontrols:SettingsCard.Description>
</tkcontrols:SettingsCard>
</DataTemplate>
</ListView.ItemTemplate>

View File

@ -2367,6 +2367,10 @@ From there, simply click on one of the supported files in the File Explorer and
<value>to</value>
<comment>as in: from x to y</comment>
</data>
<data name="Starts.Text" xml:space="preserve">
<value>starts</value>
<comment>as in: doing x will start y</comment>
</data>
<data name="LearnMore_Awake.Text" xml:space="preserve">
<value>Learn more about Awake</value>
<comment>Awake is a product name, do not loc</comment>

View File

@ -190,11 +190,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
else if (appSpecificShortcutList == null)
{
return globalShortcutList.ConvertAll(x => new AppSpecificKeysDataModel { OriginalKeys = x.OriginalKeys, NewRemapKeys = x.NewRemapKeys, NewRemapString = x.NewRemapString, TargetApp = allAppsDescription }).ToList();
return globalShortcutList.ConvertAll(x => new AppSpecificKeysDataModel { OriginalKeys = x.OriginalKeys, NewRemapKeys = x.NewRemapKeys, NewRemapString = x.NewRemapString, RunProgramFilePath = x.RunProgramFilePath, OperationType = x.OperationType, OpenUri = x.OpenUri, SecondKeyOfChord = x.SecondKeyOfChord, RunProgramArgs = x.RunProgramArgs, TargetApp = allAppsDescription }).ToList();
}
else
{
return globalShortcutList.ConvertAll(x => new AppSpecificKeysDataModel { OriginalKeys = x.OriginalKeys, NewRemapKeys = x.NewRemapKeys, NewRemapString = x.NewRemapString, TargetApp = allAppsDescription }).Concat(appSpecificShortcutList).ToList();
return globalShortcutList.ConvertAll(x => new AppSpecificKeysDataModel { OriginalKeys = x.OriginalKeys, NewRemapKeys = x.NewRemapKeys, NewRemapString = x.NewRemapString, RunProgramFilePath = x.RunProgramFilePath, OperationType = x.OperationType, OpenUri = x.OpenUri, SecondKeyOfChord = x.SecondKeyOfChord, RunProgramArgs = x.RunProgramArgs, TargetApp = allAppsDescription }).Concat(appSpecificShortcutList).ToList();
}
}

View File

@ -29,7 +29,18 @@ map<wstring, vector<wstring>> escapeInfo = {
{ L"FancyZones\\app-zone-history.json", { L"app-zone-history/app-path" } },
{ L"FancyZones\\settings.json", { L"properties/fancyzones_excluded_apps" } },
{ L"MouseWithoutBorders\\settings.json", { L"properties/SecurityKey" } }, // avoid leaking connection key
{ L"Keyboard Manager\\default.json", { L"remapKeysToText", L"remapShortcutsToText" } }, // avoid leaking personal information from text mappings
{ L"Keyboard Manager\\default.json", {
L"remapKeysToText",
L"remapShortcutsToText",
L"remapShortcuts/global/runProgramFilePath",
L"remapShortcuts/global/runProgramArgs",
L"remapShortcuts/global/runProgramStartInDir",
L"remapShortcuts/global/openUri",
L"remapShortcuts/appSpecific/runProgramFilePath",
L"remapShortcuts/appSpecific/runProgramArgs",
L"remapShortcuts/appSpecific/runProgramStartInDir",
L"remapShortcuts/appSpecific/openUri",
} }, // avoid leaking personal information from text, URI or application mappings
};
vector<wstring> filesToDelete = {