in src/interactivity/win32/windowio.cpp [579:956]
BOOL HandleMouseEvent(const SCREEN_INFORMATION& ScreenInfo,
const UINT Message,
const WPARAM wParam,
const LPARAM lParam)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
if (Message != WM_MOUSEMOVE)
{
// Log a telemetry flag saying the user interacted with the Console
Telemetry::Instance().SetUserInteractive();
}
Selection* const pSelection = &Selection::Instance();
if (!(gci.Flags & CONSOLE_HAS_FOCUS) && !pSelection->IsMouseButtonDown())
{
return TRUE;
}
if (gci.Flags & CONSOLE_IGNORE_NEXT_MOUSE_INPUT)
{
// only reset on up transition
if (Message != WM_LBUTTONDOWN && Message != WM_MBUTTONDOWN && Message != WM_RBUTTONDOWN)
{
gci.Flags &= ~CONSOLE_IGNORE_NEXT_MOUSE_INPUT;
return FALSE;
}
return TRUE;
}
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms645617(v=vs.85).aspx
// Important Do not use the LOWORD or HIWORD macros to extract the x- and y-
// coordinates of the cursor position because these macros return incorrect
// results on systems with multiple monitors. Systems with multiple monitors
// can have negative x- and y- coordinates, and LOWORD and HIWORD treat the
// coordinates as unsigned quantities.
short x = GET_X_LPARAM(lParam);
short y = GET_Y_LPARAM(lParam);
COORD MousePosition;
// If it's a *WHEEL event, it's in screen coordinates, not window
if (Message == WM_MOUSEWHEEL || Message == WM_MOUSEHWHEEL)
{
POINT coords = { x, y };
ScreenToClient(ServiceLocator::LocateConsoleWindow()->GetWindowHandle(), &coords);
MousePosition = { (SHORT)coords.x, (SHORT)coords.y };
}
else
{
MousePosition = { x, y };
}
// translate mouse position into characters, if necessary.
COORD ScreenFontSize = ScreenInfo.GetScreenFontSize();
MousePosition.X /= ScreenFontSize.X;
MousePosition.Y /= ScreenFontSize.Y;
const bool fShiftPressed = WI_IsFlagSet(GetKeyState(VK_SHIFT), KEY_PRESSED);
// We need to try and have the virtual terminal handle the mouse's position in viewport coordinates,
// not in screen buffer coordinates. It expects the top left to always be 0,0
// (the TerminalMouseInput object will add (1,1) to convert to VT coords on its own.)
// Mouse events with shift pressed will ignore this and fall through to the default handler.
// This is in line with PuTTY's behavior and vim's own documentation:
// "The xterm handling of the mouse buttons can still be used by keeping the shift key pressed." - `:help 'mouse'`, vim.
// Mouse events while we're selecting or have a selection will also skip this and fall though
// (so that the VT handler doesn't eat any selection region updates)
if (!fShiftPressed && !pSelection->IsInSelectingState())
{
short sDelta = 0;
if (Message == WM_MOUSEWHEEL)
{
sDelta = GET_WHEEL_DELTA_WPARAM(wParam);
}
if (HandleTerminalMouseEvent(MousePosition, Message, LOWORD(GetControlKeyState(0)), sDelta))
{
// Use GetControlKeyState here to get the control state in console event mode.
// This will ensure that we get ALT and SHIFT, the former of which is not available
// through MK_ constants. We only care about the bottom 16 bits.
// GH#6401: Capturing the mouse ensures that we get drag/release events
// even if the user moves outside the window.
// HandleTerminalMouseEvent returns false if the terminal's not in VT mode,
// so capturing/releasing here should not impact other console mouse event
// consumers.
switch (Message)
{
case WM_LBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
SetCapture(ServiceLocator::LocateConsoleWindow()->GetWindowHandle());
break;
case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
ReleaseCapture();
break;
}
return FALSE;
}
}
MousePosition.X += ScreenInfo.GetViewport().Left();
MousePosition.Y += ScreenInfo.GetViewport().Top();
const COORD coordScreenBufferSize = ScreenInfo.GetBufferSize().Dimensions();
// make sure mouse position is clipped to screen buffer
if (MousePosition.X < 0)
{
MousePosition.X = 0;
}
else if (MousePosition.X >= coordScreenBufferSize.X)
{
MousePosition.X = coordScreenBufferSize.X - 1;
}
if (MousePosition.Y < 0)
{
MousePosition.Y = 0;
}
else if (MousePosition.Y >= coordScreenBufferSize.Y)
{
MousePosition.Y = coordScreenBufferSize.Y - 1;
}
// Process the transparency mousewheel message before the others so that we can
// process all the mouse events within the Selection and QuickEdit check
if (Message == WM_MOUSEWHEEL)
{
const short sKeyState = GET_KEYSTATE_WPARAM(wParam);
if (WI_IsFlagSet(sKeyState, MK_CONTROL))
{
const short sDelta = GET_WHEEL_DELTA_WPARAM(wParam) / WHEEL_DELTA;
// ctrl+shift+scroll adjusts opacity of the window
if (WI_IsFlagSet(sKeyState, MK_SHIFT))
{
ServiceLocator::LocateConsoleWindow<Window>()->ChangeWindowOpacity(OPACITY_DELTA_INTERVAL * sDelta);
}
// ctrl+scroll adjusts the font size
else
{
LOG_IF_FAILED(_AdjustFontSize(sDelta));
}
}
}
if (pSelection->IsInSelectingState() || pSelection->IsInQuickEditMode())
{
if (Message == WM_LBUTTONDOWN)
{
// make sure message matches button state
if (!(GetKeyState(VK_LBUTTON) & KEY_PRESSED))
{
return FALSE;
}
if (pSelection->IsInQuickEditMode() && !pSelection->IsInSelectingState())
{
// start a mouse selection
pSelection->InitializeMouseSelection(MousePosition);
pSelection->MouseDown();
// Check for ALT-Mouse Down "use alternate selection"
// If in box mode, use line mode. If in line mode, use box mode.
// TODO: move into initialize?
pSelection->CheckAndSetAlternateSelection();
pSelection->ShowSelection();
}
else
{
bool fExtendSelection = false;
// We now capture the mouse to our Window. We do this so that the
// user can "scroll" the selection endpoint to an off screen
// position by moving the mouse off the client area.
if (pSelection->IsMouseInitiatedSelection())
{
// Check for SHIFT-Mouse Down "continue previous selection" command.
if (fShiftPressed)
{
fExtendSelection = true;
}
}
// if we chose to extend the selection, do that.
if (fExtendSelection)
{
pSelection->MouseDown();
pSelection->ExtendSelection(MousePosition);
}
else
{
// otherwise, set up a new selection from here. note that it's important to ClearSelection(true) here
// because ClearSelection() unblocks console output, causing us to have
// a line of output occur every time the user changes the selection.
pSelection->ClearSelection(true);
pSelection->InitializeMouseSelection(MousePosition);
pSelection->MouseDown();
pSelection->ShowSelection();
}
}
}
else if (Message == WM_LBUTTONUP)
{
if (pSelection->IsInSelectingState() && pSelection->IsMouseInitiatedSelection())
{
pSelection->MouseUp();
}
}
else if (Message == WM_LBUTTONDBLCLK)
{
// on double-click, attempt to select a "word" beneath the cursor
const COORD selectionAnchor = pSelection->GetSelectionAnchor();
if (MousePosition == selectionAnchor)
{
try
{
const std::pair<COORD, COORD> wordBounds = ScreenInfo.GetWordBoundary(MousePosition);
MousePosition = wordBounds.second;
// update both ends of the selection since we may have adjusted the anchor in some circumstances.
pSelection->AdjustSelection(wordBounds.first, wordBounds.second);
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
}
}
pSelection->MouseDown();
}
else if ((Message == WM_RBUTTONDOWN) || (Message == WM_RBUTTONDBLCLK))
{
if (!pSelection->IsMouseButtonDown())
{
if (pSelection->IsInSelectingState())
{
// Capture data on when quick edit copy is used in proc or raw mode
if (IsInProcessedInputMode())
{
Telemetry::Instance().LogQuickEditCopyProcUsed();
}
else
{
Telemetry::Instance().LogQuickEditCopyRawUsed();
}
// If the ALT key is held, also select HTML as well as plain text.
bool const fAlsoCopyFormatting = WI_IsFlagSet(GetKeyState(VK_MENU), KEY_PRESSED);
Clipboard::Instance().Copy(fAlsoCopyFormatting);
}
else if (gci.Flags & CONSOLE_QUICK_EDIT_MODE)
{
// Capture data on when quick edit paste is used in proc or raw mode
if (IsInProcessedInputMode())
{
Telemetry::Instance().LogQuickEditPasteProcUsed();
}
else
{
Telemetry::Instance().LogQuickEditPasteRawUsed();
}
Clipboard::Instance().Paste();
}
gci.Flags |= CONSOLE_IGNORE_NEXT_MOUSE_INPUT;
}
}
else if (Message == WM_MBUTTONDOWN)
{
ServiceLocator::LocateConsoleControl<Microsoft::Console::Interactivity::Win32::ConsoleControl>()
->EnterReaderModeHelper(ServiceLocator::LocateConsoleWindow()->GetWindowHandle());
}
else if (Message == WM_MOUSEMOVE)
{
if (pSelection->IsMouseButtonDown() && pSelection->ShouldAllowMouseDragSelection(MousePosition))
{
pSelection->ExtendSelection(MousePosition);
}
}
else if (Message == WM_MOUSEWHEEL || Message == WM_MOUSEHWHEEL)
{
return TRUE;
}
// We're done processing the messages for selection. We need to return
return FALSE;
}
if (WI_IsFlagClear(gci.pInputBuffer->InputMode, ENABLE_MOUSE_INPUT))
{
ReleaseCapture();
return TRUE;
}
ULONG ButtonFlags;
ULONG EventFlags;
switch (Message)
{
case WM_LBUTTONDOWN:
SetCapture(ServiceLocator::LocateConsoleWindow()->GetWindowHandle());
ButtonFlags = FROM_LEFT_1ST_BUTTON_PRESSED;
EventFlags = 0;
break;
case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
ReleaseCapture();
ButtonFlags = EventFlags = 0;
break;
case WM_RBUTTONDOWN:
SetCapture(ServiceLocator::LocateConsoleWindow()->GetWindowHandle());
ButtonFlags = RIGHTMOST_BUTTON_PRESSED;
EventFlags = 0;
break;
case WM_MBUTTONDOWN:
SetCapture(ServiceLocator::LocateConsoleWindow()->GetWindowHandle());
ButtonFlags = FROM_LEFT_2ND_BUTTON_PRESSED;
EventFlags = 0;
break;
case WM_MOUSEMOVE:
ButtonFlags = 0;
EventFlags = MOUSE_MOVED;
break;
case WM_LBUTTONDBLCLK:
ButtonFlags = FROM_LEFT_1ST_BUTTON_PRESSED;
EventFlags = DOUBLE_CLICK;
break;
case WM_RBUTTONDBLCLK:
ButtonFlags = RIGHTMOST_BUTTON_PRESSED;
EventFlags = DOUBLE_CLICK;
break;
case WM_MBUTTONDBLCLK:
ButtonFlags = FROM_LEFT_2ND_BUTTON_PRESSED;
EventFlags = DOUBLE_CLICK;
break;
case WM_MOUSEWHEEL:
ButtonFlags = ((UINT)wParam & 0xFFFF0000);
EventFlags = MOUSE_WHEELED;
break;
case WM_MOUSEHWHEEL:
ButtonFlags = ((UINT)wParam & 0xFFFF0000);
EventFlags = MOUSE_HWHEELED;
break;
default:
RIPMSG1(RIP_ERROR, "Invalid message 0x%x", Message);
ButtonFlags = 0;
EventFlags = 0;
break;
}
ULONG EventsWritten = 0;
try
{
std::unique_ptr<MouseEvent> mouseEvent = std::make_unique<MouseEvent>(
MousePosition,
ConvertMouseButtonState(ButtonFlags, static_cast<UINT>(wParam)),
GetControlKeyState(0),
EventFlags);
EventsWritten = static_cast<ULONG>(gci.pInputBuffer->Write(std::move(mouseEvent)));
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
EventsWritten = 0;
}
if (EventsWritten != 1)
{
RIPMSG1(RIP_WARNING, "PutInputInBuffer: EventsWritten != 1 (0x%x), 1 expected", EventsWritten);
}
return FALSE;
}