Skip to content

Commit 2fab986

Browse files
authored
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~`
1 parent 0bafab9 commit 2fab986

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
@@ -1458,61 +1458,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
14581458
// - Whether the key was handled.
14591459
bool TermControl::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down)
14601460
{
1461-
// Short-circuit isReadOnly check to avoid warning dialog
1462-
if (_core.IsInReadOnlyMode())
1463-
{
1464-
return false;
1465-
}
1466-
14671461
const auto modifiers{ _GetPressedModifierKeys() };
1468-
auto handled = false;
1469-
1470-
if (vkey == VK_MENU && !down)
1471-
{
1472-
// Manually generate an Alt KeyUp event into the key bindings or terminal.
1473-
// This is required as part of GH#6421.
1474-
(void)_TrySendKeyEvent(VK_MENU, scanCode, modifiers, false);
1475-
handled = true;
1476-
}
1477-
else if ((vkey == VK_F7 || vkey == VK_SPACE) && down)
1478-
{
1479-
// Manually generate an F7 event into the key bindings or terminal.
1480-
// This is required as part of GH#638.
1481-
// Or do so for alt+space; only send to terminal when explicitly unbound
1482-
// That is part of #GH7125
1483-
auto bindings{ _core.Settings().KeyBindings() };
1484-
auto isUnbound = false;
1485-
const KeyChord kc = {
1486-
modifiers.IsCtrlPressed(),
1487-
modifiers.IsAltPressed(),
1488-
modifiers.IsShiftPressed(),
1489-
modifiers.IsWinPressed(),
1490-
gsl::narrow_cast<WORD>(vkey),
1491-
0
1492-
};
1493-
1494-
if (bindings)
1495-
{
1496-
handled = bindings.TryKeyChord(kc);
1497-
1498-
if (!handled)
1499-
{
1500-
isUnbound = bindings.IsKeyChordExplicitlyUnbound(kc);
1501-
}
1502-
}
1503-
1504-
const auto sendToTerminal = vkey == VK_F7 || (vkey == VK_SPACE && isUnbound);
1505-
1506-
if (!handled && sendToTerminal)
1507-
{
1508-
// _TrySendKeyEvent pretends it didn't handle F7 for some unknown reason.
1509-
(void)_TrySendKeyEvent(gsl::narrow_cast<WORD>(vkey), scanCode, modifiers, true);
1510-
// GH#6438: Note that we're _not_ sending the key up here - that'll
1511-
// get passed through XAML to our KeyUp handler normally.
1512-
handled = true;
1513-
}
1514-
}
1515-
return handled;
1462+
return _KeyHandler(gsl::narrow_cast<WORD>(vkey), gsl::narrow_cast<WORD>(scanCode), modifiers, down);
15161463
}
15171464

15181465
void TermControl::_KeyDownHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
@@ -1529,13 +1476,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
15291476

15301477
void TermControl::_KeyHandler(const Input::KeyRoutedEventArgs& e, const bool keyDown)
15311478
{
1532-
// If the current focused element is a child element of searchbox,
1533-
// we do not send this event up to terminal
1534-
if (_searchBox && _searchBox->ContainsFocus())
1535-
{
1536-
return;
1537-
}
1538-
15391479
const auto keyStatus = e.KeyStatus();
15401480
const auto vkey = gsl::narrow_cast<WORD>(e.OriginalKey());
15411481
const auto scanCode = gsl::narrow_cast<WORD>(keyStatus.ScanCode);
@@ -1546,6 +1486,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation
15461486
modifiers |= ControlKeyStates::EnhancedKey;
15471487
}
15481488

1489+
e.Handled(_KeyHandler(vkey, scanCode, modifiers, keyDown));
1490+
}
1491+
1492+
bool TermControl::_KeyHandler(WORD vkey, WORD scanCode, ControlKeyStates modifiers, bool keyDown)
1493+
{
1494+
// If the current focused element is a child element of searchbox,
1495+
// we do not send this event up to terminal
1496+
if (_searchBox && _searchBox->ContainsFocus())
1497+
{
1498+
return false;
1499+
}
1500+
15491501
// GH#11076:
15501502
// For some weird reason we sometimes receive a WM_KEYDOWN
15511503
// message without vkey or scanCode if a user drags a tab.
@@ -1554,8 +1506,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
15541506
// accidental insertion of invalid KeyChords into classes like ActionMap.
15551507
if (!vkey && !scanCode)
15561508
{
1557-
e.Handled(true);
1558-
return;
1509+
return true;
15591510
}
15601511

15611512
// Mark the event as handled and do nothing if we're closing, or the key
@@ -1568,26 +1519,151 @@ namespace winrt::Microsoft::Terminal::Control::implementation
15681519
// about.
15691520
if (_IsClosing() || vkey == VK_LWIN || vkey == VK_RWIN)
15701521
{
1571-
e.Handled(true);
1572-
return;
1522+
return true;
15731523
}
15741524

15751525
// Short-circuit isReadOnly check to avoid warning dialog
15761526
if (_core.IsInReadOnlyMode())
15771527
{
1578-
e.Handled(!keyDown || _TryHandleKeyBinding(vkey, scanCode, modifiers));
1579-
return;
1528+
return !keyDown || _TryHandleKeyBinding(vkey, scanCode, modifiers);
15801529
}
15811530

1582-
// Alt-Numpad# input will send us a character once the user releases
1583-
// Alt, so we should be ignoring the individual keydowns. The character
1584-
// will be sent through the TSFInputControl. See GH#1401 for more
1585-
// details
1586-
if (modifiers.IsAltPressed() && !modifiers.IsCtrlPressed() &&
1587-
(vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9))
1531+
// Our custom TSF input control doesn't receive Alt+Numpad inputs,
1532+
// and we don't receive any via WM_CHAR as a xaml island app either.
1533+
// So, we simply implement our own Alt-Numpad handling here.
1534+
//
1535+
// This handles the case where the Alt key is released.
1536+
// We'll flush any ongoing composition in that case.
1537+
if (vkey == VK_MENU && !keyDown && _altNumpadState.active)
15881538
{
1589-
e.Handled(true);
1590-
return;
1539+
auto& s = _altNumpadState;
1540+
auto encoding = s.encoding;
1541+
wchar_t buf[4]{};
1542+
size_t buf_len = 0;
1543+
1544+
if (encoding == AltNumpadEncoding::Unicode)
1545+
{
1546+
// UTF-32 -> UTF-16
1547+
if (s.accumulator <= 0xffff)
1548+
{
1549+
buf[buf_len++] = static_cast<uint16_t>(s.accumulator);
1550+
}
1551+
else
1552+
{
1553+
buf[buf_len++] = static_cast<uint16_t>((s.accumulator >> 10) + 0xd7c0);
1554+
buf[buf_len++] = static_cast<uint16_t>((s.accumulator & 0x3ff) | 0xdc00);
1555+
}
1556+
}
1557+
else
1558+
{
1559+
const auto ansi = encoding == AltNumpadEncoding::ANSI;
1560+
const auto acp = GetACP();
1561+
auto codepage = ansi ? acp : CP_OEMCP;
1562+
1563+
// Alt+Numpad inputs are always a single codepoint, be it UTF-32 or ANSI.
1564+
// Since DBCS code pages by definition are >1 codepoint, we can't encode those.
1565+
// Traditionally, the OS uses the Latin1 or IBM code page instead.
1566+
if (acp == CP_JAPANESE ||
1567+
acp == CP_CHINESE_SIMPLIFIED ||
1568+
acp == CP_KOREAN ||
1569+
acp == CP_CHINESE_TRADITIONAL ||
1570+
acp == CP_UTF8)
1571+
{
1572+
codepage = ansi ? 1252 : 437;
1573+
}
1574+
1575+
// The OS code seemed to also simply cut off the last byte in the accumulator.
1576+
const auto ch = gsl::narrow_cast<char>(s.accumulator & 0xff);
1577+
const auto len = MultiByteToWideChar(codepage, 0, &ch, 1, &buf[0], 2);
1578+
buf_len = gsl::narrow_cast<size_t>(std::max(0, len));
1579+
}
1580+
1581+
if (buf_len != 0)
1582+
{
1583+
// WinRT always needs null-terminated strings, because HSTRING is dumb.
1584+
// If it encounters a string that isn't, cppwinrt will abort().
1585+
// It should already be null-terminated, but let's make sure to not crash.
1586+
buf[buf_len] = L'\0';
1587+
_core.SendInput(std::wstring_view{ &buf[0], buf_len });
1588+
}
1589+
1590+
s = {};
1591+
return true;
1592+
}
1593+
// As a continuation of the above, this handles the key-down case.
1594+
if (modifiers.IsAltPressed())
1595+
{
1596+
// The OS code seems to reset the composition if shift is pressed, but I couldn't
1597+
// figure out how exactly it worked. We'll simply ignore any such inputs.
1598+
static constexpr DWORD permittedModifiers =
1599+
RIGHT_ALT_PRESSED |
1600+
LEFT_ALT_PRESSED |
1601+
NUMLOCK_ON |
1602+
SCROLLLOCK_ON |
1603+
CAPSLOCK_ON;
1604+
1605+
if (keyDown && (modifiers.Value() & ~permittedModifiers) == 0)
1606+
{
1607+
auto& s = _altNumpadState;
1608+
1609+
if (vkey == VK_ADD)
1610+
{
1611+
// Alt '+' <number> is used to input Unicode code points.
1612+
// Every time you press + it resets the entire state
1613+
// in the original OS implementation as well.
1614+
s.encoding = AltNumpadEncoding::Unicode;
1615+
s.accumulator = 0;
1616+
s.active = true;
1617+
}
1618+
else if (vkey == VK_NUMPAD0 && s.encoding == AltNumpadEncoding::OEM && s.accumulator == 0)
1619+
{
1620+
// Alt '0' <number> is used to input ANSI code points.
1621+
// Otherwise, they're OEM codepoints.
1622+
s.encoding = AltNumpadEncoding::ANSI;
1623+
s.active = true;
1624+
}
1625+
else
1626+
{
1627+
// Otherwise, append the pressed key to the accumulator.
1628+
const uint32_t base = s.encoding == AltNumpadEncoding::Unicode ? 16 : 10;
1629+
uint32_t add = 0xffffff;
1630+
1631+
if (vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9)
1632+
{
1633+
add = vkey - VK_NUMPAD0;
1634+
}
1635+
else if (vkey >= 'A' && vkey <= 'F')
1636+
{
1637+
add = vkey - 'A' + 10;
1638+
}
1639+
1640+
// Pressing Alt + <not a number> should not activate the Alt+Numpad input, however.
1641+
if (add < base)
1642+
{
1643+
s.accumulator = std::min(s.accumulator * base + add, 0x10FFFFu);
1644+
s.active = true;
1645+
}
1646+
}
1647+
1648+
// If someone pressed Alt + <not a number>, we'll skip the early
1649+
// return and send the Alt key combination as per usual.
1650+
if (s.active)
1651+
{
1652+
return true;
1653+
}
1654+
1655+
// Unless I didn't code the above correctly, active == false should imply
1656+
// that _altNumpadState is in the (default constructed) base state.
1657+
assert(s.encoding == AltNumpadEncoding::OEM);
1658+
assert(s.accumulator == 0);
1659+
}
1660+
}
1661+
else if (_altNumpadState.active)
1662+
{
1663+
// If the user Alt+Tabbed in the middle of an Alt+Numpad sequence, we'll not receive a key-up event for
1664+
// the Alt key. There are several ways to detect this. Here, we simply check if the user typed another
1665+
// character, it's not an alt-up event, and we still have an ongoing composition.
1666+
_altNumpadState = {};
15911667
}
15921668

15931669
// GH#2235: Terminal::Settings hasn't been modified to differentiate
@@ -1603,20 +1679,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation
16031679
keyDown &&
16041680
_TryHandleKeyBinding(vkey, scanCode, modifiers))
16051681
{
1606-
e.Handled(true);
1607-
return;
1682+
return true;
16081683
}
16091684

16101685
if (_TrySendKeyEvent(vkey, scanCode, modifiers, keyDown))
16111686
{
1612-
e.Handled(true);
1613-
return;
1687+
return true;
16141688
}
16151689

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

16221696
// Method Description:

src/cascadia/TerminalControl/TermControl.h

+18
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,23 @@ namespace winrt::Microsoft::Terminal::Control::implementation
248248
TsfDataProvider _tsfDataProvider{ this };
249249
winrt::com_ptr<SearchBoxControl> _searchBox;
250250

251+
enum class AltNumpadEncoding
252+
{
253+
OEM,
254+
ANSI,
255+
Unicode,
256+
};
257+
struct AltNumpadState
258+
{
259+
AltNumpadEncoding encoding = AltNumpadEncoding::OEM;
260+
uint32_t accumulator = 0;
261+
// Checking for accumulator != 0 to see if we have an ongoing Alt+Numpad composition is insufficient.
262+
// The state can be active, while the accumulator is 0, if the user pressed Alt+Numpad0 which enabled
263+
// the OEM encoding mode (= active), and then pressed Numpad0 again (= accumulator is still 0).
264+
bool active = false;
265+
};
266+
AltNumpadState _altNumpadState;
267+
251268
bool _closing{ false };
252269
bool _focused{ false };
253270
bool _initializedTerminal{ false };
@@ -376,6 +393,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
376393
void _UpdateAutoScroll(const Windows::Foundation::IInspectable& sender, const Windows::Foundation::IInspectable& e);
377394

378395
void _KeyHandler(const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e, const bool keyDown);
396+
bool _KeyHandler(WORD vkey, WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers, bool keyDown);
379397
static ::Microsoft::Terminal::Core::ControlKeyStates _GetPressedModifierKeys() noexcept;
380398
bool _TryHandleKeyBinding(const WORD vkey, const WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers) const;
381399
static void _ClearKeyboardState(const WORD vkey, const WORD scanCode) noexcept;

0 commit comments

Comments
 (0)