mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-19 18:11:39 -05:00
This adds support for horizontal mouse wheel events (`WM_MOUSEHWHEEL`). With this change, applications running in the terminal can now receive and respond to horizontal scroll inputs from the mouse/trackpad. Closes #19245 Closes #10329
762 lines
33 KiB
C++
762 lines
33 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.h"
|
|
#include "ControlInteractivity.h"
|
|
#include <DefaultSettings.h>
|
|
#include <unicode.hpp>
|
|
#include <Utils.h>
|
|
#include <LibraryResources.h>
|
|
#include "../../types/inc/GlyphWidth.hpp"
|
|
#include "../../types/inc/Utils.hpp"
|
|
#include "../../buffer/out/search.h"
|
|
|
|
#include "InteractivityAutomationPeer.h"
|
|
|
|
#include "ControlInteractivity.g.cpp"
|
|
|
|
using namespace ::Microsoft::Console::Types;
|
|
using namespace ::Microsoft::Console::VirtualTerminal;
|
|
using namespace ::Microsoft::Terminal::Core;
|
|
using namespace winrt::Windows::Graphics::Display;
|
|
using namespace winrt::Windows::System;
|
|
using namespace winrt::Windows::ApplicationModel::DataTransfer;
|
|
|
|
static constexpr unsigned int MAX_CLICK_COUNT = 3;
|
|
|
|
namespace winrt::Microsoft::Terminal::Control::implementation
|
|
{
|
|
std::atomic<uint64_t> ControlInteractivity::_nextId{ 1 };
|
|
|
|
static constexpr TerminalInput::MouseButtonState toInternalMouseState(const Control::MouseButtonState& state)
|
|
{
|
|
return TerminalInput::MouseButtonState{
|
|
WI_IsFlagSet(state, MouseButtonState::IsLeftButtonDown),
|
|
WI_IsFlagSet(state, MouseButtonState::IsMiddleButtonDown),
|
|
WI_IsFlagSet(state, MouseButtonState::IsRightButtonDown)
|
|
};
|
|
}
|
|
|
|
ControlInteractivity::ControlInteractivity(IControlSettings settings,
|
|
Control::IControlAppearance unfocusedAppearance,
|
|
TerminalConnection::ITerminalConnection connection) :
|
|
_touchAnchor{ std::nullopt },
|
|
_lastMouseClickTimestamp{},
|
|
_lastMouseClickPos{},
|
|
_selectionNeedsToBeCopied{ false }
|
|
{
|
|
_id = _nextId.fetch_add(1, std::memory_order_relaxed);
|
|
|
|
_core = winrt::make_self<ControlCore>(settings, unfocusedAppearance, connection);
|
|
|
|
_core->Attached([weakThis = get_weak()](auto&&, auto&&) {
|
|
if (auto self{ weakThis.get() })
|
|
{
|
|
self->Attached.raise(*self, nullptr);
|
|
}
|
|
});
|
|
}
|
|
|
|
uint64_t ControlInteractivity::Id()
|
|
{
|
|
return _id;
|
|
}
|
|
|
|
void ControlInteractivity::Detach()
|
|
{
|
|
if (_uiaEngine)
|
|
{
|
|
// There's a potential race here where we've removed the TermControl
|
|
// from the UI tree, but the UIA engine is in the middle of a paint,
|
|
// and the UIA engine will try to dispatch to the
|
|
// TermControlAutomationPeer, which (is now)/(will very soon be) gone.
|
|
//
|
|
// To alleviate, make sure to disable the UIA engine and remove it,
|
|
// and ALSO disable the renderer. Core.Detach will take care of the
|
|
// TriggerTeardown (which will stop the renderer
|
|
// after all current engines are done painting).
|
|
//
|
|
// Simply disabling the UIA engine is not enough, because it's
|
|
// possible that it had already started presenting here.
|
|
LOG_IF_FAILED(_uiaEngine->Disable());
|
|
_core->DetachUiaEngine(_uiaEngine.get());
|
|
}
|
|
_core->Detach();
|
|
}
|
|
|
|
void ControlInteractivity::AttachToNewControl()
|
|
{
|
|
_core->AttachToNewControl();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Updates our internal settings. These settings should be
|
|
// interactivity-specific. Right now, we primarily update _rowsToScroll
|
|
// with the current value of SPI_GETWHEELSCROLLLINES.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void ControlInteractivity::UpdateSettings()
|
|
{
|
|
_updateSystemParameterSettings();
|
|
}
|
|
|
|
void ControlInteractivity::Initialize()
|
|
{
|
|
_updateSystemParameterSettings();
|
|
|
|
// import value from WinUser (convert from milli-seconds to micro-seconds)
|
|
_multiClickTimer = GetDoubleClickTime() * 1000;
|
|
}
|
|
|
|
Control::ControlCore ControlInteractivity::Core()
|
|
{
|
|
return *_core;
|
|
}
|
|
|
|
void ControlInteractivity::Close()
|
|
{
|
|
Closed.raise(*this, nullptr);
|
|
if (_core)
|
|
{
|
|
_core->Close();
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Returns the number of clicks that occurred (double and triple click support).
|
|
// Every call to this function registers a click.
|
|
// Arguments:
|
|
// - clickPos: the (x,y) position of a given cursor (i.e.: mouse cursor).
|
|
// NOTE: origin (0,0) is top-left.
|
|
// - clickTime: the timestamp that the click occurred
|
|
// Return Value:
|
|
// - if the click is in the same position as the last click and within the timeout, the number of clicks within that time window
|
|
// - otherwise, 1
|
|
unsigned int ControlInteractivity::_numberOfClicks(Core::Point clickPos,
|
|
Timestamp clickTime)
|
|
{
|
|
// if click occurred at a different location or past the multiClickTimer...
|
|
Timestamp delta;
|
|
THROW_IF_FAILED(UInt64Sub(clickTime, _lastMouseClickTimestamp, &delta));
|
|
if (clickPos != _lastMouseClickPos || delta > _multiClickTimer)
|
|
{
|
|
_multiClickCounter = 1;
|
|
}
|
|
else
|
|
{
|
|
_multiClickCounter++;
|
|
}
|
|
|
|
_lastMouseClickTimestamp = clickTime;
|
|
_lastMouseClickPos = clickPos;
|
|
return _multiClickCounter;
|
|
}
|
|
|
|
void ControlInteractivity::GotFocus()
|
|
{
|
|
if (_uiaEngine.get())
|
|
{
|
|
THROW_IF_FAILED(_uiaEngine->Enable());
|
|
}
|
|
|
|
_core->GotFocus();
|
|
|
|
_updateSystemParameterSettings();
|
|
}
|
|
|
|
void ControlInteractivity::LostFocus()
|
|
{
|
|
if (_uiaEngine.get())
|
|
{
|
|
THROW_IF_FAILED(_uiaEngine->Disable());
|
|
}
|
|
|
|
_core->LostFocus();
|
|
}
|
|
|
|
// Method Description
|
|
// - Updates internal params based on system parameters
|
|
void ControlInteractivity::_updateSystemParameterSettings() noexcept
|
|
{
|
|
if (!SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, &_rowsToScroll, 0))
|
|
{
|
|
LOG_LAST_ERROR();
|
|
// If SystemParametersInfoW fails, which it shouldn't, fall back to
|
|
// Windows' default value.
|
|
_rowsToScroll = 3;
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Given a copy-able selection, get the selected text from the buffer and send it to the
|
|
// Windows Clipboard (CascadiaWin32:main.cpp).
|
|
// Arguments:
|
|
// - singleLine: collapse all of the text to one line
|
|
// - withControlSequences: if enabled, the copied plain text contains color/style ANSI escape codes from the selection
|
|
// - formats: which formats to copy (defined by action's CopyFormatting arg). nullptr
|
|
// if we should defer which formats are copied to the global setting
|
|
bool ControlInteractivity::CopySelectionToClipboard(bool singleLine,
|
|
bool withControlSequences,
|
|
const CopyFormat formats)
|
|
{
|
|
if (_core)
|
|
{
|
|
// Return false if there's no selection to copy. If there's no
|
|
// selection, returning false will indicate that the actions that
|
|
// triggered this should _not_ be marked as handled, so ctrl+c
|
|
// without a selection can still send ^C
|
|
if (!_core->HasSelection())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Mark the current selection as copied
|
|
_selectionNeedsToBeCopied = false;
|
|
|
|
return _core->CopySelectionToClipboard(singleLine, withControlSequences, formats);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Initiate a paste operation.
|
|
void ControlInteractivity::RequestPasteTextFromClipboard()
|
|
{
|
|
auto args = winrt::make<PasteFromClipboardEventArgs>(
|
|
[core = _core](const winrt::hstring& wstr) {
|
|
core->PasteText(wstr);
|
|
},
|
|
_core->BracketedPasteEnabled());
|
|
|
|
// send paste event up to TermApp
|
|
PasteFromClipboard.raise(*this, std::move(args));
|
|
}
|
|
|
|
void ControlInteractivity::PointerPressed(Control::MouseButtonState buttonState,
|
|
const unsigned int pointerUpdateKind,
|
|
const uint64_t timestamp,
|
|
const ::Microsoft::Terminal::Core::ControlKeyStates modifiers,
|
|
const Core::Point pixelPosition)
|
|
{
|
|
// Un-rounded coordinates; we only round when selecting text
|
|
const auto terminalPosition = _getTerminalPosition(til::point{ pixelPosition }, false);
|
|
|
|
const auto altEnabled = modifiers.IsAltPressed();
|
|
const auto shiftEnabled = modifiers.IsShiftPressed();
|
|
const auto ctrlEnabled = modifiers.IsCtrlPressed();
|
|
|
|
// GH#9396: we prioritize hyper-link over VT mouse events
|
|
auto hyperlink = _core->GetHyperlink(terminalPosition.to_core_point());
|
|
if (WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown) &&
|
|
ctrlEnabled &&
|
|
!hyperlink.empty())
|
|
{
|
|
const auto clickCount = _numberOfClicks(pixelPosition, timestamp);
|
|
// Handle hyper-link only on the first click to prevent multiple activations
|
|
if (clickCount == 1)
|
|
{
|
|
_hyperlinkHandler(hyperlink);
|
|
}
|
|
}
|
|
else if (_canSendVTMouseInput(modifiers))
|
|
{
|
|
_sendMouseEventHelper(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState);
|
|
}
|
|
else if (WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown))
|
|
{
|
|
const auto clickCount = _numberOfClicks(pixelPosition, timestamp);
|
|
// This formula enables the number of clicks to cycle properly
|
|
// between single-, double-, and triple-click. To increase the
|
|
// number of acceptable click states, simply increment
|
|
// MAX_CLICK_COUNT and add another if-statement
|
|
const auto multiClickMapper = clickCount > MAX_CLICK_COUNT ? ((clickCount + MAX_CLICK_COUNT - 1) % MAX_CLICK_COUNT) + 1 : clickCount;
|
|
|
|
// Capture the position of the first click when no selection is active
|
|
if (multiClickMapper == 1)
|
|
{
|
|
_singleClickTouchdownPos = pixelPosition;
|
|
|
|
if (!_core->HasSelection())
|
|
{
|
|
_lastMouseClickPosNoSelection = pixelPosition;
|
|
}
|
|
}
|
|
const auto isOnOriginalPosition = _lastMouseClickPosNoSelection == pixelPosition;
|
|
|
|
// Rounded coordinates for text selection
|
|
_core->LeftClickOnTerminal(_getTerminalPosition(til::point{ pixelPosition }, true),
|
|
multiClickMapper,
|
|
altEnabled,
|
|
shiftEnabled,
|
|
isOnOriginalPosition,
|
|
_selectionNeedsToBeCopied);
|
|
|
|
if (_core->HasSelection())
|
|
{
|
|
// GH#9787: if selection is active we don't want to track the touchdown position
|
|
// so that dragging the mouse will extend the selection rather than starting the new one
|
|
_singleClickTouchdownPos = std::nullopt;
|
|
}
|
|
}
|
|
else if (WI_IsFlagSet(buttonState, MouseButtonState::IsRightButtonDown))
|
|
{
|
|
if (_core->Settings().RightClickContextMenu())
|
|
{
|
|
// Let the core know we're about to open a menu here. It has
|
|
// some separate conditional logic based on _where_ the user
|
|
// wanted to open the menu.
|
|
_core->AnchorContextMenu(terminalPosition);
|
|
|
|
auto contextArgs = winrt::make<ContextMenuRequestedEventArgs>(til::point{ pixelPosition }.to_winrt_point());
|
|
ContextMenuRequested.raise(*this, contextArgs);
|
|
}
|
|
else
|
|
{
|
|
// Try to copy the text and clear the selection
|
|
const auto successfulCopy = CopySelectionToClipboard(shiftEnabled, false, _core->Settings().CopyFormatting());
|
|
_core->ClearSelection();
|
|
if (_core->CopyOnSelect() || !successfulCopy)
|
|
{
|
|
// CopyOnSelect: right click always pastes!
|
|
// Otherwise: no selection --> paste
|
|
RequestPasteTextFromClipboard();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ControlInteractivity::TouchPressed(const winrt::Windows::Foundation::Point contactPoint)
|
|
{
|
|
_touchAnchor = contactPoint;
|
|
}
|
|
|
|
bool ControlInteractivity::PointerMoved(Control::MouseButtonState buttonState,
|
|
const unsigned int pointerUpdateKind,
|
|
const ::Microsoft::Terminal::Core::ControlKeyStates modifiers,
|
|
const bool focused,
|
|
const Core::Point pixelPosition,
|
|
const bool pointerPressedInBounds)
|
|
{
|
|
const auto terminalPosition = _getTerminalPosition(til::point{ pixelPosition }, false);
|
|
// Returning true from this function indicates that the caller should do no further processing of this movement.
|
|
bool handledCompletely = false;
|
|
|
|
// Short-circuit isReadOnly check to avoid warning dialog
|
|
if (focused && !_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers))
|
|
{
|
|
_sendMouseEventHelper(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState);
|
|
handledCompletely = true;
|
|
}
|
|
// GH#4603 - don't modify the selection if the pointer press didn't
|
|
// actually start _in_ the control bounds. Case in point - someone drags
|
|
// a file into the bounds of the control. That shouldn't send the
|
|
// selection into space.
|
|
else if (focused && pointerPressedInBounds && WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown))
|
|
{
|
|
if (_singleClickTouchdownPos)
|
|
{
|
|
// Figure out if the user's moved a 1/4th of a cell's smaller axis
|
|
// (practically always the width) away from the clickdown point.
|
|
const auto fontSizeInDips = _core->FontSizeInDips();
|
|
const auto touchdownPoint = *_singleClickTouchdownPos;
|
|
const auto dx = pixelPosition.X - touchdownPoint.X;
|
|
const auto dy = pixelPosition.Y - touchdownPoint.Y;
|
|
const auto w = fontSizeInDips.Width;
|
|
const auto distanceSquared = dx * dx + dy * dy;
|
|
const auto maxDistanceSquared = w * w / 16; // (w / 4)^2
|
|
|
|
if (distanceSquared >= maxDistanceSquared)
|
|
{
|
|
// GH#9955.c: Make sure to use the terminal location of the
|
|
// _touchdown_ point here. We want to start the selection
|
|
// from where the user initially clicked, not where they are
|
|
// now.
|
|
auto termPos = _getTerminalPosition(til::point{ touchdownPoint }, false);
|
|
if (dx < 0)
|
|
{
|
|
// _getTerminalPosition(_, false) will floor the x-value,
|
|
// meaning that the selection will start on the left-side
|
|
// of the current cell. This is great if the use is dragging
|
|
// towards the right.
|
|
// If the user is dragging towards the left (dx < 0),
|
|
// we want to select the current cell, so place the anchor on the right
|
|
// side of the current cell.
|
|
termPos.x++;
|
|
}
|
|
_core->SetSelectionAnchor(termPos);
|
|
|
|
// stop tracking the touchdown point
|
|
_singleClickTouchdownPos = std::nullopt;
|
|
}
|
|
}
|
|
|
|
SetEndSelectionPoint(pixelPosition);
|
|
}
|
|
|
|
_core->SetHoveredCell(terminalPosition.to_core_point());
|
|
return handledCompletely;
|
|
}
|
|
|
|
void ControlInteractivity::TouchMoved(const winrt::Windows::Foundation::Point newTouchPoint,
|
|
const bool focused)
|
|
{
|
|
if (focused &&
|
|
_touchAnchor)
|
|
{
|
|
const auto anchor = _touchAnchor.value();
|
|
|
|
// Our actualFont's size is in pixels, convert to DIPs, which the
|
|
// rest of the Points here are in.
|
|
const auto fontSizeInDips{ _core->FontSizeInDips() };
|
|
|
|
// Get the difference between the point we've dragged to and the start of the touch.
|
|
const auto dy = static_cast<float>(newTouchPoint.Y - anchor.Y);
|
|
|
|
// Start viewport scroll after we've moved more than a half row of text
|
|
if (std::abs(dy) > (fontSizeInDips.Height / 2.0f))
|
|
{
|
|
// Multiply by -1, because moving the touch point down will
|
|
// create a positive delta, but we want the viewport to move up,
|
|
// so we'll need a negative scroll amount (and the inverse for
|
|
// panning down)
|
|
const auto numRows = dy / -fontSizeInDips.Height;
|
|
|
|
const auto currentOffset = _core->ScrollOffset();
|
|
const auto newValue = numRows + currentOffset;
|
|
|
|
// Update the Core's viewport position, and raise a
|
|
// ScrollPositionChanged event to update the scrollbar
|
|
UpdateScrollbar(newValue);
|
|
|
|
// Use this point as our new scroll anchor.
|
|
_touchAnchor = newTouchPoint;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ControlInteractivity::PointerReleased(Control::MouseButtonState buttonState,
|
|
const unsigned int pointerUpdateKind,
|
|
const ::Microsoft::Terminal::Core::ControlKeyStates modifiers,
|
|
const Core::Point pixelPosition)
|
|
{
|
|
const auto terminalPosition = _getTerminalPosition(til::point{ pixelPosition }, false);
|
|
// Short-circuit isReadOnly check to avoid warning dialog
|
|
if (!_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers))
|
|
{
|
|
_sendMouseEventHelper(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState);
|
|
return;
|
|
}
|
|
|
|
// Only a left click release when copy on select is active should perform a copy.
|
|
// Right clicks and middle clicks should not need to do anything when released.
|
|
const auto isLeftMouseRelease = pointerUpdateKind == WM_LBUTTONUP;
|
|
|
|
if (_core->CopyOnSelect() &&
|
|
isLeftMouseRelease &&
|
|
_selectionNeedsToBeCopied)
|
|
{
|
|
// IMPORTANT!
|
|
// DO NOT clear the selection here!
|
|
// Otherwise, the selection will be cleared immediately after you make it.
|
|
CopySelectionToClipboard(false, false, _core->Settings().CopyFormatting());
|
|
}
|
|
|
|
_singleClickTouchdownPos = std::nullopt;
|
|
}
|
|
|
|
void ControlInteractivity::TouchReleased()
|
|
{
|
|
_touchAnchor = std::nullopt;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Actually handle a scrolling event, whether from a mouse wheel or a
|
|
// touchpad scroll. Depending upon what modifier keys are pressed,
|
|
// different actions will take place.
|
|
// * Attempts to first dispatch the mouse scroll as a VT event
|
|
// * If Ctrl+Shift are pressed, then attempts to change our opacity
|
|
// * If just Ctrl is pressed, we'll attempt to "zoom" by changing our font size
|
|
// * Otherwise, just scrolls the content of the viewport
|
|
// Arguments:
|
|
// - point: the location of the mouse during this event
|
|
// - modifiers: The modifiers pressed during this event, in the form of a VirtualKeyModifiers
|
|
// - delta: the mouse wheel delta that triggered this event.
|
|
bool ControlInteractivity::MouseWheel(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers,
|
|
const Core::Point delta,
|
|
const Core::Point pixelPosition,
|
|
const Control::MouseButtonState buttonState)
|
|
{
|
|
const auto terminalPosition = _getTerminalPosition(til::point{ pixelPosition }, false);
|
|
|
|
// Short-circuit isReadOnly check to avoid warning dialog.
|
|
//
|
|
// GH#3321: Alternate scroll mode is a special type of mouse input mode
|
|
// where the terminal sends arrow keys when the user mouse wheels, but
|
|
// the client app doesn't care for other mouse input. It's tracked
|
|
// separately from _canSendVTMouseInput.
|
|
if (!_core->IsInReadOnlyMode() &&
|
|
(_canSendVTMouseInput(modifiers) || _shouldSendAlternateScroll(modifiers, delta)))
|
|
{
|
|
// Most mouse event handlers call
|
|
// _trySendMouseEvent(point);
|
|
// here with a PointerPoint. However, as of #979, we don't have a
|
|
// PointerPoint to work with. So, we're just going to do a
|
|
// mousewheel event manually
|
|
return _sendMouseEventHelper(terminalPosition,
|
|
delta.Y != 0 ? WM_MOUSEWHEEL : WM_MOUSEHWHEEL,
|
|
modifiers,
|
|
::base::saturated_cast<short>(delta.Y != 0 ? delta.Y : delta.X),
|
|
buttonState);
|
|
}
|
|
|
|
const auto ctrlPressed = modifiers.IsCtrlPressed();
|
|
const auto shiftPressed = modifiers.IsShiftPressed();
|
|
|
|
if (ctrlPressed && shiftPressed && _core->Settings().ScrollToChangeOpacity())
|
|
{
|
|
_mouseTransparencyHandler(delta.Y);
|
|
}
|
|
else if (ctrlPressed && !shiftPressed && _core->Settings().ScrollToZoom())
|
|
{
|
|
_mouseZoomHandler(delta.Y);
|
|
}
|
|
else
|
|
{
|
|
_mouseScrollHandler(delta.Y, pixelPosition, WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Adjust the opacity of the acrylic background in response to a mouse
|
|
// scrolling event.
|
|
// Arguments:
|
|
// - mouseDelta: the mouse wheel delta that triggered this event.
|
|
void ControlInteractivity::_mouseTransparencyHandler(const int32_t mouseDelta) const
|
|
{
|
|
// Transparency is on a scale of [0.0,1.0], so only increment by .01.
|
|
const auto effectiveDelta = mouseDelta < 0 ? -.01f : .01f;
|
|
_core->AdjustOpacity(effectiveDelta);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Adjust the font size of the terminal in response to a mouse scrolling
|
|
// event.
|
|
// Arguments:
|
|
// - mouseDelta: the mouse wheel delta that triggered this event.
|
|
void ControlInteractivity::_mouseZoomHandler(const int32_t mouseDelta) const
|
|
{
|
|
const auto fontDelta = mouseDelta < 0 ? -1.0f : 1.0f;
|
|
_core->AdjustFontSize(fontDelta);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Scroll the visible viewport in response to a mouse wheel event.
|
|
// Arguments:
|
|
// - mouseDelta: the mouse wheel delta that triggered this event.
|
|
// - pixelPosition: the location of the mouse during this event
|
|
// - isLeftButtonPressed: true iff the left mouse button was pressed during this event.
|
|
void ControlInteractivity::_mouseScrollHandler(const int32_t mouseDelta,
|
|
const Core::Point pixelPosition,
|
|
const bool isLeftButtonPressed)
|
|
{
|
|
// GH#9955.b: Start scrolling from our internal scrollbar position. This
|
|
// lets us accumulate fractional numbers of rows to scroll with each
|
|
// event. Especially for precision trackpads, we might be getting scroll
|
|
// deltas smaller than a single row, but we still want lots of those to
|
|
// accumulate.
|
|
//
|
|
// At the start, let's compare what we _think_ the scrollbar is, with
|
|
// what it should be. It's possible the core scrolled out from
|
|
// underneath us. We wouldn't know - we don't want the overhead of
|
|
// another ScrollPositionChanged handler. If the scrollbar should be
|
|
// somewhere other than where it is currently, then start from that row.
|
|
const auto currentInternalRow = std::lround(_internalScrollbarPosition);
|
|
const auto currentCoreRow = _core->ScrollOffset();
|
|
const auto currentOffset = currentInternalRow == currentCoreRow ?
|
|
_internalScrollbarPosition :
|
|
currentCoreRow;
|
|
|
|
// negative = down, positive = up
|
|
// However, for us, the signs are flipped.
|
|
// With one of the precision mice, one click is always a multiple of 120 (WHEEL_DELTA),
|
|
// but the "smooth scrolling" mode results in non-int values
|
|
const auto rowDelta = mouseDelta / (-1.0f * WHEEL_DELTA);
|
|
|
|
// WHEEL_PAGESCROLL is a Win32 constant that represents the "scroll one page
|
|
// at a time" setting. If we ignore it, we will scroll a truly absurd number
|
|
// of rows.
|
|
const auto rowsToScroll{ _rowsToScroll == WHEEL_PAGESCROLL ? _core->ViewHeight() : _rowsToScroll };
|
|
const auto newValue = rowsToScroll * rowDelta + currentOffset;
|
|
|
|
// Update the Core's viewport position, and raise a
|
|
// ScrollPositionChanged event to update the scrollbar
|
|
UpdateScrollbar(newValue);
|
|
|
|
if (isLeftButtonPressed)
|
|
{
|
|
// If user is mouse selecting and scrolls, they then point at new
|
|
// character. Make sure selection reflects that immediately.
|
|
SetEndSelectionPoint(pixelPosition);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update the scroll position in such a way that should update the
|
|
// scrollbar. For example, when scrolling the buffer with the mouse or
|
|
// touch input. This will both update the Core's Terminal's buffer
|
|
// location, then also raise our own ScrollPositionChanged event.
|
|
// UserScrollViewport _won't_ raise the core's ScrollPositionChanged
|
|
// event, because it's assumed that's already being called from a context
|
|
// that knows about the change to the scrollbar. So we need to raise the
|
|
// event on our own.
|
|
// - The hosting control should make sure to listen to our own
|
|
// ScrollPositionChanged event and use that as an opportunity to update
|
|
// the location of the scrollbar.
|
|
// Arguments:
|
|
// - newValue: The new top of the viewport
|
|
// Return Value:
|
|
// - <none>
|
|
void ControlInteractivity::UpdateScrollbar(const float newValue)
|
|
{
|
|
// Set this as the new value of our internal scrollbar representation.
|
|
// We're doing this so we can accumulate fractional amounts of a row to
|
|
// scroll each time the mouse scrolls.
|
|
_internalScrollbarPosition = std::clamp(newValue, 0.0f, static_cast<float>(_core->BufferHeight()));
|
|
|
|
// If the new scrollbar position, rounded to an int, is at a different
|
|
// row, then actually update the scroll position in the core, and raise
|
|
// a ScrollPositionChanged to inform the control.
|
|
const auto viewTop = std::lround(_internalScrollbarPosition);
|
|
if (viewTop != _core->ScrollOffset())
|
|
{
|
|
_core->UserScrollViewport(viewTop);
|
|
|
|
// _core->ScrollOffset() is now set to newValue
|
|
ScrollPositionChanged.raise(*this,
|
|
winrt::make<ScrollPositionChangedArgs>(_core->ScrollOffset(),
|
|
_core->ViewHeight(),
|
|
_core->BufferHeight()));
|
|
}
|
|
}
|
|
|
|
void ControlInteractivity::_hyperlinkHandler(const std::wstring_view uri)
|
|
{
|
|
OpenHyperlink.raise(*this, winrt::make<OpenHyperlinkEventArgs>(winrt::hstring{ uri }));
|
|
}
|
|
|
|
bool ControlInteractivity::_canSendVTMouseInput(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers)
|
|
{
|
|
// If the user is holding down Shift, suppress mouse events
|
|
// TODO GH#4875: disable/customize this functionality
|
|
if (modifiers.IsShiftPressed())
|
|
{
|
|
return false;
|
|
}
|
|
return _core->IsVtMouseModeEnabled();
|
|
}
|
|
|
|
bool ControlInteractivity::_shouldSendAlternateScroll(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const Core::Point delta)
|
|
{
|
|
// If the user is holding down Shift, suppress mouse events
|
|
// TODO GH#4875: disable/customize this functionality
|
|
if (modifiers.IsShiftPressed())
|
|
{
|
|
return false;
|
|
}
|
|
if (delta.Y != 0)
|
|
{
|
|
return _core->ShouldSendAlternateScroll(WM_MOUSEWHEEL, delta.Y);
|
|
}
|
|
else
|
|
{
|
|
return _core->ShouldSendAlternateScroll(WM_MOUSEHWHEEL, delta.X);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Sets selection's end position to match supplied cursor position, e.g. while mouse dragging.
|
|
// Arguments:
|
|
// - cursorPosition: in pixels, relative to the origin of the control
|
|
void ControlInteractivity::SetEndSelectionPoint(const Core::Point pixelPosition)
|
|
{
|
|
_core->SetEndSelectionPoint(_getTerminalPosition(til::point{ pixelPosition }, true));
|
|
_selectionNeedsToBeCopied = true;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Gets the corresponding viewport terminal position for the point in
|
|
// pixels, by normalizing with the font size.
|
|
// Arguments:
|
|
// - pixelPosition: the (x,y) position of a given point (i.e.: mouse cursor).
|
|
// NOTE: origin (0,0) is top-left.
|
|
// - roundToNearestCell: if true, round the x-value. Otherwise, floor it (standard int division)
|
|
// Return Value:
|
|
// - the corresponding viewport terminal position for the given Point parameter
|
|
til::point ControlInteractivity::_getTerminalPosition(const til::point pixelPosition, bool roundToNearestCell)
|
|
{
|
|
// Get the size of the font, which is in pixels
|
|
const auto fontSize{ _core->GetFont().GetSize() };
|
|
|
|
if (roundToNearestCell)
|
|
{
|
|
// GH#5099: round the x-value to the nearest cell
|
|
til::point result;
|
|
result.x = gsl::narrow_cast<til::CoordType>(std::round(gsl::narrow_cast<double>(pixelPosition.x) / fontSize.width));
|
|
result.y = pixelPosition.y / fontSize.height;
|
|
return result;
|
|
}
|
|
// Convert the location in pixels to characters within the current viewport.
|
|
return pixelPosition / fontSize;
|
|
}
|
|
|
|
bool ControlInteractivity::_sendMouseEventHelper(const til::point terminalPosition,
|
|
const unsigned int pointerUpdateKind,
|
|
const ::Microsoft::Terminal::Core::ControlKeyStates modifiers,
|
|
const SHORT wheelDelta,
|
|
Control::MouseButtonState buttonState)
|
|
{
|
|
const auto adjustment = _core->BufferHeight() - _core->ScrollOffset() - _core->ViewHeight();
|
|
// If the click happened outside the active region, core should get a chance to filter it out or clamp it.
|
|
const auto adjustedY = terminalPosition.y - adjustment;
|
|
return _core->SendMouseEvent({ terminalPosition.x, adjustedY }, pointerUpdateKind, modifiers, wheelDelta, toInternalMouseState(buttonState));
|
|
}
|
|
|
|
// Method Description:
|
|
// - Creates an automation peer for the Terminal Control, enabling
|
|
// accessibility on our control.
|
|
// - Our implementation implements the ITextProvider pattern, and the
|
|
// IControlAccessibilityInfo, to connect to the UiaEngine, which must be
|
|
// attached to the core's renderer.
|
|
// - The TermControlAutomationPeer will connect this to the UI tree.
|
|
// Arguments:
|
|
// - None
|
|
// Return Value:
|
|
// - The automation peer for our control
|
|
Control::InteractivityAutomationPeer ControlInteractivity::OnCreateAutomationPeer()
|
|
try
|
|
{
|
|
const auto autoPeer = winrt::make_self<implementation::InteractivityAutomationPeer>(this);
|
|
if (_uiaEngine)
|
|
{
|
|
_core->DetachUiaEngine(_uiaEngine.get());
|
|
}
|
|
_uiaEngine = std::make_unique<::Microsoft::Console::Render::UiaEngine>(autoPeer.get());
|
|
_core->AttachUiaEngine(_uiaEngine.get());
|
|
return *autoPeer;
|
|
}
|
|
catch (...)
|
|
{
|
|
LOG_CAUGHT_EXCEPTION();
|
|
return nullptr;
|
|
}
|
|
|
|
::Microsoft::Console::Render::IRenderData* ControlInteractivity::GetRenderData() const
|
|
{
|
|
return _core->GetRenderData();
|
|
}
|
|
}
|