BOOL HandleMouseEvent()

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;
}