From 0d61466afedf255cfe4ba117efec54320936961c Mon Sep 17 00:00:00 2001 From: PankajBhojwani Date: Mon, 24 May 2021 10:24:28 -0700 Subject: [PATCH] Pass through double clicks and hover events in Win32 mouse mode (#10138) Each mouse-down event's time and position is now stored, and if we process a left-mouse-down event at the same position as the previous one and within the double click time we set the double click flag. Also adds a case statement to `_UpdateSGRMouseButtonState` so that we send hover events instead of ignoring them. Note: The 'right-click menu in far manager shows up at the wrong location' bug still exists with this, as it seems to use the cursor position as told by user32. Related to #376 ## Validation Steps Performed Double click in far manager works, hover in far manager works (hovering over items in the right-click menu correctly highlights them) --- .../parser/InputStateMachineEngine.cpp | 47 ++++++++++-- .../parser/InputStateMachineEngine.hpp | 9 ++- .../parser/ut_parser/InputEngineTest.cpp | 76 +++++++++++++++++++ 3 files changed, 122 insertions(+), 10 deletions(-) diff --git a/src/terminal/parser/InputStateMachineEngine.cpp b/src/terminal/parser/InputStateMachineEngine.cpp index 53a7cab66f2..b06bfa84e82 100644 --- a/src/terminal/parser/InputStateMachineEngine.cpp +++ b/src/terminal/parser/InputStateMachineEngine.cpp @@ -99,7 +99,8 @@ InputStateMachineEngine::InputStateMachineEngine(std::unique_ptr pDispatch, const bool lookingForDSR) : _pDispatch(std::move(pDispatch)), _lookingForDSR(lookingForDSR), - _pfnFlushToInputQueue(nullptr) + _pfnFlushToInputQueue(nullptr), + _doubleClickTime(std::chrono::milliseconds(GetDoubleClickTime())) { THROW_HR_IF_NULL(E_INVALIDARG, _pDispatch.get()); } @@ -386,9 +387,11 @@ bool InputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParameter DWORD buttonState = 0; DWORD eventFlags = 0; const size_t firstParameter = parameters.at(0).value_or(0); + const til::point uiPos{ parameters.at(1) - 1, parameters.at(2) - 1 }; + modifierState = _GetSGRMouseModifierState(firstParameter); - success = _UpdateSGRMouseButtonState(id, firstParameter, buttonState, eventFlags); - success = success && _WriteMouseEvent(parameters.at(1), parameters.at(2), buttonState, modifierState, eventFlags); + success = _UpdateSGRMouseButtonState(id, firstParameter, buttonState, eventFlags, uiPos); + success = success && _WriteMouseEvent(uiPos, buttonState, modifierState, eventFlags); break; } // case CsiActionCodes::DSR_DeviceStatusReportResponse: @@ -723,10 +726,8 @@ bool InputStateMachineEngine::_WriteSingleKey(const short vkey, const DWORD modi // - eventFlags - the type of mouse event to write to the mouse record. // Return Value: // - true iff we successfully wrote the keypress to the input callback. -bool InputStateMachineEngine::_WriteMouseEvent(const size_t column, const size_t line, const DWORD buttonState, const DWORD controlKeyState, const DWORD eventFlags) +bool InputStateMachineEngine::_WriteMouseEvent(const til::point uiPos, const DWORD buttonState, const DWORD controlKeyState, const DWORD eventFlags) { - COORD uiPos = { gsl::narrow(column) - 1, gsl::narrow(line) - 1 }; - INPUT_RECORD rgInput; rgInput.EventType = MOUSE_EVENT; rgInput.Event.MouseEvent.dwMousePosition = uiPos; @@ -846,7 +847,8 @@ DWORD InputStateMachineEngine::_GetModifier(const size_t modifierParam) noexcept bool InputStateMachineEngine::_UpdateSGRMouseButtonState(const VTID id, const size_t sgrEncoding, DWORD& buttonState, - DWORD& eventFlags) noexcept + DWORD& eventFlags, + const til::point uiPos) { // Starting with the state from the last mouse event we received buttonState = _mouseButtonState; @@ -862,7 +864,7 @@ bool InputStateMachineEngine::_UpdateSGRMouseButtonState(const VTID id, // This retrieves the 2 MSBs and concatenates them to the 2 LSBs to create BBBB in binary // This represents which button had a change in state const auto buttonID = (sgrEncoding & 0x3) | ((sgrEncoding & 0xC0) >> 4); - + const auto currentTime = std::chrono::steady_clock::now(); // Step 1: Translate which button was affected // NOTE: if scrolled, having buttonFlag = 0 means // we don't actually update the buttonState @@ -894,6 +896,10 @@ bool InputStateMachineEngine::_UpdateSGRMouseButtonState(const VTID id, eventFlags |= MOUSE_WHEELED; break; } + case CsiMouseButtonCodes::Released: + // hover event, we still want to send these but we don't + // need to do anything special here, so just break + break; default: // no detectable buttonID, so we can't update the state return false; @@ -908,6 +914,31 @@ bool InputStateMachineEngine::_UpdateSGRMouseButtonState(const VTID id, // NOTE: scroll events have buttonFlag = 0 // so this intentionally does nothing buttonState |= buttonFlag; + // Check if this mouse down is a double click + // and also update our trackers for last clicked position, time and button + if (_lastMouseClickPos && _lastMouseClickTime && _lastMouseClickButton && + uiPos == _lastMouseClickPos && + (currentTime - _lastMouseClickTime.value()) < _doubleClickTime && + buttonID == _lastMouseClickButton) + { + // This was a double click, set the flag and reset our trackers + // for last clicked position, time and button (this is so we don't send + // another double click on a third click) + eventFlags |= DOUBLE_CLICK; + _lastMouseClickPos.reset(); + _lastMouseClickTime.reset(); + _lastMouseClickButton.reset(); + } + else if (buttonID == CsiMouseButtonCodes::Left || + buttonID == CsiMouseButtonCodes::Right || + buttonID == CsiMouseButtonCodes::Middle) + { + // This was a single click, update our trackers for last + // clicked position and time + _lastMouseClickPos = uiPos; + _lastMouseClickTime = currentTime; + _lastMouseClickButton = buttonID; + } break; case CsiActionCodes::MouseUp: // clear flag diff --git a/src/terminal/parser/InputStateMachineEngine.hpp b/src/terminal/parser/InputStateMachineEngine.hpp index 7336511e4e1..1a91077e1cc 100644 --- a/src/terminal/parser/InputStateMachineEngine.hpp +++ b/src/terminal/parser/InputStateMachineEngine.hpp @@ -170,6 +170,10 @@ namespace Microsoft::Console::VirtualTerminal std::function _pfnFlushToInputQueue; bool _lookingForDSR; DWORD _mouseButtonState = 0; + std::chrono::milliseconds _doubleClickTime; + std::optional _lastMouseClickPos{}; + std::optional _lastMouseClickTime{}; + std::optional _lastMouseClickButton{}; DWORD _GetCursorKeysModifierState(const VTParameters parameters, const VTID id) noexcept; DWORD _GetGenericKeysModifierState(const VTParameters parameters) noexcept; @@ -181,7 +185,8 @@ namespace Microsoft::Console::VirtualTerminal bool _UpdateSGRMouseButtonState(const VTID id, const size_t sgrEncoding, DWORD& buttonState, - DWORD& eventFlags) noexcept; + DWORD& eventFlags, + const til::point uiPos); bool _GetGenericVkey(const GenericKeyIdentifiers identifier, short& vkey) const; bool _GetCursorKeysVkey(const VTID id, short& vkey) const; bool _GetSs3KeysVkey(const wchar_t wch, short& vkey) const; @@ -189,7 +194,7 @@ namespace Microsoft::Console::VirtualTerminal bool _WriteSingleKey(const short vkey, const DWORD modifierState); bool _WriteSingleKey(const wchar_t wch, const short vkey, const DWORD modifierState); - bool _WriteMouseEvent(const size_t column, const size_t line, const DWORD buttonState, const DWORD controlKeyState, const DWORD eventFlags); + bool _WriteMouseEvent(const til::point uiPos, const DWORD buttonState, const DWORD controlKeyState, const DWORD eventFlags); void _GenerateWrappedSequence(const wchar_t wch, const short vkey, diff --git a/src/terminal/parser/ut_parser/InputEngineTest.cpp b/src/terminal/parser/ut_parser/InputEngineTest.cpp index b870e8ac986..cb1ef806251 100644 --- a/src/terminal/parser/ut_parser/InputEngineTest.cpp +++ b/src/terminal/parser/ut_parser/InputEngineTest.cpp @@ -272,6 +272,8 @@ class Microsoft::Console::VirtualTerminal::InputEngineTest TEST_METHOD(SGRMouseTest_Modifiers); TEST_METHOD(SGRMouseTest_Movement); TEST_METHOD(SGRMouseTest_Scroll); + TEST_METHOD(SGRMouseTest_DoubleClick); + TEST_METHOD(SGRMouseTest_Hover); TEST_METHOD(CtrlAltZCtrlAltXTest); TEST_METHOD(TestSs3Entry); TEST_METHOD(TestSs3Immediate); @@ -1249,6 +1251,80 @@ void InputEngineTest::SGRMouseTest_Scroll() VerifySGRMouseData(testData); } +void InputEngineTest::SGRMouseTest_DoubleClick() +{ + // SGR_PARAMS serves as test input + // - the state of the buttons (constructed via InputStateMachineEngine::CsiMouseButtonCodes) + // - the modifiers for the mouse event (constructed via InputStateMachineEngine::CsiMouseModifierCodes) + // - the {x,y} position of the event on the viewport where the top-left is {1,1} + // - the direction of the mouse press (constructed via InputStateMachineEngine::CsiActionCodes) + + // MOUSE_EVENT_PARAMS serves as expected output + // - buttonState + // - controlKeyState + // - mousePosition + // - eventFlags + + // clang-format off + const std::vector> testData = { + // TEST INPUT EXPECTED OUTPUT + { { CsiMouseButtonCodes::Left, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { FROM_LEFT_1ST_BUTTON_PRESSED, 0, { 0, 0 }, 0 } }, + { { CsiMouseButtonCodes::Left, 0, { 1, 1 }, CsiActionCodes::MouseUp }, { 0, 0, { 0, 0 }, 0 } }, + + { { CsiMouseButtonCodes::Left, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { FROM_LEFT_1ST_BUTTON_PRESSED, 0, { 0, 0 }, DOUBLE_CLICK } }, + { { CsiMouseButtonCodes::Left, 0, { 1, 1 }, CsiActionCodes::MouseUp }, { 0, 0, { 0, 0 }, 0 } }, + + { { CsiMouseButtonCodes::Left, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { FROM_LEFT_1ST_BUTTON_PRESSED, 0, { 0, 0 }, 0 } }, + { { CsiMouseButtonCodes::Left, 0, { 1, 1 }, CsiActionCodes::MouseUp }, { 0, 0, { 0, 0 }, 0 } }, + + { { CsiMouseButtonCodes::Middle, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { FROM_LEFT_2ND_BUTTON_PRESSED, 0, { 0, 0 }, 0 } }, + { { CsiMouseButtonCodes::Middle, 0, { 1, 1 }, CsiActionCodes::MouseUp }, { 0, 0, { 0, 0 }, 0 } }, + + { { CsiMouseButtonCodes::Middle, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { FROM_LEFT_2ND_BUTTON_PRESSED, 0, { 0, 0 }, DOUBLE_CLICK } }, + { { CsiMouseButtonCodes::Middle, 0, { 1, 1 }, CsiActionCodes::MouseUp }, { 0, 0, { 0, 0 }, 0 } }, + + { { CsiMouseButtonCodes::Middle, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { FROM_LEFT_2ND_BUTTON_PRESSED, 0, { 0, 0 }, 0 } }, + { { CsiMouseButtonCodes::Middle, 0, { 1, 1 }, CsiActionCodes::MouseUp }, { 0, 0, { 0, 0 }, 0 } }, + + { { CsiMouseButtonCodes::Right, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { RIGHTMOST_BUTTON_PRESSED, 0, { 0, 0 }, 0 } }, + { { CsiMouseButtonCodes::Right, 0, { 1, 1 }, CsiActionCodes::MouseUp }, { 0, 0, { 0, 0 }, 0 } }, + + { { CsiMouseButtonCodes::Right, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { RIGHTMOST_BUTTON_PRESSED, 0, { 0, 0 }, DOUBLE_CLICK } }, + { { CsiMouseButtonCodes::Right, 0, { 1, 1 }, CsiActionCodes::MouseUp }, { 0, 0, { 0, 0 }, 0 } }, + + { { CsiMouseButtonCodes::Right, 0, { 1, 1 }, CsiActionCodes::MouseDown }, { RIGHTMOST_BUTTON_PRESSED, 0, { 0, 0 }, 0 } }, + { { CsiMouseButtonCodes::Right, 0, { 1, 1 }, CsiActionCodes::MouseUp }, { 0, 0, { 0, 0 }, 0 } }, + }; + // clang-format on + + VerifySGRMouseData(testData); +} + +void InputEngineTest::SGRMouseTest_Hover() +{ + // SGR_PARAMS serves as test input + // - the state of the buttons (constructed via InputStateMachineEngine::CsiMouseButtonCodes) + // - the modifiers for the mouse event (constructed via InputStateMachineEngine::CsiMouseModifierCodes) + // - the {x,y} position of the event on the viewport where the top-left is {1,1} + // - the direction of the mouse press (constructed via InputStateMachineEngine::CsiActionCodes) + + // MOUSE_EVENT_PARAMS serves as expected output + // - buttonState + // - controlKeyState + // - mousePosition + // - eventFlags + + // clang-format off + const std::vector> testData = { + // TEST INPUT EXPECTED OUTPUT + { { CsiMouseButtonCodes::Released, CsiMouseModifierCodes::Drag, { 1, 1 }, CsiActionCodes::MouseUp }, { 0, 0, { 0, 0 }, MOUSE_MOVED } }, + { { CsiMouseButtonCodes::Released, CsiMouseModifierCodes::Drag, { 2, 2 }, CsiActionCodes::MouseUp }, { 0, 0, { 1, 1 }, MOUSE_MOVED } }, + }; + // clang-format on + + VerifySGRMouseData(testData); +} + void InputEngineTest::CtrlAltZCtrlAltXTest() { auto pfn = std::bind(&TestState::TestInputCallback, &testState, std::placeholders::_1);