Skip to content

Commit

Permalink
Implement Alt-Numpad handling (#17637)
Browse files Browse the repository at this point in the history
This adds an indirection for `_KeyHandler` so that `OnDirectKeyEvent`
can call `_KeyHandler`. This allows us to consistently handle
Alt-key-up events. Then I added custom handling for Alt+ddd (OEM),
Alt+0ddd (ANSI), and Alt+'+'+xxxx (Unicode) sequences, due to the
absence of Alt-key events with xaml islands and our TSF control.

Closes #17327

## Validation Steps Performed
* Tested it according to https://conemu.github.io/en/AltNumpad.html
* Unbind Alt+Space
* Run `showkey -a`
* Alt+Space generates `^[ `
* F7 generates `^[[18~`

(cherry picked from commit 2fab986)
Service-Card-Id: PVTI_lADOAF3p4s4AmhmszgSCpCg
Service-Version: 1.21
  • Loading branch information
lhecker authored and DHowett committed Aug 23, 2024
1 parent 102181f commit 6d9fb78
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 80 deletions.
234 changes: 154 additions & 80 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1410,61 +1410,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// - Whether the key was handled.
bool TermControl::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down)
{
// Short-circuit isReadOnly check to avoid warning dialog
if (_core.IsInReadOnlyMode())
{
return false;
}

const auto modifiers{ _GetPressedModifierKeys() };
auto handled = false;

if (vkey == VK_MENU && !down)
{
// Manually generate an Alt KeyUp event into the key bindings or terminal.
// This is required as part of GH#6421.
(void)_TrySendKeyEvent(VK_MENU, scanCode, modifiers, false);
handled = true;
}
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{ _core.Settings().KeyBindings() };
auto isUnbound = false;
const KeyChord kc = {
modifiers.IsCtrlPressed(),
modifiers.IsAltPressed(),
modifiers.IsShiftPressed(),
modifiers.IsWinPressed(),
gsl::narrow_cast<WORD>(vkey),
0
};

if (bindings)
{
handled = bindings.TryKeyChord(kc);

if (!handled)
{
isUnbound = bindings.IsKeyChordExplicitlyUnbound(kc);
}
}

const auto sendToTerminal = vkey == VK_F7 || (vkey == VK_SPACE && isUnbound);

if (!handled && sendToTerminal)
{
// _TrySendKeyEvent pretends it didn't handle F7 for some unknown reason.
(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;
}
}
return handled;
return _KeyHandler(gsl::narrow_cast<WORD>(vkey), gsl::narrow_cast<WORD>(scanCode), modifiers, down);
}

void TermControl::_KeyDownHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
Expand All @@ -1481,13 +1428,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation

void TermControl::_KeyHandler(const Input::KeyRoutedEventArgs& e, const bool keyDown)
{
// If the current focused element is a child element of searchbox,
// we do not send this event up to terminal
if (_searchBox && _searchBox->ContainsFocus())
{
return;
}

const auto keyStatus = e.KeyStatus();
const auto vkey = gsl::narrow_cast<WORD>(e.OriginalKey());
const auto scanCode = gsl::narrow_cast<WORD>(keyStatus.ScanCode);
Expand All @@ -1498,6 +1438,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation
modifiers |= ControlKeyStates::EnhancedKey;
}

e.Handled(_KeyHandler(vkey, scanCode, modifiers, keyDown));
}

bool TermControl::_KeyHandler(WORD vkey, WORD scanCode, ControlKeyStates modifiers, bool keyDown)
{
// If the current focused element is a child element of searchbox,
// we do not send this event up to terminal
if (_searchBox && _searchBox->ContainsFocus())
{
return false;
}

// GH#11076:
// For some weird reason we sometimes receive a WM_KEYDOWN
// message without vkey or scanCode if a user drags a tab.
Expand All @@ -1506,8 +1458,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// accidental insertion of invalid KeyChords into classes like ActionMap.
if (!vkey && !scanCode)
{
e.Handled(true);
return;
return true;
}

// Mark the event as handled and do nothing if we're closing, or the key
Expand All @@ -1520,26 +1471,151 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// about.
if (_IsClosing() || vkey == VK_LWIN || vkey == VK_RWIN)
{
e.Handled(true);
return;
return true;
}

// Short-circuit isReadOnly check to avoid warning dialog
if (_core.IsInReadOnlyMode())
{
e.Handled(!keyDown || _TryHandleKeyBinding(vkey, scanCode, modifiers));
return;
return !keyDown || _TryHandleKeyBinding(vkey, scanCode, modifiers);
}

// Alt-Numpad# input will send us a character once the user releases
// Alt, so we should be ignoring the individual keydowns. The character
// will be sent through the TSFInputControl. See GH#1401 for more
// details
if (modifiers.IsAltPressed() && !modifiers.IsCtrlPressed() &&
(vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9))
// Our custom TSF input control doesn't receive Alt+Numpad inputs,
// and we don't receive any via WM_CHAR as a xaml island app either.
// So, we simply implement our own Alt-Numpad handling here.
//
// This handles the case where the Alt key is released.
// We'll flush any ongoing composition in that case.
if (vkey == VK_MENU && !keyDown && _altNumpadState.active)
{
e.Handled(true);
return;
auto& s = _altNumpadState;
auto encoding = s.encoding;
wchar_t buf[4]{};
size_t buf_len = 0;

if (encoding == AltNumpadEncoding::Unicode)
{
// UTF-32 -> UTF-16
if (s.accumulator <= 0xffff)
{
buf[buf_len++] = static_cast<uint16_t>(s.accumulator);
}
else
{
buf[buf_len++] = static_cast<uint16_t>((s.accumulator >> 10) + 0xd7c0);
buf[buf_len++] = static_cast<uint16_t>((s.accumulator & 0x3ff) | 0xdc00);
}
}
else
{
const auto ansi = encoding == AltNumpadEncoding::ANSI;
const auto acp = GetACP();
auto codepage = ansi ? acp : CP_OEMCP;

// Alt+Numpad inputs are always a single codepoint, be it UTF-32 or ANSI.
// Since DBCS code pages by definition are >1 codepoint, we can't encode those.
// Traditionally, the OS uses the Latin1 or IBM code page instead.
if (acp == CP_JAPANESE ||
acp == CP_CHINESE_SIMPLIFIED ||
acp == CP_KOREAN ||
acp == CP_CHINESE_TRADITIONAL ||
acp == CP_UTF8)
{
codepage = ansi ? 1252 : 437;
}

// The OS code seemed to also simply cut off the last byte in the accumulator.
const auto ch = gsl::narrow_cast<char>(s.accumulator & 0xff);
const auto len = MultiByteToWideChar(codepage, 0, &ch, 1, &buf[0], 2);
buf_len = gsl::narrow_cast<size_t>(std::max(0, len));
}

if (buf_len != 0)
{
// WinRT always needs null-terminated strings, because HSTRING is dumb.
// If it encounters a string that isn't, cppwinrt will abort().
// It should already be null-terminated, but let's make sure to not crash.
buf[buf_len] = L'\0';
_core.SendInput(std::wstring_view{ &buf[0], buf_len });
}

s = {};
return true;
}
// As a continuation of the above, this handles the key-down case.
if (modifiers.IsAltPressed())
{
// The OS code seems to reset the composition if shift is pressed, but I couldn't
// figure out how exactly it worked. We'll simply ignore any such inputs.
static constexpr DWORD permittedModifiers =
RIGHT_ALT_PRESSED |
LEFT_ALT_PRESSED |
NUMLOCK_ON |
SCROLLLOCK_ON |
CAPSLOCK_ON;

if (keyDown && (modifiers.Value() & ~permittedModifiers) == 0)
{
auto& s = _altNumpadState;

if (vkey == VK_ADD)
{
// Alt '+' <number> is used to input Unicode code points.
// Every time you press + it resets the entire state
// in the original OS implementation as well.
s.encoding = AltNumpadEncoding::Unicode;
s.accumulator = 0;
s.active = true;
}
else if (vkey == VK_NUMPAD0 && s.encoding == AltNumpadEncoding::OEM && s.accumulator == 0)
{
// Alt '0' <number> is used to input ANSI code points.
// Otherwise, they're OEM codepoints.
s.encoding = AltNumpadEncoding::ANSI;
s.active = true;
}
else
{
// Otherwise, append the pressed key to the accumulator.
const uint32_t base = s.encoding == AltNumpadEncoding::Unicode ? 16 : 10;
uint32_t add = 0xffffff;

if (vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9)
{
add = vkey - VK_NUMPAD0;
}
else if (vkey >= 'A' && vkey <= 'F')
{
add = vkey - 'A' + 10;
}

// Pressing Alt + <not a number> should not activate the Alt+Numpad input, however.
if (add < base)
{
s.accumulator = std::min(s.accumulator * base + add, 0x10FFFFu);
s.active = true;
}
}

// If someone pressed Alt + <not a number>, we'll skip the early
// return and send the Alt key combination as per usual.
if (s.active)
{
return true;
}

// Unless I didn't code the above correctly, active == false should imply
// that _altNumpadState is in the (default constructed) base state.
assert(s.encoding == AltNumpadEncoding::OEM);
assert(s.accumulator == 0);
}
}
else if (_altNumpadState.active)
{
// If the user Alt+Tabbed in the middle of an Alt+Numpad sequence, we'll not receive a key-up event for
// the Alt key. There are several ways to detect this. Here, we simply check if the user typed another
// character, it's not an alt-up event, and we still have an ongoing composition.
_altNumpadState = {};
}

// GH#2235: Terminal::Settings hasn't been modified to differentiate
Expand All @@ -1555,20 +1631,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation
keyDown &&
_TryHandleKeyBinding(vkey, scanCode, modifiers))
{
e.Handled(true);
return;
return true;
}

if (_TrySendKeyEvent(vkey, scanCode, modifiers, keyDown))
{
e.Handled(true);
return;
return true;
}

// Manually prevent keyboard navigation with tab. We want to send tab to
// the terminal, and we don't want to be able to escape focus of the
// control with tab.
e.Handled(vkey == VK_TAB);
return vkey == VK_TAB;
}

// Method Description:
Expand Down
18 changes: 18 additions & 0 deletions src/cascadia/TerminalControl/TermControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,23 @@ namespace winrt::Microsoft::Terminal::Control::implementation
TsfDataProvider _tsfDataProvider{ this };
winrt::com_ptr<SearchBoxControl> _searchBox;

enum class AltNumpadEncoding
{
OEM,
ANSI,
Unicode,
};
struct AltNumpadState
{
AltNumpadEncoding encoding = AltNumpadEncoding::OEM;
uint32_t accumulator = 0;
// Checking for accumulator != 0 to see if we have an ongoing Alt+Numpad composition is insufficient.
// The state can be active, while the accumulator is 0, if the user pressed Alt+Numpad0 which enabled
// the OEM encoding mode (= active), and then pressed Numpad0 again (= accumulator is still 0).
bool active = false;
};
AltNumpadState _altNumpadState;

bool _closing{ false };
bool _focused{ false };
bool _initializedTerminal{ false };
Expand Down Expand Up @@ -361,6 +378,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void _UpdateAutoScroll(const Windows::Foundation::IInspectable& sender, const Windows::Foundation::IInspectable& e);

void _KeyHandler(const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e, const bool keyDown);
bool _KeyHandler(WORD vkey, WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers, bool keyDown);
static ::Microsoft::Terminal::Core::ControlKeyStates _GetPressedModifierKeys() noexcept;
bool _TryHandleKeyBinding(const WORD vkey, const WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers) const;
static void _ClearKeyboardState(const WORD vkey, const WORD scanCode) noexcept;
Expand Down

0 comments on commit 6d9fb78

Please sign in to comment.