// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "scrolling.hpp" #include "selection.hpp" #include "..\interactivity\inc\ServiceLocator.hpp" using Microsoft::Console::VirtualTerminal::StateMachine; using namespace Microsoft::Console::Interactivity; ULONG Scrolling::s_ucWheelScrollLines = 0; ULONG Scrolling::s_ucWheelScrollChars = 0; void Scrolling::s_UpdateSystemMetrics() { s_ucWheelScrollLines = ServiceLocator::LocateSystemConfigurationProvider()->GetNumberOfWheelScrollLines(); s_ucWheelScrollChars = ServiceLocator::LocateSystemConfigurationProvider()->GetNumberOfWheelScrollCharacters(); } bool Scrolling::s_IsInScrollMode() { const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); return WI_IsFlagSet(gci.Flags, CONSOLE_SCROLLING); } void Scrolling::s_DoScroll() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); if (!s_IsInScrollMode()) { // clear any selection we may have -- can't scroll and select at the same time Selection::Instance().ClearSelection(); WI_SetFlag(gci.Flags, CONSOLE_SCROLLING); if (pWindow != nullptr) { pWindow->UpdateWindowText(); } } } void Scrolling::s_ClearScroll() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); WI_ClearFlag(gci.Flags, CONSOLE_SCROLLING); if (pWindow != nullptr) { pWindow->UpdateWindowText(); } } void Scrolling::s_ScrollIfNecessary(const SCREEN_INFORMATION& ScreenInfo) { IConsoleWindow* pWindow = ServiceLocator::LocateConsoleWindow(); FAIL_FAST_IF_NULL(pWindow); Selection* const pSelection = &Selection::Instance(); if (pSelection->IsInSelectingState() && pSelection->IsMouseButtonDown()) { POINT CursorPos; if (!pWindow->GetCursorPosition(&CursorPos)) { return; } RECT ClientRect; if (!pWindow->GetClientRectangle(&ClientRect)) { return; } pWindow->MapPoints((LPPOINT)&ClientRect, 2); if (!(s_IsPointInRectangle(&ClientRect, CursorPos))) { pWindow->ConvertScreenToClient(&CursorPos); COORD MousePosition; MousePosition.X = (SHORT)CursorPos.x; MousePosition.Y = (SHORT)CursorPos.y; COORD coordFontSize = ScreenInfo.GetScreenFontSize(); MousePosition.X /= coordFontSize.X; MousePosition.Y /= coordFontSize.Y; MousePosition.X += ScreenInfo.GetViewport().Left(); MousePosition.Y += ScreenInfo.GetViewport().Top(); pSelection->ExtendSelection(MousePosition); } } } void Scrolling::s_HandleMouseWheel(_In_ bool isMouseWheel, _In_ bool isMouseHWheel, _In_ short wheelDelta, _In_ bool hasShift, SCREEN_INFORMATION& ScreenInfo) { COORD NewOrigin = ScreenInfo.GetViewport().Origin(); // s_ucWheelScrollLines == 0 means that it is turned off. if (isMouseWheel && s_ucWheelScrollLines > 0) { // Rounding could cause this to be zero if gucWSL is bigger than 240 or so. ULONG const ulActualDelta = std::max(WHEEL_DELTA / s_ucWheelScrollLines, 1ul); // If we change direction we need to throw away any remainder we may have in the other direction. if ((ScreenInfo.WheelDelta > 0) == (wheelDelta > 0)) { ScreenInfo.WheelDelta += wheelDelta; } else { ScreenInfo.WheelDelta = wheelDelta; } if ((ULONG)abs(ScreenInfo.WheelDelta) >= ulActualDelta) { /* * By default, SHIFT + WM_MOUSEWHEEL will scroll 1/2 the * screen size. A ScrollScale of 1 indicates 1/2 the screen * size. This value can be modified in the registry. */ SHORT delta = 1; if (hasShift) { delta = gsl::narrow(std::max((ScreenInfo.GetViewport().Height() * ScreenInfo.ScrollScale) / 2, 1u)); // Account for scroll direction changes by adjusting delta if there was a direction change. delta *= (ScreenInfo.WheelDelta < 0 ? -1 : 1); ScreenInfo.WheelDelta %= delta; } else { delta *= (ScreenInfo.WheelDelta / (short)ulActualDelta); ScreenInfo.WheelDelta %= ulActualDelta; } NewOrigin.Y -= delta; const COORD coordBufferSize = ScreenInfo.GetBufferSize().Dimensions(); if (NewOrigin.Y < 0) { NewOrigin.Y = 0; } else if (NewOrigin.Y + ScreenInfo.GetViewport().Height() > coordBufferSize.Y) { NewOrigin.Y = coordBufferSize.Y - ScreenInfo.GetViewport().Height(); } LOG_IF_FAILED(ScreenInfo.SetViewportOrigin(true, NewOrigin, false)); } } else if (isMouseHWheel && s_ucWheelScrollChars > 0) { ULONG const ulActualDelta = std::max(WHEEL_DELTA / s_ucWheelScrollChars, 1ul); if ((ScreenInfo.HWheelDelta > 0) == (wheelDelta > 0)) { ScreenInfo.HWheelDelta += wheelDelta; } else { ScreenInfo.HWheelDelta = wheelDelta; } if ((ULONG)abs(ScreenInfo.HWheelDelta) >= ulActualDelta) { SHORT delta = 1; if (hasShift) { delta = std::max(ScreenInfo.GetViewport().RightInclusive(), 1i16); } delta *= (ScreenInfo.HWheelDelta / (short)ulActualDelta); ScreenInfo.HWheelDelta %= ulActualDelta; NewOrigin.X += delta; const COORD coordBufferSize = ScreenInfo.GetBufferSize().Dimensions(); if (NewOrigin.X < 0) { NewOrigin.X = 0; } else if (NewOrigin.X + ScreenInfo.GetViewport().Width() > coordBufferSize.X) { NewOrigin.X = coordBufferSize.X - ScreenInfo.GetViewport().Width(); } LOG_IF_FAILED(ScreenInfo.SetViewportOrigin(true, NewOrigin, false)); } } } bool Scrolling::s_HandleKeyScrollingEvent(const INPUT_KEY_INFO* const pKeyInfo) { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); IConsoleWindow* pWindow = ServiceLocator::LocateConsoleWindow(); FAIL_FAST_IF_NULL(pWindow); const WORD VirtualKeyCode = pKeyInfo->GetVirtualKey(); const bool fIsCtrlPressed = pKeyInfo->IsCtrlPressed(); const bool fIsEditLineEmpty = CommandLine::Instance().IsEditLineEmpty(); // If escape, enter or ctrl-c, cancel scroll. if (VirtualKeyCode == VK_ESCAPE || VirtualKeyCode == VK_RETURN || (VirtualKeyCode == 'C' && fIsCtrlPressed)) { Scrolling::s_ClearScroll(); } else { WORD ScrollCommand; BOOL Horizontal = FALSE; switch (VirtualKeyCode) { case VK_UP: { ScrollCommand = SB_LINEUP; break; } case VK_DOWN: { ScrollCommand = SB_LINEDOWN; break; } case VK_LEFT: { ScrollCommand = SB_LINEUP; Horizontal = TRUE; break; } case VK_RIGHT: { ScrollCommand = SB_LINEDOWN; Horizontal = TRUE; break; } case VK_NEXT: { ScrollCommand = SB_PAGEDOWN; break; } case VK_PRIOR: { ScrollCommand = SB_PAGEUP; break; } case VK_END: { if (fIsCtrlPressed) { if (fIsEditLineEmpty) { // Ctrl-End when edit line is empty will scroll to last line in the buffer. gci.GetActiveOutputBuffer().MakeCurrentCursorVisible(); return true; } else { // If edit line is non-empty, we won't handle this so it can modify the edit line (trim characters to end of line from cursor pos). return false; } } else { ScrollCommand = SB_PAGEDOWN; Horizontal = TRUE; } break; } case VK_HOME: { if (fIsCtrlPressed) { if (fIsEditLineEmpty) { // Ctrl-Home when edit line is empty will scroll to top of buffer. ScrollCommand = SB_TOP; } else { // If edit line is non-empty, we won't handle this so it can modify the edit line (trim characters to beginning of line from cursor pos). return false; } } else { ScrollCommand = SB_PAGEUP; Horizontal = TRUE; } break; } case VK_SHIFT: case VK_CONTROL: case VK_MENU: { return true; } default: { pWindow->SendNotifyBeep(); return true; } } if (Horizontal) { pWindow->HorizontalScroll(ScrollCommand, 0); } else { pWindow->VerticalScroll(ScrollCommand, 0); } } return true; } BOOL Scrolling::s_IsPointInRectangle(const RECT* const prc, const POINT pt) { return ((pt.x >= prc->left) && (pt.x < prc->right) && (pt.y >= prc->top) && (pt.y < prc->bottom)); }