Adding/fixing Alt+Space handling (#10799)

<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? -->
## Summary of the Pull Request
This PR implements/solves #7125. Concretely: two requests regarding alt+space were posted there:
1. Disabling the alt+space menu when the keychord explicitly unbound - and forwarding the keystroke to the terminal
2. Disabling the alt+space menu when the keychord is bound to an action

<!-- Other than the issue solved, is this relevant to any other issues/existing PRs? --> 
## References
Not that I know

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
* [x] Closes #7125
* [x] CLA signed.
* [x] Tests added/passed
* [x] Documentation updated. N/A
* [x] Schema updated.
* [ ] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan.
The issue was marked Help-Wanted. I am happy to change the implementation to better fit your (planned) architecture.

<!-- Provide a more detailed description of the PR, other things fixed or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

While researching the solution, I noticed that the XAML system was always opening the system menu after Alt+Space, even when explicitly setting the event to be handled according to the documentation. The only solution I could find was to hook into the "XAML bypass" already in place for F7 KeyDown, and Alt KeyUp keystrokes. This bypass sends the keystroke to the AppHost immediately. This bypass method will "fall back" to the normal XAML routing when the keystroke is not handled.

The implemented behaviour is as follows:
- Default: same as normal; system menu is working since the bypass does not handle the keystroke
- Alt+Space explicitly unbound: bypass passes the keystroke to the terminal and marks it as handled
- Alt+Space bound to command: bypass invokes the command and marks it as handled

Concretely, added a method to the KeyBindings and ActionMap interfaces to check whether a keychord is explicitly unbound. The implementation for `_GetActionByKeyChordInternal` already distinguishes between explicitly unbound and lack of binding, however this distinction is not carried over to the public methods. I decided not to change this existing method, to avoid breaking other stuff and to make the API more explicit.

Furthermore, there were some checks against Alt+Space further down in the code, preventing this keystroke from being entered in the terminal. Since the check for this keystroke is now done at a "higher" level, I thought I could safely remove these checks as otherwise the keystroke could never be sent to the terminal itself. Please correct me if I'm wrong.

Note that when alt+space is bound to an action that opens the command pallette (such as tab search), then a second press of the key combination does still open the system menu. This is because at that point, the "bypass" is cancelled (called "not a good implementation" in #4031). I don't think this can easily be solved for now, but this is a very minor bug/inconvenience.

<!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Added tests for the new method. Performed manual checking:
* [x] Default configuration still opens system menu like normal
* [x] Binding alt+space to an action performs the action and does not show the system menu
* [x] Explicitly unbinding alt+space no longer shows the system menu and sends the keystroke to the terminal. I was unable to run the debug tap (it crashed my instance - same thing happening on preview and release builds) to check for sure, but behaviour was identical to native linux terminals.
This commit is contained in:
Floris Westerman 2021-08-10 21:53:07 +02:00 committed by GitHub
parent a14b6f89f6
commit ebf41dd6b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 100 additions and 38 deletions

View file

@ -40,7 +40,7 @@ namespace SettingsModelLocalTests
TEST_METHOD(ManyKeysSameAction);
TEST_METHOD(LayerKeybindings);
TEST_METHOD(UnbindKeybindings);
TEST_METHOD(TestExplicitUnbind);
TEST_METHOD(TestArbitraryArgs);
TEST_METHOD(TestSplitPaneArgs);
@ -232,6 +232,31 @@ namespace SettingsModelLocalTests
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('C'), 0 }));
}
void KeyBindingsTests::TestExplicitUnbind()
{
const std::string bindings0String{ R"([ { "command": "copy", "keys": ["ctrl+c"] } ])" };
const std::string bindings1String{ R"([ { "command": "unbound", "keys": ["ctrl+c"] } ])" };
const std::string bindings2String{ R"([ { "command": "copy", "keys": ["ctrl+c"] } ])" };
const auto bindings0Json = VerifyParseSucceeded(bindings0String);
const auto bindings1Json = VerifyParseSucceeded(bindings1String);
const auto bindings2Json = VerifyParseSucceeded(bindings2String);
const KeyChord keyChord{ VirtualKeyModifiers::Control, static_cast<int32_t>('C'), 0 };
auto actionMap = winrt::make_self<implementation::ActionMap>();
VERIFY_IS_FALSE(actionMap->IsKeyChordExplicitlyUnbound(keyChord));
actionMap->LayerJson(bindings0Json);
VERIFY_IS_FALSE(actionMap->IsKeyChordExplicitlyUnbound(keyChord));
actionMap->LayerJson(bindings1Json);
VERIFY_IS_TRUE(actionMap->IsKeyChordExplicitlyUnbound(keyChord));
actionMap->LayerJson(bindings2Json);
VERIFY_IS_FALSE(actionMap->IsKeyChordExplicitlyUnbound(keyChord));
}
void KeyBindingsTests::TestArbitraryArgs()
{
const std::string bindings0String{ R"([

View file

@ -21,6 +21,11 @@ namespace winrt::TerminalApp::implementation
return false;
}
bool AppKeyBindings::IsKeyChordExplicitlyUnbound(const KeyChord& kc)
{
return _actionMap.IsKeyChordExplicitlyUnbound(kc);
}
void AppKeyBindings::SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch)
{
_dispatch = dispatch;

View file

@ -20,6 +20,7 @@ namespace winrt::TerminalApp::implementation
AppKeyBindings() = default;
bool TryKeyChord(winrt::Microsoft::Terminal::Control::KeyChord const& kc);
bool IsKeyChordExplicitlyUnbound(winrt::Microsoft::Terminal::Control::KeyChord const& kc);
void SetDispatch(const winrt::TerminalApp::ShortcutActionDispatch& dispatch);
void SetActionMap(const Microsoft::Terminal::Settings::Model::IActionMapView& actionMap);

View file

@ -9,5 +9,6 @@ namespace Microsoft.Terminal.Control
interface IKeyBindings
{
Boolean TryKeyChord(KeyChord kc);
Boolean IsKeyChordExplicitlyUnbound(KeyChord kc);
}
}

View file

@ -761,28 +761,39 @@ namespace winrt::Microsoft::Terminal::Control::implementation
(void)_TrySendKeyEvent(VK_MENU, scanCode, modifiers, false);
handled = true;
}
else if (vkey == VK_F7 && down)
else if ((vkey == VK_F7 || vkey == VK_SPACE) && down)
{
// Manually generate an F7 event into the key bindings or terminal.
// This is required as part of GH#638.
// Or do so for alt+space; only send to terminal when explicitly unbound
// That is part of #GH7125
auto bindings{ _settings.KeyBindings() };
bool isUnbound = false;
const KeyChord kc = {
modifiers.IsCtrlPressed(),
modifiers.IsAltPressed(),
modifiers.IsShiftPressed(),
modifiers.IsWinPressed(),
gsl::narrow_cast<WORD>(vkey),
0
};
if (bindings)
{
handled = bindings.TryKeyChord({
modifiers.IsCtrlPressed(),
modifiers.IsAltPressed(),
modifiers.IsShiftPressed(),
modifiers.IsWinPressed(),
VK_F7,
0,
});
handled = bindings.TryKeyChord(kc);
if (!handled)
{
isUnbound = bindings.IsKeyChordExplicitlyUnbound(kc);
}
}
if (!handled)
const bool sendToTerminal = vkey == VK_F7 || (vkey == VK_SPACE && isUnbound);
if (!handled && sendToTerminal)
{
// _TrySendKeyEvent pretends it didn't handle F7 for some unknown reason.
(void)_TrySendKeyEvent(VK_F7, scanCode, modifiers, true);
(void)_TrySendKeyEvent(gsl::narrow_cast<WORD>(vkey), scanCode, modifiers, true);
// GH#6438: Note that we're _not_ sending the key up here - that'll
// get passed through XAML to our KeyUp handler normally.
handled = true;

View file

@ -594,14 +594,6 @@ bool Terminal::SendKeyEvent(const WORD vkey,
const auto isAltOnlyPressed = states.IsAltPressed() && !states.IsCtrlPressed();
// DON'T manually handle Alt+Space - the system will use this to bring up
// the system menu for restore, min/maximize, size, move, close.
// (This doesn't apply to Ctrl+Alt+Space.)
if (isAltOnlyPressed && vkey == VK_SPACE)
{
return false;
}
// By default Windows treats Ctrl+Alt as an alias for AltGr.
// When the altGrAliasing setting is set to false, this behaviour should be disabled.
//
@ -672,13 +664,6 @@ bool Terminal::SendMouseEvent(const COORD viewportPos, const unsigned int uiButt
// - false otherwise.
bool Terminal::SendCharEvent(const wchar_t ch, const WORD scanCode, const ControlKeyStates states)
{
// DON'T manually handle Alt+Space - the system will use this to bring up
// the system menu for restore, min/maximize, size, move, close.
if (ch == L' ' && states.IsAltPressed() && !states.IsCtrlPressed())
{
return false;
}
auto vkey = _TakeVirtualKeyFromLastKeyEvent(scanCode);
if (vkey == 0 && scanCode != 0)
{

View file

@ -677,6 +677,32 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
}
}
// Method Description:
// - Determines whether the given key chord is explicitly unbound
// Arguments:
// - keys: the key chord to check
// Return value:
// - true if the keychord is explicitly unbound
// - false if either the keychord is bound, or not bound at all
bool ActionMap::IsKeyChordExplicitlyUnbound(Control::KeyChord const& keys) const
{
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())
{
// We use the fact that the ..Internal call returns nullptr for explicitly unbound
// key chords, and nullopt for keychord that are not bound - it allows us to distinguish
// between unbound and lack of binding.
return nullptr == _GetActionByKeyChordInternal({ modifiers, vkey, 0 });
}
return nullptr == _GetActionByKeyChordInternal({ modifiers, 0, keys.ScanCode() });
}
// Method Description:
// - Retrieves the assigned command that can be invoked with the given key chord
// Arguments:

View file

@ -61,6 +61,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// queries
Model::Command GetActionByKeyChord(Control::KeyChord const& keys) const;
bool IsKeyChordExplicitlyUnbound(Control::KeyChord const& keys) const;
Control::KeyChord GetKeyBindingForAction(ShortcutAction const& action) const;
Control::KeyChord GetKeyBindingForAction(ShortcutAction const& action, IActionArgs const& actionArgs) const;

View file

@ -8,6 +8,8 @@ namespace Microsoft.Terminal.Settings.Model
// This interface ensures that no changes are made to ActionMap
interface IActionMapView
{
Boolean IsKeyChordExplicitlyUnbound(Microsoft.Terminal.Control.KeyChord keys);
Command GetActionByKeyChord(Microsoft.Terminal.Control.KeyChord keys);
Microsoft.Terminal.Control.KeyChord GetKeyBindingForAction(ShortcutAction action);

View file

@ -29,7 +29,6 @@ namespace TerminalCoreUnitTests
};
TEST_METHOD(AltShiftKey);
TEST_METHOD(AltSpace);
TEST_METHOD(InvalidKeyEvent);
void _VerifyExpectedInput(std::wstring& actualInput)
@ -57,16 +56,6 @@ namespace TerminalCoreUnitTests
VERIFY_IS_TRUE(term.SendCharEvent(L'A', 0, ControlKeyStates::LeftAltPressed | ControlKeyStates::ShiftPressed));
}
void InputTest::AltSpace()
{
// Make sure we don't handle Alt+Space. The system will use this to
// bring up the system menu for restore, min/maximize, size, move,
// close
VERIFY_IS_FALSE(term.SendKeyEvent(L' ', 0, ControlKeyStates::LeftAltPressed, true));
VERIFY_IS_FALSE(term.SendKeyEvent(L' ', 0, ControlKeyStates::LeftAltPressed, false));
VERIFY_IS_FALSE(term.SendCharEvent(L' ', 0, ControlKeyStates::LeftAltPressed));
}
void InputTest::InvalidKeyEvent()
{
// Certain applications like AutoHotKey and its keyboard remapping feature,

View file

@ -82,6 +82,10 @@ static bool _messageIsAltKeyup(const MSG& message)
{
return (message.message == WM_KEYUP || message.message == WM_SYSKEYUP) && message.wParam == VK_MENU;
}
static bool _messageIsAltSpaceKeypress(const MSG& message)
{
return message.message == WM_SYSKEYDOWN && message.wParam == VK_SPACE;
}
int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
{
@ -171,6 +175,18 @@ int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
}
}
// GH#7125 = System XAML will show a system dialog on Alt Space. We might want to
// explicitly prevent that - for example when an action is bound to it. So similar to
// above, we steal the event and hand it off to the host. When the host does not process
// it, we will still dispatch like normal.
if (_messageIsAltSpaceKeypress(message))
{
if (host.OnDirectKeyEvent(VK_SPACE, LOBYTE(HIWORD(message.lParam)), true))
{
continue;
}
}
TranslateMessage(&message);
DispatchMessage(&message);
}