Skip to content

Commit 6d9fb78

Browse files
lheckerDHowett
authored andcommitted
Implement Alt-Numpad handling (#17637)
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
1 parent 102181f commit 6d9fb78

File tree

2 files changed

+172
-80
lines changed

2 files changed

+172
-80
lines changed

src/cascadia/TerminalControl/TermControl.cpp

+154-80
Original file line numberDiff line numberDiff line change
@@ -1410,61 +1410,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
14101410
// - Whether the key was handled.
14111411
bool TermControl::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down)
14121412
{
1413-
// Short-circuit isReadOnly check to avoid warning dialog
1414-
if (_core.IsInReadOnlyMode())
1415-
{
1416-
return false;
1417-
}
1418-
14191413
const auto modifiers{ _GetPressedModifierKeys() };
1420-
auto handled = false;
1421-
1422-
if (vkey == VK_MENU && !down)
1423-
{
1424-
// Manually generate an Alt KeyUp event into the key bindings or terminal.
1425-
// This is required as part of GH#6421.
1426-
(void)_TrySendKeyEvent(VK_MENU, scanCode, modifiers, false);
1427-
handled = true;
1428-
}
1429-
else if ((vkey == VK_F7 || vkey == VK_SPACE) && down)
1430-
{
1431-
// Manually generate an F7 event into the key bindings or terminal.
1432-
// This is required as part of GH#638.
1433-
// Or do so for alt+space; only send to terminal when explicitly unbound
1434-
// That is part of #GH7125
1435-
auto bindings{ _core.Settings().KeyBindings() };
1436-
auto isUnbound = false;
1437-
const KeyChord kc = {
1438-
modifiers.IsCtrlPressed(),
1439-
modifiers.IsAltPressed(),
1440-
modifiers.IsShiftPressed(),
1441-
modifiers.IsWinPressed(),
1442-
gsl::narrow_cast<WORD>(vkey),
1443-
0
1444-
};
1445-
1446-
if (bindings)
1447-
{
1448-
handled = bindings.TryKeyChord(kc);
1449-
1450-
if (!handled)
1451-
{
1452-
isUnbound = bindings.IsKeyChordExplicitlyUnbound(kc);
1453-
}
1454-
}
1455-
1456-
const auto sendToTerminal = vkey == VK_F7 || (vkey == VK_SPACE && isUnbound);
1457-
1458-
if (!handled && sendToTerminal)
1459-
{
1460-
// _TrySendKeyEvent pretends it didn't handle F7 for some unknown reason.
1461-
(void)_TrySendKeyEvent(gsl::narrow_cast<WORD>(vkey), scanCode, modifiers, true);
1462-
// GH#6438: Note that we're _not_ sending the key up here - that'll
1463-
// get passed through XAML to our KeyUp handler normally.
1464-
handled = true;
1465-
}
1466-
}
1467-
return handled;
1414+
return _KeyHandler(gsl::narrow_cast<WORD>(vkey), gsl::narrow_cast<WORD>(scanCode), modifiers, down);
14681415
}
14691416

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

14821429
void TermControl::_KeyHandler(const Input::KeyRoutedEventArgs& e, const bool keyDown)
14831430
{
1484-
// If the current focused element is a child element of searchbox,
1485-
// we do not send this event up to terminal
1486-
if (_searchBox && _searchBox->ContainsFocus())
1487-
{
1488-
return;
1489-
}
1490-
14911431
const auto keyStatus = e.KeyStatus();
14921432
const auto vkey = gsl::narrow_cast<WORD>(e.OriginalKey());
14931433
const auto scanCode = gsl::narrow_cast<WORD>(keyStatus.ScanCode);
@@ -1498,6 +1438,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation
14981438
modifiers |= ControlKeyStates::EnhancedKey;
14991439
}
15001440

1441+
e.Handled(_KeyHandler(vkey, scanCode, modifiers, keyDown));
1442+
}
1443+
1444+
bool TermControl::_KeyHandler(WORD vkey, WORD scanCode, ControlKeyStates modifiers, bool keyDown)
1445+
{
1446+
// If the current focused element is a child element of searchbox,
1447+
// we do not send this event up to terminal
1448+
if (_searchBox && _searchBox->ContainsFocus())
1449+
{
1450+
return false;
1451+
}
1452+
15011453
// GH#11076:
15021454
// For some weird reason we sometimes receive a WM_KEYDOWN
15031455
// message without vkey or scanCode if a user drags a tab.
@@ -1506,8 +1458,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
15061458
// accidental insertion of invalid KeyChords into classes like ActionMap.
15071459
if (!vkey && !scanCode)
15081460
{
1509-
e.Handled(true);
1510-
return;
1461+
return true;
15111462
}
15121463

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

15271477
// Short-circuit isReadOnly check to avoid warning dialog
15281478
if (_core.IsInReadOnlyMode())
15291479
{
1530-
e.Handled(!keyDown || _TryHandleKeyBinding(vkey, scanCode, modifiers));
1531-
return;
1480+
return !keyDown || _TryHandleKeyBinding(vkey, scanCode, modifiers);
15321481
}
15331482

1534-
// Alt-Numpad# input will send us a character once the user releases
1535-
// Alt, so we should be ignoring the individual keydowns. The character
1536-
// will be sent through the TSFInputControl. See GH#1401 for more
1537-
// details
1538-
if (modifiers.IsAltPressed() && !modifiers.IsCtrlPressed() &&
1539-
(vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9))
1483+
// Our custom TSF input control doesn't receive Alt+Numpad inputs,
1484+
// and we don't receive any via WM_CHAR as a xaml island app either.
1485+
// So, we simply implement our own Alt-Numpad handling here.
1486+
//
1487+
// This handles the case where the Alt key is released.
1488+
// We'll flush any ongoing composition in that case.
1489+
if (vkey == VK_MENU && !keyDown && _altNumpadState.active)
15401490
{
1541-
e.Handled(true);
1542-
return;
1491+
auto& s = _altNumpadState;
1492+
auto encoding = s.encoding;
1493+
wchar_t buf[4]{};
1494+
size_t buf_len = 0;
1495+
1496+
if (encoding == AltNumpadEncoding::Unicode)
1497+
{
1498+
// UTF-32 -> UTF-16
1499+
if (s.accumulator <= 0xffff)
1500+
{
1501+
buf[buf_len++] = static_cast<uint16_t>(s.accumulator);
1502+
}
1503+
else
1504+
{
1505+
buf[buf_len++] = static_cast<uint16_t>((s.accumulator >> 10) + 0xd7c0);
1506+
buf[buf_len++] = static_cast<uint16_t>((s.accumulator & 0x3ff) | 0xdc00);
1507+
}
1508+
}
1509+
else
1510+
{
1511+
const auto ansi = encoding == AltNumpadEncoding::ANSI;
1512+
const auto acp = GetACP();
1513+
auto codepage = ansi ? acp : CP_OEMCP;
1514+
1515+
// Alt+Numpad inputs are always a single codepoint, be it UTF-32 or ANSI.
1516+
// Since DBCS code pages by definition are >1 codepoint, we can't encode those.
1517+
// Traditionally, the OS uses the Latin1 or IBM code page instead.
1518+
if (acp == CP_JAPANESE ||
1519+
acp == CP_CHINESE_SIMPLIFIED ||
1520+
acp == CP_KOREAN ||
1521+
acp == CP_CHINESE_TRADITIONAL ||
1522+
acp == CP_UTF8)
1523+
{
1524+
codepage = ansi ? 1252 : 437;
1525+
}
1526+
1527+
// The OS code seemed to also simply cut off the last byte in the accumulator.
1528+
const auto ch = gsl::narrow_cast<char>(s.accumulator & 0xff);
1529+
const auto len = MultiByteToWideChar(codepage, 0, &ch, 1, &buf[0], 2);
1530+
buf_len = gsl::narrow_cast<size_t>(std::max(0, len));
1531+
}
1532+
1533+
if (buf_len != 0)
1534+
{
1535+
// WinRT always needs null-terminated strings, because HSTRING is dumb.
1536+
// If it encounters a string that isn't, cppwinrt will abort().
1537+
// It should already be null-terminated, but let's make sure to not crash.
1538+
buf[buf_len] = L'\0';
1539+
_core.SendInput(std::wstring_view{ &buf[0], buf_len });
1540+
}
1541+
1542+
s = {};
1543+
return true;
1544+
}
1545+
// As a continuation of the above, this handles the key-down case.
1546+
if (modifiers.IsAltPressed())
1547+
{
1548+
// The OS code seems to reset the composition if shift is pressed, but I couldn't
1549+
// figure out how exactly it worked. We'll simply ignore any such inputs.
1550+
static constexpr DWORD permittedModifiers =
1551+
RIGHT_ALT_PRESSED |
1552+
LEFT_ALT_PRESSED |
1553+
NUMLOCK_ON |
1554+
SCROLLLOCK_ON |
1555+
CAPSLOCK_ON;
1556+
1557+
if (keyDown && (modifiers.Value() & ~permittedModifiers) == 0)
1558+
{
1559+
auto& s = _altNumpadState;
1560+
1561+
if (vkey == VK_ADD)
1562+
{
1563+
// Alt '+' <number> is used to input Unicode code points.
1564+
// Every time you press + it resets the entire state
1565+
// in the original OS implementation as well.
1566+
s.encoding = AltNumpadEncoding::Unicode;
1567+
s.accumulator = 0;
1568+
s.active = true;
1569+
}
1570+
else if (vkey == VK_NUMPAD0 && s.encoding == AltNumpadEncoding::OEM && s.accumulator == 0)
1571+
{
1572+
// Alt '0' <number> is used to input ANSI code points.
1573+
// Otherwise, they're OEM codepoints.
1574+
s.encoding = AltNumpadEncoding::ANSI;
1575+
s.active = true;
1576+
}
1577+
else
1578+
{
1579+
// Otherwise, append the pressed key to the accumulator.
1580+
const uint32_t base = s.encoding == AltNumpadEncoding::Unicode ? 16 : 10;
1581+
uint32_t add = 0xffffff;
1582+
1583+
if (vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9)
1584+
{
1585+
add = vkey - VK_NUMPAD0;
1586+
}
1587+
else if (vkey >= 'A' && vkey <= 'F')
1588+
{
1589+
add = vkey - 'A' + 10;
1590+
}
1591+
1592+
// Pressing Alt + <not a number> should not activate the Alt+Numpad input, however.
1593+
if (add < base)
1594+
{
1595+
s.accumulator = std::min(s.accumulator * base + add, 0x10FFFFu);
1596+
s.active = true;
1597+
}
1598+
}
1599+
1600+
// If someone pressed Alt + <not a number>, we'll skip the early
1601+
// return and send the Alt key combination as per usual.
1602+
if (s.active)
1603+
{
1604+
return true;
1605+
}
1606+
1607+
// Unless I didn't code the above correctly, active == false should imply
1608+
// that _altNumpadState is in the (default constructed) base state.
1609+
assert(s.encoding == AltNumpadEncoding::OEM);
1610+
assert(s.accumulator == 0);
1611+
}
1612+
}
1613+
else if (_altNumpadState.active)
1614+
{
1615+
// If the user Alt+Tabbed in the middle of an Alt+Numpad sequence, we'll not receive a key-up event for
1616+
// the Alt key. There are several ways to detect this. Here, we simply check if the user typed another
1617+
// character, it's not an alt-up event, and we still have an ongoing composition.
1618+
_altNumpadState = {};
15431619
}
15441620

15451621
// GH#2235: Terminal::Settings hasn't been modified to differentiate
@@ -1555,20 +1631,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation
15551631
keyDown &&
15561632
_TryHandleKeyBinding(vkey, scanCode, modifiers))
15571633
{
1558-
e.Handled(true);
1559-
return;
1634+
return true;
15601635
}
15611636

15621637
if (_TrySendKeyEvent(vkey, scanCode, modifiers, keyDown))
15631638
{
1564-
e.Handled(true);
1565-
return;
1639+
return true;
15661640
}
15671641

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

15741648
// Method Description:

src/cascadia/TerminalControl/TermControl.h

+18
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,23 @@ namespace winrt::Microsoft::Terminal::Control::implementation
239239
TsfDataProvider _tsfDataProvider{ this };
240240
winrt::com_ptr<SearchBoxControl> _searchBox;
241241

242+
enum class AltNumpadEncoding
243+
{
244+
OEM,
245+
ANSI,
246+
Unicode,
247+
};
248+
struct AltNumpadState
249+
{
250+
AltNumpadEncoding encoding = AltNumpadEncoding::OEM;
251+
uint32_t accumulator = 0;
252+
// Checking for accumulator != 0 to see if we have an ongoing Alt+Numpad composition is insufficient.
253+
// The state can be active, while the accumulator is 0, if the user pressed Alt+Numpad0 which enabled
254+
// the OEM encoding mode (= active), and then pressed Numpad0 again (= accumulator is still 0).
255+
bool active = false;
256+
};
257+
AltNumpadState _altNumpadState;
258+
242259
bool _closing{ false };
243260
bool _focused{ false };
244261
bool _initializedTerminal{ false };
@@ -361,6 +378,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
361378
void _UpdateAutoScroll(const Windows::Foundation::IInspectable& sender, const Windows::Foundation::IInspectable& e);
362379

363380
void _KeyHandler(const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e, const bool keyDown);
381+
bool _KeyHandler(WORD vkey, WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers, bool keyDown);
364382
static ::Microsoft::Terminal::Core::ControlKeyStates _GetPressedModifierKeys() noexcept;
365383
bool _TryHandleKeyBinding(const WORD vkey, const WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers) const;
366384
static void _ClearKeyboardState(const WORD vkey, const WORD scanCode) noexcept;

0 commit comments

Comments
 (0)