Introduce vk() and sc() key chord specifiers (#10666)

This commit introduces an alternative to specifying key bindings as a combination of key modifiers and a character. It allows you to specify an explicit virtual key as `vk(nnn)`.
Additionally this commit makes it possible to bind actions to scan codes. As scan code 41 appears to be the button below the Escape key on virtually all keyboards, we'll be able to bind the quake mode hotkey to `win+sc(41)` and have it work consistently across most if not all keyboard layouts.

## PR Checklist
* [x] Closes #7539, Closes #10203
* [x] I work here
* [x] Tests added/passed

## Validation Steps Performed

The following was tested both on US and DE keyboard layouts:
* Ctrl+, opens settings ✔️
* Win+` opens quake mode window ✔️
* Ctrl+plus/minus increase/decrease font size ✔️
This commit is contained in:
Leonard Hecker 2021-07-21 00:34:51 +02:00 committed by GitHub
parent 6ce2543a94
commit 10b12ac90c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 546 additions and 337 deletions

View File

@ -1,10 +1,10 @@
Apc
apc
Apc
clickable
copyable
dalet
Dcs
dcs
Dcs
dialytika
dje
downside
@ -52,6 +52,7 @@ tonos
tshe
UIs
und
unregister
versioned
We'd
wildcards

View File

@ -63,6 +63,7 @@ IObject
iosfwd
IPackage
IPeasant
isspace
IStorage
istream
IStringable
@ -133,6 +134,7 @@ STDCPP
STDMETHOD
strchr
streambuf
strtoul
Stubless
Subheader
Subpage
@ -151,6 +153,7 @@ userenv
wcsstr
wcstoui
winmain
wmemcmp
wpc
wsregex
wwinmain

View File

@ -1502,6 +1502,7 @@ nfe
nlength
Nls
NLSMODE
nnn
NOACTIVATE
NOAPPLYNOW
NOCLIP
@ -1582,6 +1583,7 @@ NTVDM
ntverp
NTWIN
nuget
nullability
nullness
nullonfailure
nullopt

View File

@ -4,13 +4,13 @@
"title": "Microsoft's Windows Terminal Settings Profile Schema",
"definitions": {
"KeyChordSegment": {
"pattern": "^(?<modifier>(?<mod1>ctrl|alt|shift|win)(?:\\+(?<mod2>ctrl|alt|shift|win)(?<!\\k<mod1>))?(?:\\+(?<mod3>ctrl|alt|shift|win)(?<!\\k<mod1>|\\k<mod2>))?(?:\\+(?<mod4>ctrl|alt|shift|win)(?<!\\k<mod1>|\\k<mod2>|\\k<mod3>))?\\+)?(?<key>[^\\s+]|app|menu|backspace|tab|enter|esc|escape|space|pgup|pageup|pgdn|pagedown|end|home|left|up|right|down|insert|delete|(?<!shift.+)(?:numpad_?[0-9]|numpad_(?:period|decimal))|numpad_(?:multiply|plus|add|minus|subtract|divide)|f[1-9]|f1[0-9]|f2[0-4]|plus)$",
"pattern": "^(?:(?:ctrl|alt|shift|win)\\+)*(?:app|backspace|comma|delete|down|end|enter|esc|escape|home|insert|left|menu|minus|pagedown|pageup|period|pgdn|pgup|plus|right|space|tab|up|f(?:1\\d?|2[0-4]?|[3-9])|numpad\\d|numpad_(?:\\d|add|decimal|divide|minus|multiply|period|plus|subtract)|(?:vk|sc)\\((?:[1-9]|1?\\d{2}|2[0-4]\\d|25[0-5])\\)|[^\\s+])(?:\\+(?:ctrl|alt|shift|win))*$",
"type": "string",
"description": "The string should fit the format \"[ctrl+][alt+][shift+][win+]<keyName>\", where each modifier is optional, separated by + symbols, and keyName is either one of the names listed in the table below, or any single key character. The string should be written in full lowercase.\napp, menu\tMENU key\nbackspace\tBACKSPACE key\ntab\tTAB key\nenter\tENTER key\nesc, escape\tESC key\nspace\tSPACEBAR\npgup, pageup\tPAGE UP key\npgdn, pagedown\tPAGE DOWN key\nend\tEND key\nhome\tHOME key\nleft\tLEFT ARROW key\nup\tUP ARROW key\nright\tRIGHT ARROW key\ndown\tDOWN ARROW key\ninsert\tINS key\ndelete\tDEL key\nnumpad_0-numpad_9, numpad0-numpad9\tNumeric keypad keys 0 to 9. Can't be combined with the shift modifier.\nnumpad_multiply\tNumeric keypad MULTIPLY key (*)\nnumpad_plus, numpad_add\tNumeric keypad ADD key (+)\nnumpad_minus, numpad_subtract\tNumeric keypad SUBTRACT key (-)\nnumpad_period, numpad_decimal\tNumeric keypad DECIMAL key (.). Can't be combined with the shift modifier.\nnumpad_divide\tNumeric keypad DIVIDE key (/)\nf1-f24\tF1 to F24 function keys\nplus\tADD key (+)"
"description": "The string should fit the format \"[ctrl+][alt+][shift+][win+]<KeyName>\", where each modifier is optional. KeyName is either any single key character, an explicit virtual key or scan code in the form vk(nnn) and sc(nnn) respectively, or one of the special names listed at https://docs.microsoft.com/en-us/windows/terminal/customize-settings/actions#accepted-modifiers-and-keys"
},
"Color": {
"default": "#",
"pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$",
"pattern": "^#[A-Fa-f0-9]{3}(?:[A-Fa-f0-9]{3})?$",
"type": "string",
"format": "color"
},
@ -596,7 +596,7 @@
"defaultsFile",
"allFiles",
"settingsUI"
]
}
}

View File

@ -1971,9 +1971,9 @@ namespace SettingsModelLocalTests
auto settings = implementation::CascadiaSettings::FromJson(settingsObject);
VERIFY_ARE_EQUAL(3u, settings->_globals->_actionMap->_KeyMap.size());
VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('a') }));
VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('b') }));
VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('c') }));
VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('A'), 0 }));
VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('B'), 0 }));
VERIFY_IS_NULL(settings->_globals->_actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('C'), 0 }));
for (const auto& warning : settings->_globals->_keybindingsWarnings)
{
@ -2124,7 +2124,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(1u, nameMap.Size());
{
KeyChord kc{ true, false, false, static_cast<int32_t>('A') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('A'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
@ -2141,7 +2141,7 @@ namespace SettingsModelLocalTests
Log::Comment(L"Note that we're skipping ctrl+B, since that doesn't have `keys` set.");
{
KeyChord kc{ true, false, false, static_cast<int32_t>('C') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('C'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
@ -2155,7 +2155,7 @@ namespace SettingsModelLocalTests
VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('D') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('D'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
@ -2169,7 +2169,7 @@ namespace SettingsModelLocalTests
VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('E') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('E'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
@ -2183,7 +2183,7 @@ namespace SettingsModelLocalTests
VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('F') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('F'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
@ -2841,7 +2841,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(0u, settings->_warnings.Size());
VERIFY_ARE_EQUAL(1u, nameMap.Size());
const KeyChord expectedKeyChord{ true, false, true, static_cast<int>('W') };
const KeyChord expectedKeyChord{ true, false, true, false, static_cast<int>('W'), 0 };
{
// Verify NameMap returns correct value
const auto& cmd{ nameMap.TryLookup(L"foo") };

View File

@ -36,6 +36,7 @@ namespace SettingsModelLocalTests
TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml")
END_TEST_CLASS()
TEST_METHOD(KeyChords);
TEST_METHOD(ManyKeysSameAction);
TEST_METHOD(LayerKeybindings);
TEST_METHOD(UnbindKeybindings);
@ -61,6 +62,59 @@ namespace SettingsModelLocalTests
}
};
void KeyBindingsTests::KeyChords()
{
struct testCase
{
VirtualKeyModifiers modifiers;
int32_t vkey;
int32_t scanCode;
std::wstring_view expected;
};
static constexpr std::array testCases{
testCase{
VirtualKeyModifiers::None,
'A',
0,
L"a",
},
testCase{
VirtualKeyModifiers::Control,
'A',
0,
L"ctrl+a",
},
testCase{
VirtualKeyModifiers::Control | VirtualKeyModifiers::Shift,
VK_OEM_PLUS,
0,
L"ctrl+shift+plus",
},
testCase{
VirtualKeyModifiers::Control | VirtualKeyModifiers::Menu | VirtualKeyModifiers::Shift | VirtualKeyModifiers::Windows,
255,
0,
L"ctrl+shift+alt+win+vk(255)",
},
testCase{
VirtualKeyModifiers::Windows,
0,
123,
L"ctrl+shift+alt+win+sc(123)",
},
};
for (const auto& tc : testCases)
{
KeyChord expectedKeyChord{ tc.modifiers, tc.vkey, tc.scanCode };
const auto actualString = KeyChordSerialization::ToString(expectedKeyChord);
VERIFY_ARE_EQUAL(tc.expected, actualString);
const auto actualKeyChord = KeyChordSerialization::FromString(actualString);
VERIFY_ARE_EQUAL(expectedKeyChord, actualKeyChord);
}
}
void KeyBindingsTests::ManyKeysSameAction()
{
const std::string bindings0String{ R"([ { "command": "copy", "keys": ["ctrl+c"] } ])" };
@ -75,7 +129,6 @@ namespace SettingsModelLocalTests
const auto bindings2Json = VerifyParseSucceeded(bindings2String);
auto actionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_NOT_NULL(actionMap);
VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size());
actionMap->LayerJson(bindings0Json);
@ -99,7 +152,6 @@ namespace SettingsModelLocalTests
const auto bindings2Json = VerifyParseSucceeded(bindings2String);
auto actionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_NOT_NULL(actionMap);
VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size());
actionMap->LayerJson(bindings0Json);
@ -129,7 +181,6 @@ namespace SettingsModelLocalTests
const auto bindings5Json = VerifyParseSucceeded(bindings5String);
auto actionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_NOT_NULL(actionMap);
VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size());
actionMap->LayerJson(bindings0Json);
@ -142,7 +193,7 @@ namespace SettingsModelLocalTests
L"Try unbinding a key using `\"unbound\"` to unbind the key"));
actionMap->LayerJson(bindings2Json);
VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size());
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('c') }));
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('C'), 0 }));
Log::Comment(NoThrowString().Format(
L"Try unbinding a key using `null` to unbind the key"));
@ -152,7 +203,7 @@ namespace SettingsModelLocalTests
// Then try layering in the bad setting
actionMap->LayerJson(bindings3Json);
VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size());
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('c') }));
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('C'), 0 }));
Log::Comment(NoThrowString().Format(
L"Try unbinding a key using an unrecognized command to unbind the key"));
@ -162,7 +213,7 @@ namespace SettingsModelLocalTests
// Then try layering in the bad setting
actionMap->LayerJson(bindings4Json);
VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size());
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('c') }));
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('C'), 0 }));
Log::Comment(NoThrowString().Format(
L"Try unbinding a key using a straight up invalid value to unbind the key"));
@ -172,13 +223,13 @@ namespace SettingsModelLocalTests
// Then try layering in the bad setting
actionMap->LayerJson(bindings5Json);
VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size());
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('c') }));
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('C'), 0 }));
Log::Comment(NoThrowString().Format(
L"Try unbinding a key that wasn't bound at all"));
actionMap->LayerJson(bindings2Json);
VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size());
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('c') }));
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('C'), 0 }));
}
void KeyBindingsTests::TestArbitraryArgs()
@ -203,7 +254,6 @@ namespace SettingsModelLocalTests
const auto bindings0Json = VerifyParseSucceeded(bindings0String);
auto actionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_NOT_NULL(actionMap);
VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size());
actionMap->LayerJson(bindings0Json);
VERIFY_ARE_EQUAL(10u, actionMap->_KeyMap.size());
@ -211,10 +261,9 @@ namespace SettingsModelLocalTests
{
Log::Comment(NoThrowString().Format(
L"Verify that `copy` without args parses as Copy(SingleLine=false)"));
KeyChord kc{ true, false, false, static_cast<int32_t>('C') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('C'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
const auto& realArgs = actionAndArgs.Args().try_as<CopyTextArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<CopyTextArgs>();
// Verify the args have the expected value
VERIFY_IS_FALSE(realArgs.SingleLine());
}
@ -222,10 +271,9 @@ namespace SettingsModelLocalTests
{
Log::Comment(NoThrowString().Format(
L"Verify that `copy` with args parses them correctly"));
KeyChord kc{ true, false, true, static_cast<int32_t>('C') };
KeyChord kc{ true, false, true, false, static_cast<int32_t>('C'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
const auto& realArgs = actionAndArgs.Args().try_as<CopyTextArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<CopyTextArgs>();
// Verify the args have the expected value
VERIFY_IS_FALSE(realArgs.SingleLine());
}
@ -233,10 +281,9 @@ namespace SettingsModelLocalTests
{
Log::Comment(NoThrowString().Format(
L"Verify that `copy` with args parses them correctly"));
KeyChord kc{ false, true, true, static_cast<int32_t>('C') };
KeyChord kc{ false, true, true, false, static_cast<int32_t>('C'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
const auto& realArgs = actionAndArgs.Args().try_as<CopyTextArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<CopyTextArgs>();
// Verify the args have the expected value
VERIFY_IS_TRUE(realArgs.SingleLine());
}
@ -244,11 +291,10 @@ namespace SettingsModelLocalTests
{
Log::Comment(NoThrowString().Format(
L"Verify that `newTab` without args parses as NewTab(Index=null)"));
KeyChord kc{ true, false, false, static_cast<int32_t>('T') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('T'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<NewTabArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<NewTabArgs>();
// Verify the args have the expected value
VERIFY_IS_NOT_NULL(realArgs.TerminalArgs());
VERIFY_IS_NULL(realArgs.TerminalArgs().ProfileIndex());
@ -256,11 +302,10 @@ namespace SettingsModelLocalTests
{
Log::Comment(NoThrowString().Format(
L"Verify that `newTab` parses args correctly"));
KeyChord kc{ true, false, true, static_cast<int32_t>('T') };
KeyChord kc{ true, false, true, false, static_cast<int32_t>('T'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<NewTabArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<NewTabArgs>();
// Verify the args have the expected value
VERIFY_IS_NOT_NULL(realArgs.TerminalArgs());
VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().ProfileIndex());
@ -270,11 +315,10 @@ namespace SettingsModelLocalTests
Log::Comment(NoThrowString().Format(
L"Verify that `newTab` with an index greater than the legacy "
L"args afforded parses correctly"));
KeyChord kc{ true, false, true, static_cast<int32_t>('Y') };
KeyChord kc{ true, false, true, false, static_cast<int32_t>('Y'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<NewTabArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<NewTabArgs>();
// Verify the args have the expected value
VERIFY_IS_NOT_NULL(realArgs.TerminalArgs());
VERIFY_IS_NOT_NULL(realArgs.TerminalArgs().ProfileIndex());
@ -284,11 +328,10 @@ namespace SettingsModelLocalTests
{
Log::Comment(NoThrowString().Format(
L"Verify that `copy` ignores args it doesn't understand"));
KeyChord kc{ true, false, true, static_cast<int32_t>('B') };
KeyChord kc{ true, false, true, false, static_cast<int32_t>('B'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::CopyText, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<CopyTextArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<CopyTextArgs>();
// Verify the args have the expected value
VERIFY_IS_FALSE(realArgs.SingleLine());
}
@ -296,11 +339,10 @@ namespace SettingsModelLocalTests
{
Log::Comment(NoThrowString().Format(
L"Verify that `copy` null as it's `args` parses as the default option"));
KeyChord kc{ true, false, true, static_cast<int32_t>('B') };
KeyChord kc{ true, false, true, false, static_cast<int32_t>('B'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::CopyText, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<CopyTextArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<CopyTextArgs>();
// Verify the args have the expected value
VERIFY_IS_FALSE(realArgs.SingleLine());
}
@ -308,11 +350,10 @@ namespace SettingsModelLocalTests
{
Log::Comment(NoThrowString().Format(
L"Verify that `adjustFontSize` with a positive delta parses args correctly"));
KeyChord kc{ true, false, false, static_cast<int32_t>('F') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('F'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::AdjustFontSize, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<AdjustFontSizeArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<AdjustFontSizeArgs>();
// Verify the args have the expected value
VERIFY_ARE_EQUAL(1, realArgs.Delta());
}
@ -320,11 +361,10 @@ namespace SettingsModelLocalTests
{
Log::Comment(NoThrowString().Format(
L"Verify that `adjustFontSize` with a negative delta parses args correctly"));
KeyChord kc{ true, false, false, static_cast<int32_t>('G') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('G'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::AdjustFontSize, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<AdjustFontSizeArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<AdjustFontSizeArgs>();
// Verify the args have the expected value
VERIFY_ARE_EQUAL(-1, realArgs.Delta());
}
@ -342,44 +382,39 @@ namespace SettingsModelLocalTests
const auto bindings0Json = VerifyParseSucceeded(bindings0String);
auto actionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_NOT_NULL(actionMap);
VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size());
actionMap->LayerJson(bindings0Json);
VERIFY_ARE_EQUAL(4u, actionMap->_KeyMap.size());
{
KeyChord kc{ true, false, false, static_cast<int32_t>('D') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('D'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<SplitPaneArgs>();
// Verify the args have the expected value
VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('E') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('E'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<SplitPaneArgs>();
// Verify the args have the expected value
VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('G') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('G'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<SplitPaneArgs>();
// Verify the args have the expected value
VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('H') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('H'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<SplitPaneArgs>();
// Verify the args have the expected value
VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle());
}
@ -396,37 +431,33 @@ namespace SettingsModelLocalTests
const auto bindings0Json = VerifyParseSucceeded(bindings0String);
auto actionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_NOT_NULL(actionMap);
VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size());
actionMap->LayerJson(bindings0Json);
VERIFY_ARE_EQUAL(3u, actionMap->_KeyMap.size());
{
KeyChord kc{ true, false, false, static_cast<int32_t>('C') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('C'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SetTabColor, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SetTabColorArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<SetTabColorArgs>();
// Verify the args have the expected value
VERIFY_IS_NULL(realArgs.TabColor());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('D') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('D'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SetTabColor, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SetTabColorArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<SetTabColorArgs>();
// Verify the args have the expected value
VERIFY_IS_NOT_NULL(realArgs.TabColor());
// Remember that COLORREFs are actually BBGGRR order, while the string is in #RRGGBB order
VERIFY_ARE_EQUAL(til::color(0x563412), til::color(realArgs.TabColor().Value()));
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('F') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('F'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SetTabColor, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SetTabColorArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<SetTabColorArgs>();
// Verify the args have the expected value
VERIFY_IS_NULL(realArgs.TabColor());
}
@ -441,16 +472,14 @@ namespace SettingsModelLocalTests
const auto bindings0Json = VerifyParseSucceeded(bindings0String);
auto actionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_NOT_NULL(actionMap);
VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size());
actionMap->LayerJson(bindings0Json);
VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size());
{
KeyChord kc{ true, false, false, static_cast<int32_t>('C') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('C'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
const auto& realArgs = actionAndArgs.Args().try_as<CopyTextArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<CopyTextArgs>();
// Verify the args have the expected value
VERIFY_IS_FALSE(realArgs.SingleLine());
}
@ -470,63 +499,56 @@ namespace SettingsModelLocalTests
const auto bindings0Json = VerifyParseSucceeded(bindings0String);
auto actionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_NOT_NULL(actionMap);
VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size());
actionMap->LayerJson(bindings0Json);
VERIFY_ARE_EQUAL(6u, actionMap->_KeyMap.size());
{
KeyChord kc{ false, false, false, static_cast<int32_t>(VK_UP) };
KeyChord kc{ false, false, false, false, static_cast<int32_t>(VK_UP), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<ScrollUpArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<ScrollUpArgs>();
// Verify the args have the expected value
VERIFY_IS_NULL(realArgs.RowsToScroll());
}
{
KeyChord kc{ false, false, false, static_cast<int32_t>(VK_DOWN) };
KeyChord kc{ false, false, false, false, static_cast<int32_t>(VK_DOWN), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<ScrollDownArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<ScrollDownArgs>();
// Verify the args have the expected value
VERIFY_IS_NULL(realArgs.RowsToScroll());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>(VK_UP) };
KeyChord kc{ true, false, false, false, static_cast<int32_t>(VK_UP), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<ScrollUpArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<ScrollUpArgs>();
// Verify the args have the expected value
VERIFY_IS_NULL(realArgs.RowsToScroll());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>(VK_DOWN) };
KeyChord kc{ true, false, false, false, static_cast<int32_t>(VK_DOWN), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<ScrollDownArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<ScrollDownArgs>();
// Verify the args have the expected value
VERIFY_IS_NULL(realArgs.RowsToScroll());
}
{
KeyChord kc{ true, false, true, static_cast<int32_t>(VK_UP) };
KeyChord kc{ true, false, true, false, static_cast<int32_t>(VK_UP), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::ScrollUp, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<ScrollUpArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<ScrollUpArgs>();
// Verify the args have the expected value
VERIFY_IS_NOT_NULL(realArgs.RowsToScroll());
VERIFY_ARE_EQUAL(10u, realArgs.RowsToScroll().Value());
}
{
KeyChord kc{ true, false, true, static_cast<int32_t>(VK_DOWN) };
KeyChord kc{ true, false, true, false, static_cast<int32_t>(VK_DOWN), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::ScrollDown, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<ScrollDownArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<ScrollDownArgs>();
// Verify the args have the expected value
VERIFY_IS_NOT_NULL(realArgs.RowsToScroll());
VERIFY_ARE_EQUAL(10u, realArgs.RowsToScroll().Value());
@ -535,7 +557,6 @@ namespace SettingsModelLocalTests
const std::string bindingsInvalidString{ R"([{ "keys": ["up"], "command": { "action": "scrollDown", "rowsToScroll": -1 } }])" };
const auto bindingsInvalidJson = VerifyParseSucceeded(bindingsInvalidString);
auto invalidActionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_NOT_NULL(invalidActionMap);
VERIFY_ARE_EQUAL(0u, invalidActionMap->_KeyMap.size());
VERIFY_THROWS(invalidActionMap->LayerJson(bindingsInvalidJson);, std::exception);
}
@ -551,26 +572,23 @@ namespace SettingsModelLocalTests
const auto bindings0Json = VerifyParseSucceeded(bindings0String);
auto actionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_NOT_NULL(actionMap);
VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size());
actionMap->LayerJson(bindings0Json);
VERIFY_ARE_EQUAL(2u, actionMap->_KeyMap.size());
{
KeyChord kc{ false, false, false, static_cast<int32_t>(VK_UP) };
KeyChord kc{ false, false, false, false, static_cast<int32_t>(VK_UP), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::MoveTab, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<MoveTabArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<MoveTabArgs>();
// Verify the args have the expected value
VERIFY_ARE_EQUAL(realArgs.Direction(), MoveTabDirection::Forward);
}
{
KeyChord kc{ false, false, false, static_cast<int32_t>(VK_DOWN) };
KeyChord kc{ false, false, false, false, static_cast<int32_t>(VK_DOWN), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::MoveTab, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<MoveTabArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<MoveTabArgs>();
// Verify the args have the expected value
VERIFY_ARE_EQUAL(realArgs.Direction(), MoveTabDirection::Backward);
}
@ -584,7 +602,6 @@ namespace SettingsModelLocalTests
const std::string bindingsInvalidString{ R"([{ "keys": ["up"], "command": { "action": "moveTab", "direction": "bad" } }])" };
const auto bindingsInvalidJson = VerifyParseSucceeded(bindingsInvalidString);
auto invalidActionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_NOT_NULL(invalidActionMap);
VERIFY_ARE_EQUAL(0u, invalidActionMap->_KeyMap.size());
VERIFY_THROWS(invalidActionMap->LayerJson(bindingsInvalidJson);, std::exception);
}
@ -601,35 +618,31 @@ namespace SettingsModelLocalTests
const auto bindings0Json = VerifyParseSucceeded(bindings0String);
auto actionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_NOT_NULL(actionMap);
VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size());
actionMap->LayerJson(bindings0Json);
VERIFY_ARE_EQUAL(3u, actionMap->_KeyMap.size());
{
KeyChord kc{ false, false, false, static_cast<int32_t>(VK_UP) };
KeyChord kc{ false, false, false, false, static_cast<int32_t>(VK_UP), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::ToggleCommandPalette, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<ToggleCommandPaletteArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<ToggleCommandPaletteArgs>();
// Verify the args have the expected value
VERIFY_ARE_EQUAL(realArgs.LaunchMode(), CommandPaletteLaunchMode::Action);
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>(VK_UP) };
KeyChord kc{ true, false, false, false, static_cast<int32_t>(VK_UP), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::ToggleCommandPalette, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<ToggleCommandPaletteArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<ToggleCommandPaletteArgs>();
// Verify the args have the expected value
VERIFY_ARE_EQUAL(realArgs.LaunchMode(), CommandPaletteLaunchMode::Action);
}
{
KeyChord kc{ true, false, true, static_cast<int32_t>(VK_UP) };
KeyChord kc{ true, false, true, false, static_cast<int32_t>(VK_UP), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::ToggleCommandPalette, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<ToggleCommandPaletteArgs>();
VERIFY_IS_NOT_NULL(realArgs);
const auto& realArgs = actionAndArgs.Args().as<ToggleCommandPaletteArgs>();
// Verify the args have the expected value
VERIFY_ARE_EQUAL(realArgs.LaunchMode(), CommandPaletteLaunchMode::CommandLine);
}
@ -637,7 +650,6 @@ namespace SettingsModelLocalTests
const std::string bindingsInvalidString{ R"([{ "keys": ["up"], "command": { "action": "commandPalette", "launchMode": "bad" } }])" };
const auto bindingsInvalidJson = VerifyParseSucceeded(bindingsInvalidString);
auto invalidActionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_NOT_NULL(invalidActionMap);
VERIFY_ARE_EQUAL(0u, invalidActionMap->_KeyMap.size());
VERIFY_THROWS(invalidActionMap->LayerJson(bindingsInvalidJson);, std::exception);
}
@ -669,7 +681,6 @@ namespace SettingsModelLocalTests
};
auto actionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_NOT_NULL(actionMap);
VERIFY_ARE_EQUAL(0u, actionMap->_KeyMap.size());
{
@ -677,7 +688,7 @@ namespace SettingsModelLocalTests
actionMap->LayerJson(bindings0Json);
VERIFY_ARE_EQUAL(1u, actionMap->_KeyMap.size());
const auto& kbd{ actionMap->GetKeyBindingForAction(ShortcutAction::CloseWindow) };
VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast<int32_t>('A') }, kbd);
VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast<int32_t>('A'), 0 }, kbd);
}
{
Log::Comment(L"command with args");
@ -688,7 +699,7 @@ namespace SettingsModelLocalTests
args->SingleLine(true);
const auto& kbd{ actionMap->GetKeyBindingForAction(ShortcutAction::CopyText, *args) };
VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast<int32_t>('B') }, kbd);
VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast<int32_t>('B'), 0 }, kbd);
}
{
Log::Comment(L"command with new terminal args");
@ -700,7 +711,7 @@ namespace SettingsModelLocalTests
auto args{ winrt::make_self<implementation::NewTabArgs>(*newTerminalArgs) };
const auto& kbd{ actionMap->GetKeyBindingForAction(ShortcutAction::NewTab, *args) };
VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast<int32_t>('C') }, kbd);
VerifyKeyChordEquality({ VirtualKeyModifiers::Control, static_cast<int32_t>('C'), 0 }, kbd);
}
{
Log::Comment(L"command with hidden args");
@ -708,7 +719,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(4u, actionMap->_KeyMap.size());
const auto& kbd{ actionMap->GetKeyBindingForAction(ShortcutAction::ToggleCommandPalette) };
VerifyKeyChordEquality({ VirtualKeyModifiers::Control | VirtualKeyModifiers::Shift, static_cast<int32_t>('P') }, kbd);
VerifyKeyChordEquality({ VirtualKeyModifiers::Control | VirtualKeyModifiers::Shift, static_cast<int32_t>('P'), 0 }, kbd);
}
}
}

View File

@ -113,7 +113,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(12u, actionMapImpl->_KeyMap.size());
{
KeyChord kc{ true, false, false, static_cast<int32_t>('A') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('A'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
@ -134,7 +134,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(1, termSettings.HistorySize());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('B') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('B'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
@ -156,7 +156,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(2, termSettings.HistorySize());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('C') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('C'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
@ -178,7 +178,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(2, termSettings.HistorySize());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('D') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('D'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
@ -200,7 +200,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(3, termSettings.HistorySize());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('E') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('E'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
@ -222,7 +222,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(1, termSettings.HistorySize());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('F') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('F'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
@ -245,7 +245,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(2, termSettings.HistorySize());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('G') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('G'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<NewTabArgs>();
@ -265,7 +265,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(1, termSettings.HistorySize());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('H') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('H'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<NewTabArgs>();
@ -287,7 +287,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(1, termSettings.HistorySize());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('I') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('I'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<NewTabArgs>();
@ -310,7 +310,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(3, termSettings.HistorySize());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('J') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('J'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<NewTabArgs>();
@ -332,7 +332,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(1, termSettings.HistorySize());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('K') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('K'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<NewTabArgs>();
@ -355,7 +355,7 @@ namespace SettingsModelLocalTests
VERIFY_ARE_EQUAL(3, termSettings.HistorySize());
}
{
KeyChord kc{ true, false, false, static_cast<int32_t>('L') };
KeyChord kc{ true, false, false, false, static_cast<int32_t>('L'), 0 };
auto actionAndArgs = ::TestUtils::GetActionAndArgs(actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<NewTabArgs>();

View File

@ -250,10 +250,12 @@ namespace winrt::TerminalApp::implementation
void CommandPalette::_previewKeyDownHandler(IInspectable const& /*sender*/,
Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e)
{
auto key = e.OriginalKey();
auto const ctrlDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down);
auto const altDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down);
auto const shiftDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down);
const auto key = e.OriginalKey();
const auto scanCode = e.KeyStatus().ScanCode;
const auto coreWindow = CoreWindow::GetForCurrentThread();
const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down);
const auto altDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down);
const auto shiftDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down);
// Some keypresses such as Tab, Return, Esc, and Arrow Keys are ignored by controls because
// they're not considered input key presses. While they don't raise KeyDown events,
@ -264,7 +266,7 @@ namespace winrt::TerminalApp::implementation
// a really widely used keyboard navigation key.
if (_currentMode == CommandPaletteMode::TabSwitchMode && _actionMap)
{
winrt::Microsoft::Terminal::Control::KeyChord kc{ ctrlDown, altDown, shiftDown, static_cast<int32_t>(key) };
winrt::Microsoft::Terminal::Control::KeyChord kc{ ctrlDown, altDown, shiftDown, false, static_cast<int32_t>(key), static_cast<int32_t>(scanCode) };
if (const auto cmd{ _actionMap.GetActionByKeyChord(kc) })
{
if (cmd.ActionAndArgs().Action() == ShortcutAction::PrevTab)
@ -402,9 +404,10 @@ namespace winrt::TerminalApp::implementation
// - <none>
void CommandPalette::_anchorKeyUpHandler()
{
auto const ctrlDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down);
auto const altDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down);
auto const shiftDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down);
const auto coreWindow = CoreWindow::GetForCurrentThread();
const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down);
const auto altDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down);
const auto shiftDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down);
if (!ctrlDown && !altDown && !shiftDown)
{

View File

@ -934,12 +934,14 @@ namespace winrt::TerminalApp::implementation
// - <none>
void TerminalPage::_KeyDownHandler(Windows::Foundation::IInspectable const& /*sender*/, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e)
{
auto key = e.OriginalKey();
auto const ctrlDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down);
auto const altDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down);
auto const shiftDown = WI_IsFlagSet(CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down);
const auto key = e.OriginalKey();
const auto scanCode = e.KeyStatus().ScanCode;
const auto coreWindow = CoreWindow::GetForCurrentThread();
const auto ctrlDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Control), CoreVirtualKeyStates::Down);
const auto altDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Menu), CoreVirtualKeyStates::Down);
const auto shiftDown = WI_IsFlagSet(coreWindow.GetKeyState(winrt::Windows::System::VirtualKey::Shift), CoreVirtualKeyStates::Down);
winrt::Microsoft::Terminal::Control::KeyChord kc{ ctrlDown, altDown, shiftDown, static_cast<int32_t>(key) };
winrt::Microsoft::Terminal::Control::KeyChord kc{ ctrlDown, altDown, shiftDown, false, static_cast<int32_t>(key), static_cast<int32_t>(scanCode) };
if (const auto cmd{ _settings.ActionMap().GetActionByKeyChord(kc) })
{
if (CommandPalette().Visibility() == Visibility::Visible && cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette)

View File

@ -10,32 +10,27 @@ using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers;
namespace winrt::Microsoft::Terminal::Control::implementation
{
KeyChord::KeyChord() noexcept :
_modifiers{ 0 },
_vkey{ 0 }
static VirtualKeyModifiers modifiersFromBooleans(bool ctrl, bool alt, bool shift, bool win)
{
VirtualKeyModifiers modifiers = VirtualKeyModifiers::None;
WI_SetFlagIf(modifiers, VirtualKeyModifiers::Control, ctrl);
WI_SetFlagIf(modifiers, VirtualKeyModifiers::Menu, alt);
WI_SetFlagIf(modifiers, VirtualKeyModifiers::Shift, shift);
WI_SetFlagIf(modifiers, VirtualKeyModifiers::Windows, win);
return modifiers;
}
KeyChord::KeyChord(bool ctrl, bool alt, bool shift, bool win, int32_t vkey, int32_t scanCode) noexcept :
_modifiers{ modifiersFromBooleans(ctrl, alt, shift, win) },
_vkey{ vkey },
_scanCode{ scanCode }
{
}
KeyChord::KeyChord(bool ctrl, bool alt, bool shift, int32_t vkey) noexcept :
_modifiers{ (ctrl ? VirtualKeyModifiers::Control : VirtualKeyModifiers::None) |
(alt ? VirtualKeyModifiers::Menu : VirtualKeyModifiers::None) |
(shift ? VirtualKeyModifiers::Shift : VirtualKeyModifiers::None) },
_vkey{ vkey }
{
}
KeyChord::KeyChord(bool ctrl, bool alt, bool shift, bool win, int32_t vkey) noexcept :
_modifiers{ (ctrl ? VirtualKeyModifiers::Control : VirtualKeyModifiers::None) |
(alt ? VirtualKeyModifiers::Menu : VirtualKeyModifiers::None) |
(shift ? VirtualKeyModifiers::Shift : VirtualKeyModifiers::None) |
(win ? VirtualKeyModifiers::Windows : VirtualKeyModifiers::None) },
_vkey{ vkey }
{
}
KeyChord::KeyChord(VirtualKeyModifiers const& modifiers, int32_t vkey) noexcept :
KeyChord::KeyChord(const VirtualKeyModifiers modifiers, int32_t vkey, int32_t scanCode) noexcept :
_modifiers{ modifiers },
_vkey{ vkey }
_vkey{ vkey },
_scanCode{ scanCode }
{
}
@ -58,4 +53,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
_vkey = value;
}
int32_t KeyChord::ScanCode() noexcept
{
return _scanCode;
}
void KeyChord::ScanCode(int32_t value) noexcept
{
_scanCode = value;
}
}

View File

@ -9,19 +9,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
struct KeyChord : KeyChordT<KeyChord>
{
KeyChord() noexcept;
KeyChord(winrt::Windows::System::VirtualKeyModifiers const& modifiers, int32_t vkey) noexcept;
KeyChord(bool ctrl, bool alt, bool shift, int32_t vkey) noexcept;
KeyChord(bool ctrl, bool alt, bool shift, bool win, int32_t vkey) noexcept;
KeyChord() noexcept = default;
KeyChord(const winrt::Windows::System::VirtualKeyModifiers modifiers, int32_t vkey, int32_t scanCode) noexcept;
KeyChord(bool ctrl, bool alt, bool shift, bool win, int32_t vkey, int32_t scanCode) noexcept;
winrt::Windows::System::VirtualKeyModifiers Modifiers() noexcept;
void Modifiers(winrt::Windows::System::VirtualKeyModifiers const& value) noexcept;
int32_t Vkey() noexcept;
void Vkey(int32_t value) noexcept;
int32_t ScanCode() noexcept;
void ScanCode(int32_t value) noexcept;
private:
winrt::Windows::System::VirtualKeyModifiers _modifiers;
int32_t _vkey;
winrt::Windows::System::VirtualKeyModifiers _modifiers{};
int32_t _vkey{};
int32_t _scanCode{};
};
}

View File

@ -7,11 +7,11 @@ namespace Microsoft.Terminal.Control
runtimeclass KeyChord
{
KeyChord();
KeyChord(Windows.System.VirtualKeyModifiers modifiers, Int32 vkey);
KeyChord(Boolean ctrl, Boolean alt, Boolean shift, Int32 vkey);
KeyChord(Boolean ctrl, Boolean alt, Boolean shift, Boolean win, Int32 vkey);
KeyChord(Windows.System.VirtualKeyModifiers modifiers, Int32 vkey, Int32 scanCode);
KeyChord(Boolean ctrl, Boolean alt, Boolean shift, Boolean win, Int32 vkey, Int32 scanCode);
Windows.System.VirtualKeyModifiers Modifiers;
Int32 Vkey;
Int32 ScanCode;
}
}

View File

@ -798,7 +798,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
modifiers.IsCtrlPressed(),
modifiers.IsAltPressed(),
modifiers.IsShiftPressed(),
modifiers.IsWinPressed(),
VK_F7,
0,
});
}
@ -927,6 +929,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
modifiers.IsShiftPressed(),
modifiers.IsWinPressed(),
vkey,
scanCode,
});
if (!success)
{

View File

@ -18,6 +18,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
DependencyProperty KeyChordListener::_KeysProperty{ nullptr };
// The ModifierKeys have been sorted by value.
// Not just binary search, but also your CPU likes sorted data.
static constexpr std::array ModifierKeys{
VirtualKey::Shift,
VirtualKey::Control,
@ -129,7 +131,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
}
// Permitted key events are used to update _Keys
Keys({ modifiers, static_cast<int32_t>(key) });
Keys({ modifiers, static_cast<int32_t>(key), 0 });
e.Handled(true);
}
}

View File

@ -513,9 +513,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// If we had to find one from a layer above that, parent->_MaskingActions
// would have found it, so we inherit it for free!
const auto& inheritedCmd{ parent->_GetActionByID(actionID) };
if (inheritedCmd.has_value() && inheritedCmd.value())
if (inheritedCmd && *inheritedCmd)
{
const auto& inheritedCmdImpl{ get_self<Command>(inheritedCmd.value()) };
const auto& inheritedCmdImpl{ get_self<Command>(*inheritedCmd) };
maskingCmd = *inheritedCmdImpl->Copy();
_MaskingActions.emplace(actionID, maskingCmd);
}
@ -683,18 +683,24 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// - keys: the key chord of the command to search for
// Return Value:
// - the command with the given key chord
// - nullptr if the key chord is explicitly unbound
// - nullptr if the key chord doesn't exist
Model::Command ActionMap::GetActionByKeyChord(Control::KeyChord const& keys) const
{
// Check the current layer
const auto cmd{ _GetActionByKeyChordInternal(keys) };
if (cmd.has_value())
const auto modifiers = keys.Modifiers();
// The "keys" given to us can contain both a Vkey, as well as a ScanCode.
// For instance our UI code fills out a KeyChord with all available information.
// But our _KeyMap only contains KeyChords that contain _either_ a Vkey or ScanCode.
// Due to this we'll have to call _GetActionByKeyChordInternal twice.
if (auto vkey = keys.Vkey())
{
return *cmd;
if (auto command = _GetActionByKeyChordInternal({ modifiers, vkey, 0 }))
{
return *command;
}
}
// This key chord is not explicitly bound
return nullptr;
return _GetActionByKeyChordInternal({ modifiers, 0, keys.ScanCode() }).value_or(nullptr);
}
// Method Description:
@ -709,8 +715,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
std::optional<Model::Command> ActionMap::_GetActionByKeyChordInternal(Control::KeyChord const& keys) const
{
// Check the current layer
const auto actionIDPair{ _KeyMap.find(keys) };
if (actionIDPair != _KeyMap.end())
if (const auto actionIDPair = _KeyMap.find(keys); actionIDPair != _KeyMap.end())
{
// the command was explicitly bound,
// return what we found (invalid commands exposed as nullptr)
@ -723,7 +728,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
for (const auto& parent : _parents)
{
const auto& inheritedCmd{ parent->_GetActionByKeyChordInternal(keys) };
if (inheritedCmd.has_value())
if (inheritedCmd)
{
return *inheritedCmd;
}
@ -765,7 +770,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
const auto hash{ Hash(actionAndArgs) };
if (const auto& cmd{ _GetActionByID(hash) })
{
return cmd.value().Keys();
return cmd->Keys();
}
// Check our parents

View File

@ -36,7 +36,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
std::size_t operator()(const Control::KeyChord& key) const
{
return ::Microsoft::Terminal::Settings::Model::HashUtils::HashProperty(key.Modifiers(), key.Vkey());
return ::Microsoft::Terminal::Settings::Model::HashUtils::HashProperty(key.Modifiers(), key.Vkey(), key.ScanCode());
}
};
@ -44,7 +44,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
bool operator()(const Control::KeyChord& lhs, const Control::KeyChord& rhs) const
{
return lhs.Modifiers() == rhs.Modifiers() && lhs.Vkey() == rhs.Vkey();
return lhs.Modifiers() == rhs.Modifiers() && lhs.Vkey() == rhs.Vkey() && lhs.ScanCode() == rhs.ScanCode();
}
};
@ -79,7 +79,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
private:
std::optional<Model::Command> _GetActionByID(const InternalActionID actionID) const;
std::optional<Model::Command> _GetActionByKeyChordInternal(Control::KeyChord const& keys) const;
std::optional<Model::Command> _GetActionByKeyChordInternal(const Control::KeyChord& keys) const;
void _PopulateAvailableActionsWithStandardCommands(std::unordered_map<hstring, Model::ActionAndArgs>& availableActions, std::unordered_set<InternalActionID>& visitedActionIDs) const;
void _PopulateNameMapWithSpecialCommands(std::unordered_map<hstring, Model::Command>& nameMap) const;

View File

@ -17,6 +17,8 @@ constexpr std::wstring_view SHIFT_KEY{ L"shift" };
constexpr std::wstring_view ALT_KEY{ L"alt" };
constexpr std::wstring_view WIN_KEY{ L"win" };
// If you modify this list you should modify the
// KeyChordSegment description in profiles.schema.json.
#define VKEY_NAME_PAIRS(XX) \
XX(VK_RETURN, L"enter") \
XX(VK_TAB, L"tab") \
@ -73,7 +75,33 @@ constexpr std::wstring_view WIN_KEY{ L"win" };
XX(VK_NUMPAD7, L"numpad7", L"numpad_7") \
XX(VK_NUMPAD8, L"numpad8", L"numpad_8") \
XX(VK_NUMPAD9, L"numpad9", L"numpad_9") \
XX(VK_OEM_PLUS, L"plus")
XX(VK_OEM_PLUS, L"plus") /* '+' any country */ \
XX(VK_OEM_COMMA, L"comma") /* ',' any country */ \
XX(VK_OEM_MINUS, L"minus") /* '-' any country */ \
XX(VK_OEM_PERIOD, L"period") /* '.' any country */
constexpr std::wstring_view vkeyPrefix{ L"vk(" };
constexpr std::wstring_view scanCodePrefix{ L"sc(" };
constexpr std::wstring_view codeSuffix{ L")" };
// Parses a vk(nnn) or sc(nnn) key chord part.
// If the part doesn't contain either of these two this function returns 0.
// For invalid arguments we throw an exception.
static int32_t parseNumericCode(const std::wstring_view& str, const std::wstring_view& prefix, const std::wstring_view& suffix)
{
if (!til::ends_with(str, suffix) || !til::starts_with(str, prefix))
{
return 0;
}
const auto value = til::from_wchars({ str.data() + prefix.size(), str.size() - prefix.size() - suffix.size() });
if (value > 0 && value < 256)
{
return gsl::narrow_cast<int32_t>(value);
}
throw winrt::hresult_invalid_argument(L"Invalid numeric argument to vk() or sc()");
}
// Function Description:
// - Deserializes the given string into a new KeyChord instance. If this
@ -109,6 +137,7 @@ static KeyChord _fromString(std::wstring_view wstr)
VirtualKeyModifiers modifiers = VirtualKeyModifiers::None;
int32_t vkey = 0;
int32_t scanCode = 0;
while (!wstr.empty())
{
@ -132,10 +161,9 @@ static KeyChord _fromString(std::wstring_view wstr)
}
else
{
if (vkey)
if (vkey || scanCode)
{
// Key bindings like Ctrl+A+B are not valid.
throw winrt::hresult_invalid_argument();
throw winrt::hresult_invalid_argument(L"Key bindings like Ctrl+A+B are not valid");
}
// Characters 0-9, a-z, A-Z directly map to virtual keys.
@ -149,6 +177,22 @@ static KeyChord _fromString(std::wstring_view wstr)
}
}
// vk() allows a user to specify a virtual key code
// and sc() allows them to specify a scan code manually.
//
// ctrl+vk(0x09) for instance is the same as ctrl+tab, while win+sc(41) specifies
// a key binding which is (seemingly) always bound to the key below Esc.
vkey = parseNumericCode(part, vkeyPrefix, codeSuffix);
if (vkey)
{
continue;
}
scanCode = parseNumericCode(part, scanCodePrefix, codeSuffix);
if (scanCode)
{
continue;
}
// nameToVkey contains a few more mappings like "F11".
if (const auto it = nameToVkey.find(part); it != nameToVkey.end())
{
@ -172,11 +216,11 @@ static KeyChord _fromString(std::wstring_view wstr)
}
}
throw winrt::hresult_invalid_argument();
throw winrt::hresult_invalid_argument(L"Invalid key binding");
}
}
return KeyChord{ modifiers, vkey };
return KeyChord{ modifiers, vkey, scanCode };
}
// Function Description:
@ -204,6 +248,7 @@ static std::wstring _toString(const KeyChord& chord)
const auto modifiers = chord.Modifiers();
const auto vkey = chord.Vkey();
const auto scanCode = chord.ScanCode();
std::wstring buffer;
// Add modifiers
@ -228,6 +273,14 @@ static std::wstring _toString(const KeyChord& chord)
buffer.push_back(L'+');
}
if (scanCode)
{
buffer.append(scanCodePrefix);
buffer.append(std::to_wstring(scanCode));
buffer.append(codeSuffix);
return buffer;
}
// Quick lookup: ranges of vkeys that correlate directly to a key.
if ((vkey >= L'0' && vkey <= L'9') || (vkey >= L'A' && vkey <= L'Z'))
{
@ -248,6 +301,14 @@ static std::wstring _toString(const KeyChord& chord)
return buffer;
}
if (vkey)
{
buffer.append(vkeyPrefix);
buffer.append(std::to_wstring(vkey));
buffer.append(codeSuffix);
return buffer;
}
return {};
}

View File

@ -170,8 +170,8 @@
{ "command": "scrollUpPage", "keys": "ctrl+shift+pgup" },
// Visual Adjustments
{ "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+=" },
{ "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+-" },
{ "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+plus" },
{ "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+minus" },
{ "command": "resetFontSize", "keys": "ctrl+0" },
// Other commands

View File

@ -298,7 +298,7 @@
{ "command": "commandPalette", "keys":"ctrl+shift+p" },
{ "command": "identifyWindow" },
{ "command": "openWindowRenamer" },
{ "command": "quakeMode", "keys":"win+`" },
{ "command": "quakeMode", "keys":"win+sc(41)" },
// Tab Management
// "command": "closeTab" is unbound by default.
@ -362,8 +362,8 @@
{ "command": "scrollToBottom", "keys": "ctrl+shift+end" },
// Visual Adjustments
{ "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+=" },
{ "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+-" },
{ "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+plus" },
{ "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+minus" },
{ "command": { "action": "adjustFontSize", "delta": 1 }, "keys": "ctrl+numpad_plus" },
{ "command": { "action": "adjustFontSize", "delta": -1 }, "keys": "ctrl+numpad_minus" },
{ "command": "resetFontSize", "keys": "ctrl+0" },

View File

@ -669,23 +669,30 @@ void AppHost::_listenForInboundConnections()
winrt::fire_and_forget AppHost::_setupGlobalHotkeys()
{
// The hotkey MUST be registered on the main thread. It will fail otherwise!
co_await winrt::resume_foreground(_logic.GetRoot().Dispatcher(),
winrt::Windows::UI::Core::CoreDispatcherPriority::Normal);
co_await winrt::resume_foreground(_logic.GetRoot().Dispatcher());
// Remove all the already registered hotkeys before setting up the new ones.
_window->UnsetHotkeys(_hotkeys);
_hotkeyActions = _logic.GlobalHotkeys();
_hotkeys.clear();
for (const auto& [k, v] : _hotkeyActions)
// Unregister all previously registered hotkeys.
//
// RegisterHotKey(), will not unregister hotkeys automatically.
// If a hotkey with a given HWND and ID combination already exists
// then a duplicate one will be added, which we don't want.
// (Additionally we want to remove hotkeys that were removed from the settings.)
for (int i = 0, count = gsl::narrow_cast<int>(_hotkeys.size()); i < count; ++i)
{
if (k != nullptr)
{
_hotkeys.push_back(k);
}
_window->UnregisterHotKey(i);
}
_window->SetGlobalHotkeys(_hotkeys);
_hotkeys.clear();
// Re-register all current hotkeys.
for (const auto& [keyChord, cmd] : _logic.GlobalHotkeys())
{
if (auto summonArgs = cmd.ActionAndArgs().Args().try_as<Settings::Model::GlobalSummonArgs>())
{
_window->RegisterHotKey(gsl::narrow_cast<int>(_hotkeys.size()), keyChord);
_hotkeys.emplace_back(summonArgs);
}
}
}
// Method Description:
@ -706,47 +713,40 @@ void AppHost::_GlobalHotkeyPressed(const long hotkeyIndex)
{
return;
}
// Lookup the matching keychord
Control::KeyChord kc = _hotkeys.at(hotkeyIndex);
// Get the stored Command for that chord
if (const auto& cmd{ _hotkeyActions.Lookup(kc) })
const auto& summonArgs = til::at(_hotkeys, hotkeyIndex);
Remoting::SummonWindowSelectionArgs args{ summonArgs.Name() };
// desktop:any - MoveToCurrentDesktop=false, OnCurrentDesktop=false
// desktop:toCurrent - MoveToCurrentDesktop=true, OnCurrentDesktop=false
// desktop:onCurrent - MoveToCurrentDesktop=false, OnCurrentDesktop=true
args.OnCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::OnCurrent);
args.SummonBehavior().MoveToCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::ToCurrent);
args.SummonBehavior().ToggleVisibility(summonArgs.ToggleVisibility());
args.SummonBehavior().DropdownDuration(summonArgs.DropdownDuration());
switch (summonArgs.Monitor())
{
if (const auto& summonArgs{ cmd.ActionAndArgs().Args().try_as<Settings::Model::GlobalSummonArgs>() })
{
Remoting::SummonWindowSelectionArgs args{ summonArgs.Name() };
case Settings::Model::MonitorBehavior::Any:
args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace);
break;
case Settings::Model::MonitorBehavior::ToCurrent:
args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToCurrent);
break;
case Settings::Model::MonitorBehavior::ToMouse:
args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToMouse);
break;
}
// desktop:any - MoveToCurrentDesktop=false, OnCurrentDesktop=false
// desktop:toCurrent - MoveToCurrentDesktop=true, OnCurrentDesktop=false
// desktop:onCurrent - MoveToCurrentDesktop=false, OnCurrentDesktop=true
args.OnCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::OnCurrent);
args.SummonBehavior().MoveToCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::ToCurrent);
args.SummonBehavior().ToggleVisibility(summonArgs.ToggleVisibility());
args.SummonBehavior().DropdownDuration(summonArgs.DropdownDuration());
switch (summonArgs.Monitor())
{
case Settings::Model::MonitorBehavior::Any:
args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace);
break;
case Settings::Model::MonitorBehavior::ToCurrent:
args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToCurrent);
break;
case Settings::Model::MonitorBehavior::ToMouse:
args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::ToMouse);
break;
}
_windowManager.SummonWindow(args);
if (args.FoundMatch())
{
// Excellent, the window was found. We have nothing else to do here.
}
else
{
// We should make the window ourselves.
_createNewTerminalWindow(summonArgs);
}
}
_windowManager.SummonWindow(args);
if (args.FoundMatch())
{
// Excellent, the window was found. We have nothing else to do here.
}
else
{
// We should make the window ourselves.
_createNewTerminalWindow(summonArgs);
}
}

View File

@ -20,19 +20,17 @@ public:
bool HasWindow();
private:
bool _useNonClientArea;
std::unique_ptr<IslandWindow> _window;
winrt::TerminalApp::App _app;
winrt::TerminalApp::AppLogic _logic;
bool _shouldCreateWindow{ false };
winrt::Microsoft::Terminal::Remoting::WindowManager _windowManager{ nullptr };
std::vector<winrt::Microsoft::Terminal::Control::KeyChord> _hotkeys{};
winrt::Windows::Foundation::Collections::IMapView<winrt::Microsoft::Terminal::Control::KeyChord, winrt::Microsoft::Terminal::Settings::Model::Command> _hotkeyActions{ nullptr };
std::vector<winrt::Microsoft::Terminal::Settings::Model::GlobalSummonArgs> _hotkeys;
winrt::com_ptr<IVirtualDesktopManager> _desktopManager{ nullptr };
bool _shouldCreateWindow{ false };
bool _useNonClientArea{ false };
void _HandleCommandlineArgs();
void _HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode);

View File

@ -966,62 +966,62 @@ void IslandWindow::_SetIsFullscreen(const bool fullscreenEnabled)
}
// Method Description:
// - Call UnregisterHotKey once for each entry in hotkeyList, to unset all the bound global hotkeys.
// Arguments:
// - hotkeyList: a list of hotkeys to unbind
// - Call UnregisterHotKey once for each previously registered hotkey.
// Return Value:
// - <none>
void IslandWindow::UnsetHotkeys(const std::vector<winrt::Microsoft::Terminal::Control::KeyChord>& hotkeyList)
void IslandWindow::UnregisterHotKey(const int index) noexcept
{
TraceLoggingWrite(g_hWindowsTerminalProvider,
"UnsetHotkeys",
TraceLoggingDescription("Emitted when clearing previously set hotkeys"),
TraceLoggingInt64(hotkeyList.size(), "numHotkeys", "The number of hotkeys to unset"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
TraceLoggingWrite(
g_hWindowsTerminalProvider,
"UnregisterAllHotKeys",
TraceLoggingDescription("Emitted when clearing previously set hotkeys"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
for (int i = 0; i < ::base::saturated_cast<int>(hotkeyList.size()); i++)
{
LOG_IF_WIN32_BOOL_FALSE(UnregisterHotKey(_window.get(), i));
}
LOG_IF_WIN32_BOOL_FALSE(::UnregisterHotKey(_window.get(), index));
}
// Method Description:
// - Call RegisterHotKey once for each entry in hotkeyList, to attempt to
// register that keybinding as a global hotkey.
// - Call RegisterHotKey to attempt to register that keybinding as a global hotkey.
// - When these keys are pressed, we'll get a WM_HOTKEY message with the payload
// containing the index we registered here.
// - Call UnregisterHotKey() before registering your hotkeys.
// See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey#remarks
// Arguments:
// - hotkeyList: a list of hotkeys to bind
// - hotkey: The key-combination to register.
// Return Value:
// - <none>
void IslandWindow::SetGlobalHotkeys(const std::vector<winrt::Microsoft::Terminal::Control::KeyChord>& hotkeyList)
void IslandWindow::RegisterHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept
{
TraceLoggingWrite(g_hWindowsTerminalProvider,
"SetGlobalHotkeys",
TraceLoggingDescription("Emitted when setting hotkeys"),
TraceLoggingInt64(hotkeyList.size(), "numHotkeys", "The number of hotkeys to set"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
int index = 0;
for (const auto& hotkey : hotkeyList)
TraceLoggingWrite(
g_hWindowsTerminalProvider,
"RegisterHotKey",
TraceLoggingDescription("Emitted when setting hotkeys"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
auto vkey = hotkey.Vkey();
if (!vkey)
{
vkey = MapVirtualKeyW(hotkey.ScanCode(), MAPVK_VSC_TO_VK);
}
if (!vkey)
{
return;
}
auto hotkeyFlags = MOD_NOREPEAT;
{
const auto modifiers = hotkey.Modifiers();
const auto hotkeyFlags = MOD_NOREPEAT |
(WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows) ? MOD_WIN : 0) |
(WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu) ? MOD_ALT : 0) |
(WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control) ? MOD_CONTROL : 0) |
(WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift) ? MOD_SHIFT : 0);
// TODO GH#8888: We should display a warning of some kind if this fails.
// This can fail if something else already bound this hotkey.
LOG_IF_WIN32_BOOL_FALSE(RegisterHotKey(_window.get(),
index,
hotkeyFlags,
hotkey.Vkey()));
index++;
WI_SetFlagIf(hotkeyFlags, MOD_WIN, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows));
WI_SetFlagIf(hotkeyFlags, MOD_ALT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu));
WI_SetFlagIf(hotkeyFlags, MOD_CONTROL, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control));
WI_SetFlagIf(hotkeyFlags, MOD_SHIFT, WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift));
}
// TODO GH#8888: We should display a warning of some kind if this fails.
// This can fail if something else already bound this hotkey.
LOG_IF_WIN32_BOOL_FALSE(::RegisterHotKey(_window.get(), index, hotkeyFlags, vkey));
}
// Method Description:

View File

@ -38,8 +38,8 @@ public:
void FlashTaskbar();
void SetTaskbarProgress(const size_t state, const size_t progress);
void UnsetHotkeys(const std::vector<winrt::Microsoft::Terminal::Control::KeyChord>& hotkeyList);
void SetGlobalHotkeys(const std::vector<winrt::Microsoft::Terminal::Control::KeyChord>& hotkeyList);
void UnregisterHotKey(const int index) noexcept;
void RegisterHotKey(const int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept;
winrt::fire_and_forget SummonWindow(winrt::Microsoft::Terminal::Remoting::SummonWindowBehavior args);
@ -65,8 +65,8 @@ protected:
HWND _interopWindowHandle;
winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _source;
winrt::Windows::UI::Xaml::Controls::Grid _rootGrid;
wil::com_ptr<ITaskbarList3> _taskbar;
std::function<void(const HWND, const RECT, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode)> _pfnCreateCallback;
std::function<float(bool, float)> _pfnSnapDimensionCallback;
@ -79,9 +79,9 @@ protected:
bool _alwaysOnTop{ false };
bool _fullscreen{ false };
bool _fWasMaximizedBeforeFullscreen{ false };
RECT _rcWindowBeforeFullscreen;
RECT _rcWorkBeforeFullscreen;
UINT _dpiBeforeFullscreen;
RECT _rcWindowBeforeFullscreen{};
RECT _rcWorkBeforeFullscreen{};
UINT _dpiBeforeFullscreen{ 96 };
virtual void _SetIsBorderless(const bool borderlessEnabled);
virtual void _SetIsFullscreen(const bool fullscreenEnabled);
@ -90,8 +90,6 @@ protected:
LONG _getDesiredWindowStyle() const;
wil::com_ptr<ITaskbarList3> _taskbar;
void _OnGetMinMaxInfo(const WPARAM wParam, const LPARAM lParam);
long _calculateTotalSize(const bool isWidth, const long clientSize, const long nonClientSize);

View File

@ -73,6 +73,77 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
return ends_with<>(str, prefix);
}
inline constexpr unsigned long from_wchars_error = ULONG_MAX;
// Just like std::wcstoul, but without annoying locales and null-terminating strings.
// It has been fuzz-tested against clang's strtoul implementation.
_TIL_INLINEPREFIX unsigned long from_wchars(const std::wstring_view& str) noexcept
{
static constexpr unsigned long maximumValue = ULONG_MAX / 16;
// We don't have to test ptr for nullability, as we only access it under either condition:
// * str.length() > 0, for determining the base
// * ptr != end, when parsing the characters; if ptr is null, length will be 0 and thus end == ptr
#pragma warning(push)
#pragma warning(disable : 26429) // Symbol 'ptr' is never tested for nullness, it can be marked as not_null
#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead
auto ptr = str.data();
const auto end = ptr + str.length();
unsigned long base = 10;
unsigned long accumulator = 0;
unsigned long value = ULONG_MAX;
if (str.length() > 1 && *ptr == L'0')
{
base = 8;
ptr++;
if (str.length() > 2 && (*ptr == L'x' || *ptr == L'X'))
{
base = 16;
ptr++;
}
}
if (ptr == end)
{
return from_wchars_error;
}
for (;; accumulator *= base)
{
value = ULONG_MAX;
if (*ptr >= L'0' && *ptr <= L'9')
{
value = *ptr - L'0';
}
else if (*ptr >= L'A' && *ptr <= L'F')
{
value = *ptr - L'A' + 10;
}
else if (*ptr >= L'a' && *ptr <= L'f')
{
value = *ptr - L'a' + 10;
}
else
{
return from_wchars_error;
}
accumulator += value;
if (accumulator >= maximumValue)
{
return from_wchars_error;
}
if (++ptr == end)
{
return accumulator;
}
}
#pragma warning(pop)
}
// Just like std::tolower, but without annoying locales.
template<typename T>
constexpr T tolower_ascii(T c)

View File

@ -53,6 +53,56 @@ class StringTests
VERIFY_IS_TRUE(til::ends_with("0abc", "abc"));
}
// Normally this would be the spot where you'd find a TEST_METHOD(from_wchars).
// I didn't quite trust my coding skills and thus opted to use fuzz-testing.
// The below function was used to test from_wchars for unsafety and conformance with clang's strtoul.
// The test was run as:
// clang++ -fsanitize=address,undefined,fuzzer -std=c++17 file.cpp
// and was run for 20min across 16 jobs in parallel.
#if 0
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
while (size > 0 && (isspace(*data) || *data == '+' || *data == '-'))
{
--size;
++data;
}
if (size == 0 || size > 127)
{
return 0;
}
char narrow_buffer[128];
wchar_t wide_buffer[128];
memcpy(narrow_buffer, data, size);
for (size_t i = 0; i < size; ++i)
{
wide_buffer[i] = data[i];
}
// strtoul requires a null terminator
narrow_buffer[size] = 0;
wide_buffer[size] = 0;
char* end;
const auto expected = strtoul(narrow_buffer, &end, 0);
if (end != narrow_buffer + size || expected >= ULONG_MAX / 16)
{
return 0;
}
const auto actual = from_wchars({ wide_buffer, size });
if (expected != actual)
{
__builtin_trap();
}
return 0;
}
#endif
TEST_METHOD(tolower_ascii)
{
for (wchar_t ch = 0; ch < 128; ++ch)

View File

@ -2,34 +2,26 @@
# the contents of that json files as a constexpr string_view in the header.
param (
[parameter(Mandatory=$true, Position=0)]
[parameter(Mandatory = $true)]
[string]$JsonFile,
[parameter(Mandatory=$true, Position=1)]
[parameter(Mandatory = $true)]
[string]$OutPath,
[parameter(Mandatory=$true, Position=2)]
[parameter(Mandatory = $true)]
[string]$VariableName
)
# Load the xml files.
$fullPath = Resolve-Path $JsonFile
$jsonData = Get-Content $JsonFile
Write-Output "// Copyright (c) Microsoft Corporation" | Out-File -FilePath $OutPath -Encoding ASCII
Write-Output "// Licensed under the MIT license." | Out-File -FilePath $OutPath -Encoding ASCII -Append
Write-Output "" | Out-File -FilePath $OutPath -Encoding ASCII -Append
Write-Output "// THIS IS AN AUTO-GENERATED FILE" | Out-File -FilePath $OutPath -Encoding ASCII -Append
Write-Output "// Generated from " | Out-File -FilePath $OutPath -Encoding ASCII -Append -NoNewline
$fullPath = Resolve-Path -Path $JsonFile
Write-Output $fullPath.Path | Out-File -FilePath $OutPath -Encoding ASCII -Append
Write-Output "constexpr std::string_view $($VariableName){ " | Out-File -FilePath $OutPath -Encoding ASCII -Append
# Write each line escaped on its own, as it's own literal. This file is _very
# big_, so big that it cannot fit in a single string literal :O The compiler is,
# however, smart enough to just concatenate all these literals into one big
# string.
$jsonData | foreach {
Write-Output "R`"($_`n)`"" | Out-File -FilePath $OutPath -Encoding ASCII -Append
}
Write-Output "};" | Out-File -FilePath $OutPath -Encoding ASCII -Append
@(
"// Copyright (c) Microsoft Corporation",
"// Licensed under the MIT license.",
"",
"// THIS IS AN AUTO-GENERATED FILE",
"// Generated from $($fullPath.Path)",
"constexpr std::string_view $($VariableName){",
($jsonData | ForEach-Object { "R`"#($_`n)#`"" }),
"};"
) | Out-File -FilePath $OutPath -Encoding utf8