diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index be9de020b54..0e571132752 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1454,61 +1454,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(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(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(vkey), gsl::narrow_cast(scanCode), modifiers, down); } void TermControl::_KeyDownHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, @@ -1525,13 +1472,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(e.OriginalKey()); const auto scanCode = gsl::narrow_cast(keyStatus.ScanCode); @@ -1542,6 +1482,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. @@ -1550,8 +1502,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 @@ -1564,26 +1515,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(s.accumulator); + } + else + { + buf[buf_len++] = static_cast((s.accumulator >> 10) + 0xd7c0); + buf[buf_len++] = static_cast((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(s.accumulator & 0xff); + const auto len = MultiByteToWideChar(codepage, 0, &ch, 1, &buf[0], 2); + buf_len = gsl::narrow_cast(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 '+' 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' 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 + 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 + , 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 @@ -1599,20 +1675,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: diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index e8cf2cd20f6..d1b53f01dd6 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -248,6 +248,23 @@ namespace winrt::Microsoft::Terminal::Control::implementation TsfDataProvider _tsfDataProvider{ this }; winrt::com_ptr _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 }; @@ -375,6 +392,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;