Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[VT Mouse Mode] Translate SGR Mouse VT Sequences to MOUSE_EVENT_RECORD #3963

Merged
9 commits merged into from
Feb 3, 2020
Merged
309 changes: 227 additions & 82 deletions src/terminal/parser/InputStateMachineEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,47 +15,6 @@

using namespace Microsoft::Console::VirtualTerminal;

// The values used by VkKeyScan to encode modifiers in the high order byte
const short KEYSCAN_SHIFT = 1;
const short KEYSCAN_CTRL = 2;
const short KEYSCAN_ALT = 4;

// The values with which VT encodes modifier values.
const short VT_SHIFT = 1;
const short VT_ALT = 2;
const short VT_CTRL = 4;

const size_t WRAPPED_SEQUENCE_MAX_LENGTH = 8;

// For reference, the equivalent INPUT_RECORD values are:
// RIGHT_ALT_PRESSED 0x0001
// LEFT_ALT_PRESSED 0x0002
// RIGHT_CTRL_PRESSED 0x0004
// LEFT_CTRL_PRESSED 0x0008
// SHIFT_PRESSED 0x0010
// NUMLOCK_ON 0x0020
// SCROLLLOCK_ON 0x0040
// CAPSLOCK_ON 0x0080
// ENHANCED_KEY 0x0100

enum class CsiActionCodes : wchar_t
{
ArrowUp = L'A',
ArrowDown = L'B',
ArrowRight = L'C',
ArrowLeft = L'D',
Home = L'H',
End = L'F',
Generic = L'~', // Used for a whole bunch of possible keys
CSI_F1 = L'P',
CSI_F2 = L'Q',
CSI_F3 = L'R', // Both F3 and DSR are on R.
// DSR_DeviceStatusReportResponse = L'R',
CSI_F4 = L'S',
DTTERM_WindowManipulation = L't',
CursorBackTab = L'Z',
};

struct CsiToVkey
{
CsiActionCodes action;
Expand All @@ -80,25 +39,6 @@ static bool operator==(const CsiToVkey& pair, const CsiActionCodes code) noexcep
return pair.action == code;
}

// Sequences ending in '~' use these numbers as identifiers.
enum class GenericKeyIdentifiers : unsigned short
{
GenericHome = 1,
Insert = 2,
Delete = 3,
GenericEnd = 4,
Prior = 5, //PgUp
Next = 6, //PgDn
F5 = 15,
F6 = 17,
F7 = 18,
F8 = 19,
F9 = 20,
F10 = 21,
F11 = 23,
F12 = 24,
};

struct GenericToVkey
{
GenericKeyIdentifiers identifier;
Expand Down Expand Up @@ -127,22 +67,6 @@ static bool operator==(const GenericToVkey& pair, const GenericKeyIdentifiers id
return pair.identifier == identifier;
}

enum class Ss3ActionCodes : wchar_t
{
// The "Cursor Keys" are sometimes sent as a SS3 in "application mode"
// But for now we'll only accept them as Normal Mode sequences, as CSI's.
// ArrowUp = L'A',
// ArrowDown = L'B',
// ArrowRight = L'C',
// ArrowLeft = L'D',
// Home = L'H',
// End = L'F',
SS3_F1 = L'P',
SS3_F2 = L'Q',
SS3_F3 = L'R',
SS3_F4 = L'S',
};

struct Ss3ToVkey
{
Ss3ActionCodes action;
Expand Down Expand Up @@ -382,7 +306,7 @@ bool InputStateMachineEngine::ActionEscDispatch(const wchar_t wch,
// Return Value:
// - true iff we successfully dispatched the sequence.
bool InputStateMachineEngine::ActionCsiDispatch(const wchar_t wch,
const std::basic_string_view<wchar_t> /*intermediates*/,
const std::basic_string_view<wchar_t> intermediates,
const std::basic_string_view<size_t> parameters)
{
DWORD modifierState = 0;
Expand All @@ -395,6 +319,29 @@ bool InputStateMachineEngine::ActionCsiDispatch(const wchar_t wch,
const auto remainingArgs = parameters.size() > 1 ? parameters.substr(1) : std::basic_string_view<size_t>{};

bool success = false;
// Handle intermediate characters, if any
if (!intermediates.empty())
{
switch (static_cast<CsiIntermediateCodes>(intermediates.at(0)))
{
case CsiIntermediateCodes::MOUSE_SGR:
{
DWORD buttonState = 0;
DWORD eventFlags = 0;
modifierState = _GetSGRMouseModifierState(parameters);
success = _GetSGRXYPosition(parameters, row, col);

// we need _UpdateSGRMouseButtonState() on the left side here because we _always_ should be updating our state
// even if we failed to parse a portion of this sequence.
success = _UpdateSGRMouseButtonState(wch, parameters, buttonState, eventFlags) && success;
carlos-zamora marked this conversation as resolved.
Show resolved Hide resolved
success = success && _WriteMouseEvent(col, row, buttonState, modifierState, eventFlags);
}
default:
success = false;
break;
}
return success;
}
switch (static_cast<CsiActionCodes>(wch))
{
case CsiActionCodes::Generic:
Expand Down Expand Up @@ -720,10 +667,37 @@ bool InputStateMachineEngine::_WriteSingleKey(const wchar_t wch, const short vke
// - modifierState - the modifier state to write with the key.
// Return Value:
// - true iff we successfully wrote the keypress to the input callback.
bool InputStateMachineEngine::_WriteSingleKey(const short vkey, const DWORD dwModifierState)
bool InputStateMachineEngine::_WriteSingleKey(const short vkey, const DWORD modifierState)
{
const wchar_t wch = gsl::narrow_cast<wchar_t>(MapVirtualKey(vkey, MAPVK_VK_TO_CHAR));
return _WriteSingleKey(wch, vkey, dwModifierState);
return _WriteSingleKey(wch, vkey, modifierState);
}

// Method Description:
// - Writes a Mouse Event Record to the input callback based on the state of the mouse.
// Arguments:
// - column - the X/Column position on the viewport (0 = left-most)
// - line - the Y/Line/Row position on the viewport (0 = top-most)
// - buttonState - the mouse buttons that are being modified
// - modifierState - the modifier state to write mouse record.
// - 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)
{
COORD uiPos = { gsl::narrow<short>(column) - 1, gsl::narrow<short>(line) - 1 };

INPUT_RECORD rgInput;
rgInput.EventType = MOUSE_EVENT;
rgInput.Event.MouseEvent.dwMousePosition = uiPos;
rgInput.Event.MouseEvent.dwButtonState = buttonState;
rgInput.Event.MouseEvent.dwControlKeyState = controlKeyState;
rgInput.Event.MouseEvent.dwEventFlags = eventFlags;

// pack and write input record
// 1 record - the modifiers don't get their own events
std::deque<std::unique_ptr<IInputEvent>> inputEvents = IInputEvent::Create(gsl::make_span(&rgInput, 1));
return _pDispatch->WriteInput(inputEvents);
}

// Method Description:
Expand Down Expand Up @@ -758,6 +732,33 @@ DWORD InputStateMachineEngine::_GetGenericKeysModifierState(const std::basic_str
return modifiers;
}

// Method Description:
// - Retrieves the modifier state from a set of parameters for an SGR
// Mouse Sequence - one who's sequence is terminated with an 'M' or 'm'.
// Arguments:
// - parameters - the set of parameters to get the modifier state from.
// Return Value:
// - the INPUT_RECORD compatible modifier state.
DWORD InputStateMachineEngine::_GetSGRMouseModifierState(const std::basic_string_view<size_t> parameters) noexcept
{
DWORD modifiers = 0;
if (parameters.size() == 3)
{
// The first parameter of mouse events is encoded as the following two bytes:
carlos-zamora marked this conversation as resolved.
Show resolved Hide resolved
// BBDM'MMBB
// Where each of the bits mean the following
// BB__'__BB - which button was pressed/released
// MMM - Control, Alt, Shift state (respectively)
// D - flag signifying a drag event
// This retrieves the modifier state from bits [5..3] ('M' above)
const auto modifierParam = til::at(parameters, 0);
WI_SetFlagIf(modifiers, SHIFT_PRESSED, WI_IsFlagSet(modifierParam, CsiMouseModifierCodes::Shift));
WI_SetFlagIf(modifiers, LEFT_ALT_PRESSED, WI_IsFlagSet(modifierParam, CsiMouseModifierCodes::Meta));
WI_SetFlagIf(modifiers, LEFT_CTRL_PRESSED, WI_IsFlagSet(modifierParam, CsiMouseModifierCodes::Ctrl));
}
return modifiers;
}

// Method Description:
// - Determines if a set of parameters indicates a modified keypress
// Arguments:
Expand Down Expand Up @@ -790,6 +791,111 @@ DWORD InputStateMachineEngine::_GetModifier(const size_t modifierParam) noexcept
return modifierState;
}

// Method Description:
// - Synthesize the button state for the Mouse Input Record from an SGR VT Sequence
// - Here, we refer to and maintain the global state of our mouse.
// - Mouse wheel events are added at the end to keep them out of the global state
// Arguments:
// - wch: the wchar_t representing whether the button was pressed or released
// - parameters: the wchar_t to get the mapped vkey of. Represents the direction of the button (down vs up)
// - buttonState: Recieves the button state for the record
// - eventFlags: Recieves the special mouse events for the record
// Return Value:
// true iff we were able to synthesize buttonState
bool InputStateMachineEngine::_UpdateSGRMouseButtonState(const wchar_t wch,
const std::basic_string_view<size_t> parameters,
DWORD& buttonState,
DWORD& eventFlags) noexcept
{
if (parameters.empty())
{
return false;
}

// Starting with the state from the last mouse event we received
buttonState = _mouseButtonState;
eventFlags = 0;

// The first parameter of mouse events is encoded as the following two bytes:
// BBDM'MMBB
// Where each of the bits mean the following
// BB__'__BB - which button was pressed/released
// MMM - Control, Alt, Shift state (respectively)
// D - flag signifying a drag event
const auto sgrEncoding = til::at(parameters, 0);

// 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);

// Step 1: Translate which button was affected
// NOTE: if scrolled, having buttonFlag = 0 means
// we don't actually update the buttonState
DWORD buttonFlag = 0;
switch (buttonID)
{
case CsiMouseButtonCodes::Left:
buttonFlag = FROM_LEFT_1ST_BUTTON_PRESSED;
break;
case CsiMouseButtonCodes::Right:
buttonFlag = RIGHTMOST_BUTTON_PRESSED;
break;
case CsiMouseButtonCodes::Middle:
buttonFlag = FROM_LEFT_2ND_BUTTON_PRESSED;
break;
case CsiMouseButtonCodes::ScrollBack:
{
// set high word to proper scroll direction
// scroll intensity is assumed to be constant value
buttonState |= SCROLL_DELTA_BACKWARD;
eventFlags |= MOUSE_WHEELED;
break;
}
case CsiMouseButtonCodes::ScrollForward:
{
// set high word to proper scroll direction
// scroll intensity is assumed to be constant value
buttonState |= SCROLL_DELTA_FORWARD;
eventFlags |= MOUSE_WHEELED;
break;
}
default:
// no detectable buttonID, so we can't update the state
return false;
}

// Step 2: Decide whether to set or clear that button's bit
// NOTE: WI_SetFlag/WI_ClearFlag can't be used here because buttonFlag would have to be a compile-time constant
switch (static_cast<CsiActionCodes>(wch))
{
case CsiActionCodes::MouseDown:
// set flag
// NOTE: scroll events have buttonFlag = 0
// so this intentionally does nothing
buttonState |= buttonFlag;
break;
case CsiActionCodes::MouseUp:
// clear flag
buttonState &= (~buttonFlag);
break;
default:
// no detectable change of state, so we can't update the state
return false;
}

// Step 3: check if mouse moved
if (WI_IsFlagSet(sgrEncoding, CsiMouseModifierCodes::Drag))
{
eventFlags |= MOUSE_MOVED;
}

// Step 4: update internal state before returning, even if we couldn't fully understand this
// only take LOWORD here because HIWORD is reserved for mouse wheel delta and release events for the wheel buttons are not reported
_mouseButtonState = LOWORD(buttonState);

return true;
}

// Method Description:
// - Gets the Vkey form the generic keys table associated with a particular
// identifier code. The identifier code will be the first param in rgusParams.
Expand Down Expand Up @@ -985,7 +1091,6 @@ bool InputStateMachineEngine::_GetXYPosition(const std::basic_string_view<size_t
size_t& line,
size_t& column) const noexcept
{
bool success = true;
line = DefaultLine;
column = DefaultColumn;

Expand All @@ -1006,7 +1111,7 @@ bool InputStateMachineEngine::_GetXYPosition(const std::basic_string_view<size_t
}
else
{
success = false;
return false;
}

// Distances of 0 should be changed to 1.
Expand All @@ -1020,5 +1125,45 @@ bool InputStateMachineEngine::_GetXYPosition(const std::basic_string_view<size_t
column = DefaultColumn;
}

return success;
return true;
}

// Routine Description:
// - Retrieves an X/Y coordinate pair for an SGR Mouse sequence from the parameter pool stored during Param actions.
// Arguments:
// - parameters - set of numeric parameters collected while pasring the sequence.
// - line - Receives the Y/Line/Row position
// - column - Receives the X/Column position
// Return Value:
// - True if we successfully pulled the cursor coordinates from the parameters we've stored. False otherwise.
bool InputStateMachineEngine::_GetSGRXYPosition(const std::basic_string_view<size_t> parameters,
size_t& line,
size_t& column) const noexcept
{
line = DefaultLine;
column = DefaultColumn;

// SGR Mouse sequences have exactly 3 parameters
if (parameters.size() == 3)
{
column = til::at(parameters, 1);
line = til::at(parameters, 2);
}
else
{
return false;
}

// Distances of 0 should be changed to 1.
if (line == 0)
{
line = DefaultLine;
}

if (column == 0)
{
column = DefaultColumn;
}

return true;
}
Loading