mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-11 13:56:33 -06:00
`IInputEvent` makes adding Unicode support to `InputBuffer` more
difficult than necessary as the abstract class makes downcasting
as well as copying quite verbose. I found that using `INPUT_RECORD`s
directly leads to a significantly simplified implementation.
In addition, this commit fixes at least one bug: The previous approach
to detect the null key via `DoActiveModifierKeysMatch` didn't work.
As it compared the modifier keys as a bitset with `==` it failed to
match whenever the numpad key was set, which it usually is.
## Validation Steps Performed
* Unit and feature tests are ✅
2457 lines
96 KiB
C++
2457 lines
96 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.h"
|
|
#include "ControlCore.h"
|
|
|
|
// MidiAudio
|
|
#include <mmeapi.h>
|
|
#include <dsound.h>
|
|
|
|
#include <DefaultSettings.h>
|
|
#include <unicode.hpp>
|
|
#include <WinUser.h>
|
|
#include <LibraryResources.h>
|
|
|
|
#include "EventArgs.h"
|
|
#include "../../types/inc/GlyphWidth.hpp"
|
|
#include "../../buffer/out/search.h"
|
|
#include "../../renderer/atlas/AtlasEngine.h"
|
|
#include "../../renderer/dx/DxRenderer.hpp"
|
|
|
|
#include "ControlCore.g.cpp"
|
|
#include "SelectionColor.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;
|
|
|
|
// The minimum delay between updates to the scroll bar's values.
|
|
// The updates are throttled to limit power usage.
|
|
constexpr const auto ScrollBarUpdateInterval = std::chrono::milliseconds(8);
|
|
|
|
// The minimum delay between updating the TSF input control.
|
|
constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(100);
|
|
|
|
// The minimum delay between updating the locations of regex patterns
|
|
constexpr const auto UpdatePatternLocationsInterval = std::chrono::milliseconds(500);
|
|
|
|
namespace winrt::Microsoft::Terminal::Control::implementation
|
|
{
|
|
static winrt::Microsoft::Terminal::Core::OptionalColor OptionalFromColor(const til::color& c)
|
|
{
|
|
Core::OptionalColor result;
|
|
result.Color = c;
|
|
result.HasValue = true;
|
|
return result;
|
|
}
|
|
static winrt::Microsoft::Terminal::Core::OptionalColor OptionalFromColor(const std::optional<til::color>& c)
|
|
{
|
|
Core::OptionalColor result;
|
|
if (c.has_value())
|
|
{
|
|
result.Color = *c;
|
|
result.HasValue = true;
|
|
}
|
|
else
|
|
{
|
|
result.HasValue = false;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
TextColor SelectionColor::AsTextColor() const noexcept
|
|
{
|
|
if (IsIndex16())
|
|
{
|
|
return { Color().r, false };
|
|
}
|
|
else
|
|
{
|
|
return { static_cast<COLORREF>(Color()) };
|
|
}
|
|
}
|
|
|
|
ControlCore::ControlCore(Control::IControlSettings settings,
|
|
Control::IControlAppearance unfocusedAppearance,
|
|
TerminalConnection::ITerminalConnection connection) :
|
|
_desiredFont{ DEFAULT_FONT_FACE, 0, DEFAULT_FONT_WEIGHT, DEFAULT_FONT_SIZE, CP_UTF8 },
|
|
_actualFont{ DEFAULT_FONT_FACE, 0, DEFAULT_FONT_WEIGHT, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false }
|
|
{
|
|
_settings = winrt::make_self<implementation::ControlSettings>(settings, unfocusedAppearance);
|
|
_terminal = std::make_shared<::Microsoft::Terminal::Core::Terminal>();
|
|
|
|
_setupDispatcherAndCallbacks();
|
|
|
|
Connection(connection);
|
|
|
|
_terminal->SetWriteInputCallback([this](std::wstring_view wstr) {
|
|
_sendInputToConnection(wstr);
|
|
});
|
|
|
|
// GH#8969: pre-seed working directory to prevent potential races
|
|
_terminal->SetWorkingDirectory(_settings->StartingDirectory());
|
|
|
|
auto pfnCopyToClipboard = std::bind(&ControlCore::_terminalCopyToClipboard, this, std::placeholders::_1);
|
|
_terminal->SetCopyToClipboardCallback(pfnCopyToClipboard);
|
|
|
|
auto pfnWarningBell = std::bind(&ControlCore::_terminalWarningBell, this);
|
|
_terminal->SetWarningBellCallback(pfnWarningBell);
|
|
|
|
auto pfnTitleChanged = std::bind(&ControlCore::_terminalTitleChanged, this, std::placeholders::_1);
|
|
_terminal->SetTitleChangedCallback(pfnTitleChanged);
|
|
|
|
auto pfnScrollPositionChanged = std::bind(&ControlCore::_terminalScrollPositionChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
|
|
_terminal->SetScrollPositionChangedCallback(pfnScrollPositionChanged);
|
|
|
|
auto pfnTerminalCursorPositionChanged = std::bind(&ControlCore::_terminalCursorPositionChanged, this);
|
|
_terminal->SetCursorPositionChangedCallback(pfnTerminalCursorPositionChanged);
|
|
|
|
auto pfnTerminalTaskbarProgressChanged = std::bind(&ControlCore::_terminalTaskbarProgressChanged, this);
|
|
_terminal->TaskbarProgressChangedCallback(pfnTerminalTaskbarProgressChanged);
|
|
|
|
auto pfnShowWindowChanged = std::bind(&ControlCore::_terminalShowWindowChanged, this, std::placeholders::_1);
|
|
_terminal->SetShowWindowCallback(pfnShowWindowChanged);
|
|
|
|
auto pfnPlayMidiNote = std::bind(&ControlCore::_terminalPlayMidiNote, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
|
|
_terminal->SetPlayMidiNoteCallback(pfnPlayMidiNote);
|
|
|
|
// MSFT 33353327: Initialize the renderer in the ctor instead of Initialize().
|
|
// We need the renderer to be ready to accept new engines before the SwapChainPanel is ready to go.
|
|
// If we wait, a screen reader may try to get the AutomationPeer (aka the UIA Engine), and we won't be able to attach
|
|
// the UIA Engine to the renderer. This prevents us from signaling changes to the cursor or buffer.
|
|
{
|
|
// First create the render thread.
|
|
// Then stash a local pointer to the render thread so we can initialize it and enable it
|
|
// to paint itself *after* we hand off its ownership to the renderer.
|
|
// We split up construction and initialization of the render thread object this way
|
|
// because the renderer and render thread have circular references to each other.
|
|
auto renderThread = std::make_unique<::Microsoft::Console::Render::RenderThread>();
|
|
auto* const localPointerToThread = renderThread.get();
|
|
|
|
// Now create the renderer and initialize the render thread.
|
|
const auto& renderSettings = _terminal->GetRenderSettings();
|
|
_renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(renderSettings, _terminal.get(), nullptr, 0, std::move(renderThread));
|
|
|
|
_renderer->SetBackgroundColorChangedCallback([this]() { _rendererBackgroundColorChanged(); });
|
|
_renderer->SetFrameColorChangedCallback([this]() { _rendererTabColorChanged(); });
|
|
_renderer->SetRendererEnteredErrorStateCallback([this]() { _RendererEnteredErrorStateHandlers(nullptr, nullptr); });
|
|
|
|
THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get()));
|
|
}
|
|
|
|
UpdateSettings(settings, unfocusedAppearance);
|
|
}
|
|
|
|
void ControlCore::_setupDispatcherAndCallbacks()
|
|
{
|
|
// Get our dispatcher. If we're hosted in-proc with XAML, this will get
|
|
// us the same dispatcher as TermControl::Dispatcher(). If we're out of
|
|
// proc, this'll return null. We'll need to instead make a new
|
|
// DispatcherQueue (on a new thread), so we can use that for throttled
|
|
// functions.
|
|
_dispatcher = winrt::Windows::System::DispatcherQueue::GetForCurrentThread();
|
|
if (!_dispatcher)
|
|
{
|
|
auto controller{ winrt::Windows::System::DispatcherQueueController::CreateOnDedicatedThread() };
|
|
_dispatcher = controller.DispatcherQueue();
|
|
}
|
|
|
|
// A few different events should be throttled, so they don't fire absolutely all the time:
|
|
// * _tsfTryRedrawCanvas: When the cursor position moves, we need to
|
|
// inform TSF, so it can move the canvas for the composition. We
|
|
// throttle this so that we're not hopping across the process boundary
|
|
// every time that the cursor moves.
|
|
// * _updatePatternLocations: When there's new output, or we scroll the
|
|
// viewport, we should re-check if there are any visible hyperlinks.
|
|
// But we don't really need to do this every single time text is
|
|
// output, we can limit this update to once every 500ms.
|
|
// * _updateScrollBar: Same idea as the TSF update - we don't _really_
|
|
// need to hop across the process boundary every time text is output.
|
|
// We can throttle this to once every 8ms, which will get us out of
|
|
// the way of the main output & rendering threads.
|
|
const auto shared = _shared.lock();
|
|
shared->tsfTryRedrawCanvas = std::make_shared<ThrottledFuncTrailing<>>(
|
|
_dispatcher,
|
|
TsfRedrawInterval,
|
|
[weakThis = get_weak()]() {
|
|
if (auto core{ weakThis.get() }; !core->_IsClosing())
|
|
{
|
|
core->_CursorPositionChangedHandlers(*core, nullptr);
|
|
}
|
|
});
|
|
|
|
// NOTE: Calling UpdatePatternLocations from a background
|
|
// thread is a workaround for us to hit GH#12607 less often.
|
|
shared->updatePatternLocations = std::make_unique<til::throttled_func_trailing<>>(
|
|
UpdatePatternLocationsInterval,
|
|
[weakTerminal = std::weak_ptr{ _terminal }]() {
|
|
if (const auto t = weakTerminal.lock())
|
|
{
|
|
auto lock = t->LockForWriting();
|
|
t->UpdatePatternsUnderLock();
|
|
}
|
|
});
|
|
|
|
shared->updateScrollBar = std::make_shared<ThrottledFuncTrailing<Control::ScrollPositionChangedArgs>>(
|
|
_dispatcher,
|
|
ScrollBarUpdateInterval,
|
|
[weakThis = get_weak()](const auto& update) {
|
|
if (auto core{ weakThis.get() }; !core->_IsClosing())
|
|
{
|
|
core->_ScrollPositionChangedHandlers(*core, update);
|
|
}
|
|
});
|
|
}
|
|
|
|
ControlCore::~ControlCore()
|
|
{
|
|
Close();
|
|
|
|
if (_renderer)
|
|
{
|
|
_renderer->TriggerTeardown();
|
|
}
|
|
}
|
|
|
|
void ControlCore::Detach()
|
|
{
|
|
// Disable the renderer, so that it doesn't try to start any new frames
|
|
// for our engines while we're not attached to anything.
|
|
_renderer->WaitForPaintCompletionAndDisable(INFINITE);
|
|
|
|
// Clear out any throttled funcs that we had wired up to run on this UI
|
|
// thread. These will be recreated in _setupDispatcherAndCallbacks, when
|
|
// we're re-attached to a new control (on a possibly new UI thread).
|
|
const auto shared = _shared.lock();
|
|
shared->tsfTryRedrawCanvas.reset();
|
|
shared->updatePatternLocations.reset();
|
|
shared->updateScrollBar.reset();
|
|
}
|
|
|
|
void ControlCore::AttachToNewControl(const Microsoft::Terminal::Control::IKeyBindings& keyBindings)
|
|
{
|
|
_settings->KeyBindings(keyBindings);
|
|
_setupDispatcherAndCallbacks();
|
|
const auto actualNewSize = _actualFont.GetSize();
|
|
// Bubble this up, so our new control knows how big we want the font.
|
|
_FontSizeChangedHandlers(actualNewSize.width, actualNewSize.height, true);
|
|
|
|
// The renderer will be re-enabled in Initialize
|
|
|
|
_AttachedHandlers(*this, nullptr);
|
|
}
|
|
|
|
TerminalConnection::ITerminalConnection ControlCore::Connection()
|
|
{
|
|
return _connection;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Setup our event handlers for this connection. If we've currently got a
|
|
// connection, then this'll revoke the existing connection's handlers.
|
|
// - This will not call Start on the incoming connection. The caller should do that.
|
|
// - If the caller doesn't want the old connection to be closed, then they
|
|
// should grab a reference to it before calling this (so that it doesn't
|
|
// destruct, and close) during this call.
|
|
void ControlCore::Connection(const TerminalConnection::ITerminalConnection& newConnection)
|
|
{
|
|
auto oldState = ConnectionState(); // rely on ControlCore's automatic null handling
|
|
// revoke ALL old handlers immediately
|
|
|
|
_connectionOutputEventRevoker.revoke();
|
|
_connectionStateChangedRevoker.revoke();
|
|
|
|
_connection = newConnection;
|
|
if (_connection)
|
|
{
|
|
// Subscribe to the connection's disconnected event and call our connection closed handlers.
|
|
_connectionStateChangedRevoker = newConnection.StateChanged(winrt::auto_revoke, [this](auto&& /*s*/, auto&& /*v*/) {
|
|
_ConnectionStateChangedHandlers(*this, nullptr);
|
|
});
|
|
|
|
// Get our current size in rows/cols, and hook them up to
|
|
// this connection too.
|
|
{
|
|
const auto vp = _terminal->GetViewport();
|
|
const auto width = vp.Width();
|
|
const auto height = vp.Height();
|
|
|
|
newConnection.Resize(height, width);
|
|
}
|
|
// Window owner too.
|
|
if (auto conpty{ newConnection.try_as<TerminalConnection::ConptyConnection>() })
|
|
{
|
|
conpty.ReparentWindow(_owningHwnd);
|
|
}
|
|
|
|
// This event is explicitly revoked in the destructor: does not need weak_ref
|
|
_connectionOutputEventRevoker = _connection.TerminalOutput(winrt::auto_revoke, { this, &ControlCore::_connectionOutputHandler });
|
|
}
|
|
|
|
// Fire off a connection state changed notification, to let our hosting
|
|
// app know that we're in a different state now.
|
|
if (oldState != ConnectionState())
|
|
{ // rely on the null handling again
|
|
// send the notification
|
|
_ConnectionStateChangedHandlers(*this, nullptr);
|
|
}
|
|
}
|
|
|
|
bool ControlCore::Initialize(const float actualWidth,
|
|
const float actualHeight,
|
|
const float compositionScale)
|
|
{
|
|
assert(_settings);
|
|
|
|
_panelWidth = actualWidth;
|
|
_panelHeight = actualHeight;
|
|
_compositionScale = compositionScale;
|
|
|
|
{ // scope for terminalLock
|
|
auto terminalLock = _terminal->LockForWriting();
|
|
|
|
if (_initializedTerminal.load(std::memory_order_relaxed))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const auto windowWidth = actualWidth * compositionScale;
|
|
const auto windowHeight = actualHeight * compositionScale;
|
|
|
|
if (windowWidth == 0 || windowHeight == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (_settings->UseAtlasEngine())
|
|
{
|
|
_renderEngine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
|
|
}
|
|
else
|
|
{
|
|
_renderEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
|
|
}
|
|
|
|
_renderer->AddRenderEngine(_renderEngine.get());
|
|
|
|
// Initialize our font with the renderer
|
|
// We don't have to care about DPI. We'll get a change message immediately if it's not 96
|
|
// and react accordingly.
|
|
_updateFont(true);
|
|
|
|
const til::size windowSize{ til::math::rounding, windowWidth, windowHeight };
|
|
|
|
// First set up the dx engine with the window size in pixels.
|
|
// Then, using the font, get the number of characters that can fit.
|
|
// Resize our terminal connection to match that size, and initialize the terminal with that size.
|
|
const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize);
|
|
LOG_IF_FAILED(_renderEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() }));
|
|
|
|
// Update DxEngine's SelectionBackground
|
|
_renderEngine->SetSelectionBackground(til::color{ _settings->SelectionBackground() });
|
|
|
|
const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels);
|
|
const auto width = vp.Width();
|
|
const auto height = vp.Height();
|
|
_connection.Resize(height, width);
|
|
|
|
if (_owningHwnd != 0)
|
|
{
|
|
if (auto conpty{ _connection.try_as<TerminalConnection::ConptyConnection>() })
|
|
{
|
|
conpty.ReparentWindow(_owningHwnd);
|
|
}
|
|
}
|
|
|
|
// Override the default width and height to match the size of the swapChainPanel
|
|
_settings->InitialCols(width);
|
|
_settings->InitialRows(height);
|
|
|
|
_terminal->CreateFromSettings(*_settings, *_renderer);
|
|
|
|
// IMPORTANT! Set this callback up sooner than later. If we do it
|
|
// after Enable, then it'll be possible to paint the frame once
|
|
// _before_ the warning handler is set up, and then warnings from
|
|
// the first paint will be ignored!
|
|
_renderEngine->SetWarningCallback(std::bind(&ControlCore::_rendererWarning, this, std::placeholders::_1));
|
|
|
|
// Tell the render engine to notify us when the swap chain changes.
|
|
// We do this after we initially set the swapchain so as to avoid
|
|
// unnecessary callbacks (and locking problems)
|
|
_renderEngine->SetCallback([this](HANDLE handle) {
|
|
_renderEngineSwapChainChanged(handle);
|
|
});
|
|
|
|
_renderEngine->SetRetroTerminalEffect(_settings->RetroTerminalEffect());
|
|
_renderEngine->SetPixelShaderPath(_settings->PixelShaderPath());
|
|
_renderEngine->SetForceFullRepaintRendering(_settings->ForceFullRepaintRendering());
|
|
_renderEngine->SetSoftwareRendering(_settings->SoftwareRendering());
|
|
|
|
_updateAntiAliasingMode();
|
|
|
|
// GH#5098: Inform the engine of the opacity of the default text background.
|
|
// GH#11315: Always do this, even if they don't have acrylic on.
|
|
_renderEngine->EnableTransparentBackground(_isBackgroundTransparent());
|
|
|
|
THROW_IF_FAILED(_renderEngine->Enable());
|
|
|
|
_initializedTerminal.store(true, std::memory_order_relaxed);
|
|
} // scope for TerminalLock
|
|
|
|
// Start the connection outside of lock, because it could
|
|
// start writing output immediately.
|
|
_connection.Start();
|
|
|
|
return true;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Tell the renderer to start painting.
|
|
// - !! IMPORTANT !! Make sure that we've attached our swap chain to an
|
|
// actual target before calling this.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void ControlCore::EnablePainting()
|
|
{
|
|
if (_initializedTerminal.load(std::memory_order_relaxed))
|
|
{
|
|
_renderer->EnablePainting();
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Writes the given sequence as input to the active terminal connection.
|
|
// - This method has been overloaded to allow zero-copy winrt::param::hstring optimizations.
|
|
// Arguments:
|
|
// - wstr: the string of characters to write to the terminal connection.
|
|
// Return Value:
|
|
// - <none>
|
|
void ControlCore::_sendInputToConnection(std::wstring_view wstr)
|
|
{
|
|
if (_isReadOnly)
|
|
{
|
|
_raiseReadOnlyWarning();
|
|
}
|
|
else
|
|
{
|
|
_connection.WriteInput(wstr);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Writes the given sequence as input to the active terminal connection,
|
|
// Arguments:
|
|
// - wstr: the string of characters to write to the terminal connection.
|
|
// Return Value:
|
|
// - <none>
|
|
void ControlCore::SendInput(const winrt::hstring& wstr)
|
|
{
|
|
_sendInputToConnection(wstr);
|
|
}
|
|
|
|
bool ControlCore::SendCharEvent(const wchar_t ch,
|
|
const WORD scanCode,
|
|
const ::Microsoft::Terminal::Core::ControlKeyStates modifiers)
|
|
{
|
|
const wchar_t CtrlD = 0x4;
|
|
const wchar_t Enter = '\r';
|
|
|
|
if (_connection.State() >= winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::Closed)
|
|
{
|
|
if (ch == CtrlD)
|
|
{
|
|
_CloseTerminalRequestedHandlers(*this, nullptr);
|
|
return true;
|
|
}
|
|
|
|
if (ch == Enter)
|
|
{
|
|
// Ask the hosting application to give us a new connection.
|
|
_RestartTerminalRequestedHandlers(*this, nullptr);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (ch == L'\x3') // Ctrl+C or Ctrl+Break
|
|
{
|
|
_handleControlC();
|
|
}
|
|
|
|
return _terminal->SendCharEvent(ch, scanCode, modifiers);
|
|
}
|
|
|
|
void ControlCore::_handleControlC()
|
|
{
|
|
if (!_midiAudioSkipTimer)
|
|
{
|
|
_midiAudioSkipTimer = _dispatcher.CreateTimer();
|
|
_midiAudioSkipTimer.Interval(std::chrono::seconds(1));
|
|
_midiAudioSkipTimer.IsRepeating(false);
|
|
_midiAudioSkipTimer.Tick([weakSelf = get_weak()](auto&&, auto&&) {
|
|
if (const auto self = weakSelf.get())
|
|
{
|
|
self->_midiAudio.EndSkip();
|
|
}
|
|
});
|
|
}
|
|
|
|
_midiAudio.BeginSkip();
|
|
_midiAudioSkipTimer.Start();
|
|
}
|
|
|
|
bool ControlCore::_shouldTryUpdateSelection(const WORD vkey)
|
|
{
|
|
// GH#6423 - don't update selection if the key that was pressed was a
|
|
// modifier key. We'll wait for a real keystroke to dismiss the
|
|
// GH #7395 - don't update selection when taking PrintScreen
|
|
// selection.
|
|
return HasSelection() && ::Microsoft::Terminal::Core::Terminal::IsInputKey(vkey);
|
|
}
|
|
|
|
bool ControlCore::TryMarkModeKeybinding(const WORD vkey,
|
|
const ::Microsoft::Terminal::Core::ControlKeyStates mods)
|
|
{
|
|
if (_shouldTryUpdateSelection(vkey) && _terminal->SelectionMode() == ::Terminal::SelectionInteractionMode::Mark)
|
|
{
|
|
if (vkey == 'A' && !mods.IsAltPressed() && !mods.IsShiftPressed() && mods.IsCtrlPressed())
|
|
{
|
|
// Ctrl + A --> Select all
|
|
auto lock = _terminal->LockForWriting();
|
|
_terminal->SelectAll();
|
|
_updateSelectionUI();
|
|
return true;
|
|
}
|
|
else if (vkey == VK_TAB && !mods.IsAltPressed() && !mods.IsCtrlPressed() && _settings->DetectURLs())
|
|
{
|
|
// [Shift +] Tab --> next/previous hyperlink
|
|
auto lock = _terminal->LockForWriting();
|
|
const auto direction = mods.IsShiftPressed() ? ::Terminal::SearchDirection::Backward : ::Terminal::SearchDirection::Forward;
|
|
_terminal->SelectHyperlink(direction);
|
|
_updateSelectionUI();
|
|
return true;
|
|
}
|
|
else if (vkey == VK_RETURN && mods.IsCtrlPressed() && !mods.IsAltPressed() && !mods.IsShiftPressed())
|
|
{
|
|
// Ctrl + Enter --> Open URL
|
|
auto lock = _terminal->LockForReading();
|
|
if (const auto uri = _terminal->GetHyperlinkAtBufferPosition(_terminal->GetSelectionAnchor()); !uri.empty())
|
|
{
|
|
_OpenHyperlinkHandlers(*this, winrt::make<OpenHyperlinkEventArgs>(winrt::hstring{ uri }));
|
|
}
|
|
else
|
|
{
|
|
const auto selectedText = _terminal->GetTextBuffer().GetPlainText(_terminal->GetSelectionAnchor(), _terminal->GetSelectionEnd());
|
|
_OpenHyperlinkHandlers(*this, winrt::make<OpenHyperlinkEventArgs>(winrt::hstring{ selectedText }));
|
|
}
|
|
return true;
|
|
}
|
|
else if (vkey == VK_RETURN && !mods.IsCtrlPressed() && !mods.IsAltPressed())
|
|
{
|
|
// [Shift +] Enter --> copy text
|
|
// Don't lock here! CopySelectionToClipboard already locks for you!
|
|
CopySelectionToClipboard(mods.IsShiftPressed(), nullptr);
|
|
_terminal->ClearSelection();
|
|
_updateSelectionUI();
|
|
return true;
|
|
}
|
|
else if (vkey == VK_ESCAPE)
|
|
{
|
|
_terminal->ClearSelection();
|
|
_updateSelectionUI();
|
|
return true;
|
|
}
|
|
else if (const auto updateSlnParams{ _terminal->ConvertKeyEventToUpdateSelectionParams(mods, vkey) })
|
|
{
|
|
// try to update the selection
|
|
auto lock = _terminal->LockForWriting();
|
|
_terminal->UpdateSelection(updateSlnParams->first, updateSlnParams->second, mods);
|
|
_updateSelectionUI();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Send this particular key event to the terminal.
|
|
// See Terminal::SendKeyEvent for more information.
|
|
// - Clears the current selection.
|
|
// - Makes the cursor briefly visible during typing.
|
|
// Arguments:
|
|
// - vkey: The vkey of the key pressed.
|
|
// - scanCode: The scan code of the key pressed.
|
|
// - modifiers: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states.
|
|
// - keyDown: If true, the key was pressed, otherwise the key was released.
|
|
bool ControlCore::TrySendKeyEvent(const WORD vkey,
|
|
const WORD scanCode,
|
|
const ControlKeyStates modifiers,
|
|
const bool keyDown)
|
|
{
|
|
// Update the selection, if it's present
|
|
// GH#8522, GH#3758 - Only modify the selection on key _down_. If we
|
|
// modify on key up, then there's chance that we'll immediately dismiss
|
|
// a selection created by an action bound to a keydown.
|
|
if (_shouldTryUpdateSelection(vkey) && keyDown)
|
|
{
|
|
// try to update the selection
|
|
if (const auto updateSlnParams{ _terminal->ConvertKeyEventToUpdateSelectionParams(modifiers, vkey) })
|
|
{
|
|
auto lock = _terminal->LockForWriting();
|
|
_terminal->UpdateSelection(updateSlnParams->first, updateSlnParams->second, modifiers);
|
|
_updateSelectionUI();
|
|
return true;
|
|
}
|
|
|
|
// GH#8791 - don't dismiss selection if Windows key was also pressed as a key-combination.
|
|
if (!modifiers.IsWinPressed())
|
|
{
|
|
_terminal->ClearSelection();
|
|
_updateSelectionUI();
|
|
}
|
|
|
|
// When there is a selection active, escape should clear it and NOT flow through
|
|
// to the terminal. With any other keypress, it should clear the selection AND
|
|
// flow through to the terminal.
|
|
if (vkey == VK_ESCAPE)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// If the terminal translated the key, mark the event as handled.
|
|
// This will prevent the system from trying to get the character out
|
|
// of it and sending us a CharacterReceived event.
|
|
return vkey ? _terminal->SendKeyEvent(vkey,
|
|
scanCode,
|
|
modifiers,
|
|
keyDown) :
|
|
true;
|
|
}
|
|
|
|
bool ControlCore::SendMouseEvent(const til::point viewportPos,
|
|
const unsigned int uiButton,
|
|
const ControlKeyStates states,
|
|
const short wheelDelta,
|
|
const TerminalInput::MouseButtonState state)
|
|
{
|
|
return _terminal->SendMouseEvent(viewportPos, uiButton, states, wheelDelta, state);
|
|
}
|
|
|
|
void ControlCore::UserScrollViewport(const int viewTop)
|
|
{
|
|
// Clear the regex pattern tree so the renderer does not try to render them while scrolling
|
|
_terminal->ClearPatternTree();
|
|
|
|
// This is a scroll event that wasn't initiated by the terminal
|
|
// itself - it was initiated by the mouse wheel, or the scrollbar.
|
|
_terminal->UserScrollViewport(viewTop);
|
|
|
|
const auto shared = _shared.lock_shared();
|
|
if (shared->updatePatternLocations)
|
|
{
|
|
(*shared->updatePatternLocations)();
|
|
}
|
|
}
|
|
|
|
void ControlCore::AdjustOpacity(const double adjustment)
|
|
{
|
|
if (adjustment == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_setOpacity(Opacity() + adjustment);
|
|
}
|
|
|
|
void ControlCore::_setOpacity(const double opacity)
|
|
{
|
|
const auto newOpacity = std::clamp(opacity,
|
|
0.0,
|
|
1.0);
|
|
|
|
if (newOpacity == Opacity())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Update our runtime opacity value
|
|
_runtimeOpacity = newOpacity;
|
|
|
|
// Manually turn off acrylic if they turn off transparency.
|
|
_runtimeUseAcrylic = newOpacity < 1.0 && _settings->UseAcrylic();
|
|
|
|
// Update the renderer as well. It might need to fall back from
|
|
// cleartype -> grayscale if the BG is transparent / acrylic.
|
|
if (_renderEngine)
|
|
{
|
|
_renderEngine->EnableTransparentBackground(_isBackgroundTransparent());
|
|
_renderer->NotifyPaintFrame();
|
|
}
|
|
|
|
auto eventArgs = winrt::make_self<TransparencyChangedEventArgs>(newOpacity);
|
|
_TransparencyChangedHandlers(*this, *eventArgs);
|
|
}
|
|
|
|
void ControlCore::ToggleShaderEffects()
|
|
{
|
|
const auto path = _settings->PixelShaderPath();
|
|
auto lock = _terminal->LockForWriting();
|
|
// Originally, this action could be used to enable the retro effects
|
|
// even when they're set to `false` in the settings. If the user didn't
|
|
// specify a custom pixel shader, manually enable the legacy retro
|
|
// effect first. This will ensure that a toggle off->on will still work,
|
|
// even if they currently have retro effect off.
|
|
if (path.empty())
|
|
{
|
|
_renderEngine->SetRetroTerminalEffect(!_renderEngine->GetRetroTerminalEffect());
|
|
}
|
|
else
|
|
{
|
|
_renderEngine->SetPixelShaderPath(_renderEngine->GetPixelShaderPath().empty() ? std::wstring_view{ path } : std::wstring_view{});
|
|
}
|
|
// Always redraw after toggling effects. This way even if the control
|
|
// does not have focus it will update immediately.
|
|
_renderer->TriggerRedrawAll();
|
|
}
|
|
|
|
// Method description:
|
|
// - Updates last hovered cell, renders / removes rendering of hyper-link if required
|
|
// Arguments:
|
|
// - terminalPosition: The terminal position of the pointer
|
|
void ControlCore::SetHoveredCell(Core::Point pos)
|
|
{
|
|
_updateHoveredCell(std::optional<til::point>{ pos });
|
|
}
|
|
void ControlCore::ClearHoveredCell()
|
|
{
|
|
_updateHoveredCell(std::nullopt);
|
|
}
|
|
|
|
void ControlCore::_updateHoveredCell(const std::optional<til::point> terminalPosition)
|
|
{
|
|
if (terminalPosition == _lastHoveredCell)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// GH#9618 - lock while we're reading from the terminal, and if we need
|
|
// to update something, then lock again to write the terminal.
|
|
|
|
_lastHoveredCell = terminalPosition;
|
|
uint16_t newId{ 0u };
|
|
// we can't use auto here because we're pre-declaring newInterval.
|
|
decltype(_terminal->GetHyperlinkIntervalFromViewportPosition({})) newInterval{ std::nullopt };
|
|
if (terminalPosition.has_value())
|
|
{
|
|
auto lock = _terminal->LockForReading(); // Lock for the duration of our reads.
|
|
newId = _terminal->GetHyperlinkIdAtViewportPosition(*terminalPosition);
|
|
newInterval = _terminal->GetHyperlinkIntervalFromViewportPosition(*terminalPosition);
|
|
}
|
|
|
|
// If the hyperlink ID changed or the interval changed, trigger a redraw all
|
|
// (so this will happen both when we move onto a link and when we move off a link)
|
|
if (newId != _lastHoveredId ||
|
|
(newInterval != _lastHoveredInterval))
|
|
{
|
|
// Introduce scope for lock - we don't want to raise the
|
|
// HoveredHyperlinkChanged event under lock, because then handlers
|
|
// wouldn't be able to ask us about the hyperlink text/position
|
|
// without deadlocking us.
|
|
{
|
|
auto lock = _terminal->LockForWriting();
|
|
|
|
_lastHoveredId = newId;
|
|
_lastHoveredInterval = newInterval;
|
|
_renderer->UpdateHyperlinkHoveredId(newId);
|
|
_renderer->UpdateLastHoveredInterval(newInterval);
|
|
_renderer->TriggerRedrawAll();
|
|
}
|
|
|
|
_HoveredHyperlinkChangedHandlers(*this, nullptr);
|
|
}
|
|
}
|
|
|
|
winrt::hstring ControlCore::GetHyperlink(const Core::Point pos) const
|
|
{
|
|
// Lock for the duration of our reads.
|
|
auto lock = _terminal->LockForReading();
|
|
return winrt::hstring{ _terminal->GetHyperlinkAtViewportPosition(til::point{ pos }) };
|
|
}
|
|
|
|
winrt::hstring ControlCore::HoveredUriText() const
|
|
{
|
|
auto lock = _terminal->LockForReading(); // Lock for the duration of our reads.
|
|
if (_lastHoveredCell.has_value())
|
|
{
|
|
auto uri{ _terminal->GetHyperlinkAtViewportPosition(*_lastHoveredCell) };
|
|
uri.resize(std::min<size_t>(1024u, uri.size())); // Truncate for display
|
|
return winrt::hstring{ uri };
|
|
}
|
|
return {};
|
|
}
|
|
|
|
Windows::Foundation::IReference<Core::Point> ControlCore::HoveredCell() const
|
|
{
|
|
return _lastHoveredCell.has_value() ? Windows::Foundation::IReference<Core::Point>{ _lastHoveredCell.value().to_core_point() } : nullptr;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Updates the settings of the current terminal.
|
|
// - INVARIANT: This method can only be called if the caller DOES NOT HAVE writing lock on the terminal.
|
|
void ControlCore::UpdateSettings(const IControlSettings& settings, const IControlAppearance& newAppearance)
|
|
{
|
|
_settings = winrt::make_self<implementation::ControlSettings>(settings, newAppearance);
|
|
|
|
auto lock = _terminal->LockForWriting();
|
|
|
|
_cellWidth = CSSLengthPercentage::FromString(_settings->CellWidth().c_str());
|
|
_cellHeight = CSSLengthPercentage::FromString(_settings->CellHeight().c_str());
|
|
_runtimeOpacity = std::nullopt;
|
|
|
|
// Manually turn off acrylic if they turn off transparency.
|
|
_runtimeUseAcrylic = _settings->Opacity() < 1.0 && _settings->UseAcrylic();
|
|
|
|
const auto sizeChanged = _setFontSizeUnderLock(_settings->FontSize());
|
|
|
|
// Update the terminal core with its new Core settings
|
|
_terminal->UpdateSettings(*_settings);
|
|
|
|
if (!_initializedTerminal.load(std::memory_order_relaxed))
|
|
{
|
|
// If we haven't initialized, there's no point in continuing.
|
|
// Initialization will handle the renderer settings.
|
|
return;
|
|
}
|
|
|
|
_renderEngine->SetForceFullRepaintRendering(_settings->ForceFullRepaintRendering());
|
|
_renderEngine->SetSoftwareRendering(_settings->SoftwareRendering());
|
|
// Inform the renderer of our opacity
|
|
_renderEngine->EnableTransparentBackground(_isBackgroundTransparent());
|
|
|
|
// Trigger a redraw to repaint the window background and tab colors.
|
|
_renderer->TriggerRedrawAll(true, true);
|
|
|
|
_updateAntiAliasingMode();
|
|
|
|
if (sizeChanged)
|
|
{
|
|
_refreshSizeUnderLock();
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Updates the appearance of the current terminal.
|
|
// - INVARIANT: This method can only be called if the caller DOES NOT HAVE writing lock on the terminal.
|
|
void ControlCore::ApplyAppearance(const bool& focused)
|
|
{
|
|
auto lock = _terminal->LockForWriting();
|
|
const auto& newAppearance{ focused ? _settings->FocusedAppearance() : _settings->UnfocusedAppearance() };
|
|
// Update the terminal core with its new Core settings
|
|
_terminal->UpdateAppearance(*newAppearance);
|
|
|
|
// Update DxEngine settings under the lock
|
|
if (_renderEngine)
|
|
{
|
|
// Update DxEngine settings under the lock
|
|
_renderEngine->SetSelectionBackground(til::color{ newAppearance->SelectionBackground() });
|
|
_renderEngine->SetRetroTerminalEffect(newAppearance->RetroTerminalEffect());
|
|
_renderEngine->SetPixelShaderPath(newAppearance->PixelShaderPath());
|
|
_renderer->TriggerRedrawAll();
|
|
}
|
|
}
|
|
|
|
void ControlCore::_updateAntiAliasingMode()
|
|
{
|
|
D2D1_TEXT_ANTIALIAS_MODE mode;
|
|
// Update DxEngine's AntialiasingMode
|
|
switch (_settings->AntialiasingMode())
|
|
{
|
|
case TextAntialiasingMode::Cleartype:
|
|
mode = D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE;
|
|
break;
|
|
case TextAntialiasingMode::Aliased:
|
|
mode = D2D1_TEXT_ANTIALIAS_MODE_ALIASED;
|
|
break;
|
|
default:
|
|
mode = D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE;
|
|
break;
|
|
}
|
|
|
|
_renderEngine->SetAntialiasingMode(mode);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update the font with the renderer. This will be called either when the
|
|
// font changes or the DPI changes, as DPI changes will necessitate a
|
|
// font change. This method will *not* change the buffer/viewport size
|
|
// to account for the new glyph dimensions. Callers should make sure to
|
|
// appropriately call _doResizeUnderLock after this method is called.
|
|
// - The write lock should be held when calling this method.
|
|
// Arguments:
|
|
// - initialUpdate: whether this font update should be considered as being
|
|
// concerned with initialization process. Value forwarded to event handler.
|
|
void ControlCore::_updateFont(const bool initialUpdate)
|
|
{
|
|
const auto newDpi = static_cast<int>(lrint(_compositionScale * USER_DEFAULT_SCREEN_DPI));
|
|
|
|
_terminal->SetFontInfo(_actualFont);
|
|
|
|
if (_renderEngine)
|
|
{
|
|
std::unordered_map<std::wstring_view, uint32_t> featureMap;
|
|
if (const auto fontFeatures = _settings->FontFeatures())
|
|
{
|
|
featureMap.reserve(fontFeatures.Size());
|
|
|
|
for (const auto& [tag, param] : fontFeatures)
|
|
{
|
|
featureMap.emplace(tag, param);
|
|
}
|
|
}
|
|
std::unordered_map<std::wstring_view, float> axesMap;
|
|
if (const auto fontAxes = _settings->FontAxes())
|
|
{
|
|
axesMap.reserve(fontAxes.Size());
|
|
|
|
for (const auto& [axis, value] : fontAxes)
|
|
{
|
|
axesMap.emplace(axis, value);
|
|
}
|
|
}
|
|
|
|
// TODO: MSFT:20895307 If the font doesn't exist, this doesn't
|
|
// actually fail. We need a way to gracefully fallback.
|
|
LOG_IF_FAILED(_renderEngine->UpdateDpi(newDpi));
|
|
LOG_IF_FAILED(_renderEngine->UpdateFont(_desiredFont, _actualFont, featureMap, axesMap));
|
|
}
|
|
|
|
// If the actual font isn't what was requested...
|
|
if (_actualFont.GetFaceName() != _desiredFont.GetFaceName())
|
|
{
|
|
// Then warn the user that we picked something because we couldn't find their font.
|
|
// Format message with user's choice of font and the font that was chosen instead.
|
|
const winrt::hstring message{ fmt::format(std::wstring_view{ RS_(L"NoticeFontNotFound") },
|
|
_desiredFont.GetFaceName(),
|
|
_actualFont.GetFaceName()) };
|
|
auto noticeArgs = winrt::make<NoticeEventArgs>(NoticeLevel::Warning, message);
|
|
_RaiseNoticeHandlers(*this, std::move(noticeArgs));
|
|
}
|
|
|
|
const auto actualNewSize = _actualFont.GetSize();
|
|
_FontSizeChangedHandlers(actualNewSize.width, actualNewSize.height, initialUpdate);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Set the font size of the terminal control.
|
|
// Arguments:
|
|
// - fontSize: The size of the font.
|
|
// Return Value:
|
|
// - Returns true if you need to call _refreshSizeUnderLock().
|
|
bool ControlCore::_setFontSizeUnderLock(float fontSize)
|
|
{
|
|
// Make sure we have a non-zero font size
|
|
const auto newSize = std::max(fontSize, 1.0f);
|
|
const auto fontFace = _settings->FontFace();
|
|
const auto fontWeight = _settings->FontWeight();
|
|
_desiredFont = { fontFace, 0, fontWeight.Weight, newSize, CP_UTF8 };
|
|
_actualFont = { fontFace, 0, fontWeight.Weight, _desiredFont.GetEngineSize(), CP_UTF8, false };
|
|
_actualFontFaceName = { fontFace };
|
|
|
|
_desiredFont.SetCellSize(_cellWidth, _cellHeight);
|
|
|
|
const auto before = _actualFont.GetSize();
|
|
_updateFont();
|
|
const auto after = _actualFont.GetSize();
|
|
return before != after;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Reset the font size of the terminal to its default size.
|
|
// Arguments:
|
|
// - none
|
|
void ControlCore::ResetFontSize()
|
|
{
|
|
const auto lock = _terminal->LockForWriting();
|
|
|
|
if (_setFontSizeUnderLock(_settings->FontSize()))
|
|
{
|
|
_refreshSizeUnderLock();
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Adjust the font size of the terminal control.
|
|
// Arguments:
|
|
// - fontSizeDelta: The amount to increase or decrease the font size by.
|
|
void ControlCore::AdjustFontSize(float fontSizeDelta)
|
|
{
|
|
const auto lock = _terminal->LockForWriting();
|
|
|
|
if (_setFontSizeUnderLock(_desiredFont.GetFontSize() + fontSizeDelta))
|
|
{
|
|
_refreshSizeUnderLock();
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Process a resize event that was initiated by the user. This can either
|
|
// be due to the user resizing the window (causing the swapchain to
|
|
// resize) or due to the DPI changing (causing us to need to resize the
|
|
// buffer to match)
|
|
// - Note that a DPI change will also trigger a font size change, and will
|
|
// call into here.
|
|
// - The write lock should be held when calling this method, we might be
|
|
// changing the buffer size in _refreshSizeUnderLock.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void ControlCore::_refreshSizeUnderLock()
|
|
{
|
|
if (_IsClosing())
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto cx = gsl::narrow_cast<til::CoordType>(lrint(_panelWidth * _compositionScale));
|
|
auto cy = gsl::narrow_cast<til::CoordType>(lrint(_panelHeight * _compositionScale));
|
|
|
|
// Don't actually resize so small that a single character wouldn't fit
|
|
// in either dimension. The buffer really doesn't like being size 0.
|
|
cx = std::max(cx, _actualFont.GetSize().width);
|
|
cy = std::max(cy, _actualFont.GetSize().height);
|
|
|
|
// Convert our new dimensions to characters
|
|
const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, { cx, cy });
|
|
const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels);
|
|
|
|
_terminal->ClearSelection();
|
|
|
|
// Tell the dx engine that our window is now the new size.
|
|
THROW_IF_FAILED(_renderEngine->SetWindowSize({ cx, cy }));
|
|
|
|
// Invalidate everything
|
|
_renderer->TriggerRedrawAll();
|
|
|
|
// If this function succeeds with S_FALSE, then the terminal didn't
|
|
// actually change size. No need to notify the connection of this no-op.
|
|
const auto hr = _terminal->UserResize({ vp.Width(), vp.Height() });
|
|
if (SUCCEEDED(hr) && hr != S_FALSE)
|
|
{
|
|
_connection.Resize(vp.Height(), vp.Width());
|
|
}
|
|
}
|
|
|
|
void ControlCore::SizeChanged(const float width,
|
|
const float height)
|
|
{
|
|
SizeOrScaleChanged(width, height, _compositionScale);
|
|
}
|
|
|
|
void ControlCore::ScaleChanged(const float scale)
|
|
{
|
|
if (!_renderEngine)
|
|
{
|
|
return;
|
|
}
|
|
SizeOrScaleChanged(_panelWidth, _panelHeight, scale);
|
|
}
|
|
|
|
void ControlCore::SizeOrScaleChanged(const float width,
|
|
const float height,
|
|
const float scale)
|
|
{
|
|
const auto scaleChanged = _compositionScale != scale;
|
|
// _refreshSizeUnderLock redraws the entire terminal.
|
|
// Don't call it if we don't have to.
|
|
if (_panelWidth == width && _panelHeight == height && !scaleChanged)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_panelWidth = width;
|
|
_panelHeight = height;
|
|
_compositionScale = scale;
|
|
|
|
auto lock = _terminal->LockForWriting();
|
|
if (scaleChanged)
|
|
{
|
|
// _updateFont relies on the new _compositionScale set above
|
|
_updateFont();
|
|
}
|
|
_refreshSizeUnderLock();
|
|
}
|
|
|
|
void ControlCore::SetSelectionAnchor(const til::point position)
|
|
{
|
|
auto lock = _terminal->LockForWriting();
|
|
_terminal->SetSelectionAnchor(position);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Retrieves selection metadata from Terminal necessary to draw the
|
|
// selection markers.
|
|
// - Since all of this needs to be done under lock, it is more performant
|
|
// to throw it all in a struct and pass it along.
|
|
Control::SelectionData ControlCore::SelectionInfo() const
|
|
{
|
|
auto lock = _terminal->LockForReading();
|
|
Control::SelectionData info;
|
|
|
|
const auto start{ _terminal->SelectionStartForRendering() };
|
|
info.StartPos = { start.x, start.y };
|
|
|
|
const auto end{ _terminal->SelectionEndForRendering() };
|
|
info.EndPos = { end.x, end.y };
|
|
|
|
info.Endpoint = static_cast<SelectionEndpointTarget>(_terminal->SelectionEndpointTarget());
|
|
|
|
const auto bufferSize{ _terminal->GetTextBuffer().GetSize() };
|
|
info.StartAtLeftBoundary = _terminal->GetSelectionAnchor().x == bufferSize.Left();
|
|
info.EndAtRightBoundary = _terminal->GetSelectionEnd().x == bufferSize.RightInclusive();
|
|
return info;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Sets selection's end position to match supplied cursor position, e.g. while mouse dragging.
|
|
// Arguments:
|
|
// - position: the point in terminal coordinates (in cells, not pixels)
|
|
void ControlCore::SetEndSelectionPoint(const til::point position)
|
|
{
|
|
if (!_terminal->IsSelectionActive())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Have to take the lock because the renderer will not draw correctly if
|
|
// you move its endpoints while it is generating a frame.
|
|
auto lock = _terminal->LockForWriting();
|
|
|
|
til::point terminalPosition{
|
|
std::clamp(position.x, 0, _terminal->GetViewport().Width() - 1),
|
|
std::clamp(position.y, 0, _terminal->GetViewport().Height() - 1)
|
|
};
|
|
|
|
// save location (for rendering) + render
|
|
_terminal->SetSelectionEnd(terminalPosition);
|
|
_updateSelectionUI();
|
|
}
|
|
|
|
// Called when the Terminal wants to set something to the clipboard, i.e.
|
|
// when an OSC 52 is emitted.
|
|
void ControlCore::_terminalCopyToClipboard(std::wstring_view wstr)
|
|
{
|
|
_CopyToClipboardHandlers(*this, winrt::make<implementation::CopyToClipboardEventArgs>(winrt::hstring{ wstr }));
|
|
}
|
|
|
|
// 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
|
|
// - 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 ControlCore::CopySelectionToClipboard(bool singleLine,
|
|
const Windows::Foundation::IReference<CopyFormat>& formats)
|
|
{
|
|
// no selection --> nothing to copy
|
|
if (!_terminal->IsSelectionActive())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// extract text from buffer
|
|
// RetrieveSelectedTextFromBuffer will lock while it's reading
|
|
const auto bufferData = _terminal->RetrieveSelectedTextFromBuffer(singleLine);
|
|
|
|
// convert text: vector<string> --> string
|
|
std::wstring textData;
|
|
for (const auto& text : bufferData.text)
|
|
{
|
|
textData += text;
|
|
}
|
|
|
|
const auto bgColor = _terminal->GetAttributeColors({}).second;
|
|
|
|
// convert text to HTML format
|
|
// GH#5347 - Don't provide a title for the generated HTML, as many
|
|
// web applications will paste the title first, followed by the HTML
|
|
// content, which is unexpected.
|
|
const auto htmlData = formats == nullptr || WI_IsFlagSet(formats.Value(), CopyFormat::HTML) ?
|
|
TextBuffer::GenHTML(bufferData,
|
|
_actualFont.GetUnscaledSize().height,
|
|
_actualFont.GetFaceName(),
|
|
bgColor) :
|
|
"";
|
|
|
|
// convert to RTF format
|
|
const auto rtfData = formats == nullptr || WI_IsFlagSet(formats.Value(), CopyFormat::RTF) ?
|
|
TextBuffer::GenRTF(bufferData,
|
|
_actualFont.GetUnscaledSize().height,
|
|
_actualFont.GetFaceName(),
|
|
bgColor) :
|
|
"";
|
|
|
|
// send data up for clipboard
|
|
_CopyToClipboardHandlers(*this,
|
|
winrt::make<CopyToClipboardEventArgs>(winrt::hstring{ textData },
|
|
winrt::to_hstring(htmlData),
|
|
winrt::to_hstring(rtfData),
|
|
formats));
|
|
return true;
|
|
}
|
|
|
|
void ControlCore::SelectAll()
|
|
{
|
|
auto lock = _terminal->LockForWriting();
|
|
_terminal->SelectAll();
|
|
_updateSelectionUI();
|
|
}
|
|
|
|
void ControlCore::ClearSelection()
|
|
{
|
|
auto lock = _terminal->LockForWriting();
|
|
_terminal->ClearSelection();
|
|
_updateSelectionUI();
|
|
}
|
|
|
|
bool ControlCore::ToggleBlockSelection()
|
|
{
|
|
auto lock = _terminal->LockForWriting();
|
|
if (_terminal->IsSelectionActive())
|
|
{
|
|
_terminal->SetBlockSelection(!_terminal->IsBlockSelection());
|
|
_renderer->TriggerSelection();
|
|
// do not update the selection markers!
|
|
// if we were showing them, keep it that way.
|
|
// otherwise, continue to not show them
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ControlCore::ToggleMarkMode()
|
|
{
|
|
auto lock = _terminal->LockForWriting();
|
|
_terminal->ToggleMarkMode();
|
|
_updateSelectionUI();
|
|
}
|
|
|
|
Control::SelectionInteractionMode ControlCore::SelectionMode() const
|
|
{
|
|
return static_cast<Control::SelectionInteractionMode>(_terminal->SelectionMode());
|
|
}
|
|
|
|
bool ControlCore::SwitchSelectionEndpoint()
|
|
{
|
|
if (_terminal->IsSelectionActive())
|
|
{
|
|
_terminal->SwitchSelectionEndpoint();
|
|
_updateSelectionUI();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ControlCore::ExpandSelectionToWord()
|
|
{
|
|
if (_terminal->IsSelectionActive())
|
|
{
|
|
_terminal->ExpandSelectionToWord();
|
|
_updateSelectionUI();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Pre-process text pasted (presumably from the clipboard)
|
|
// before sending it over the terminal's connection.
|
|
void ControlCore::PasteText(const winrt::hstring& hstr)
|
|
{
|
|
_terminal->WritePastedText(hstr);
|
|
_terminal->ClearSelection();
|
|
_updateSelectionUI();
|
|
_terminal->TrySnapOnInput();
|
|
}
|
|
|
|
FontInfo ControlCore::GetFont() const
|
|
{
|
|
return _actualFont;
|
|
}
|
|
|
|
winrt::Windows::Foundation::Size ControlCore::FontSize() const noexcept
|
|
{
|
|
const auto fontSize = _actualFont.GetSize();
|
|
return {
|
|
::base::saturated_cast<float>(fontSize.width),
|
|
::base::saturated_cast<float>(fontSize.height)
|
|
};
|
|
}
|
|
winrt::hstring ControlCore::FontFaceName() const noexcept
|
|
{
|
|
// This getter used to return _actualFont.GetFaceName(), however GetFaceName() returns a STL
|
|
// string and we need to return a WinRT string. This would require an additional allocation.
|
|
// This method is called 10/s by TSFInputControl at the time of writing.
|
|
return _actualFontFaceName;
|
|
}
|
|
|
|
uint16_t ControlCore::FontWeight() const noexcept
|
|
{
|
|
return static_cast<uint16_t>(_actualFont.GetWeight());
|
|
}
|
|
|
|
winrt::Windows::Foundation::Size ControlCore::FontSizeInDips() const
|
|
{
|
|
const auto fontSize = _actualFont.GetSize();
|
|
const auto scale = 1.0f / _compositionScale;
|
|
return {
|
|
fontSize.width * scale,
|
|
fontSize.height * scale,
|
|
};
|
|
}
|
|
|
|
TerminalConnection::ConnectionState ControlCore::ConnectionState() const
|
|
{
|
|
return _connection ? _connection.State() : TerminalConnection::ConnectionState::Closed;
|
|
}
|
|
|
|
hstring ControlCore::Title()
|
|
{
|
|
return hstring{ _terminal->GetConsoleTitle() };
|
|
}
|
|
|
|
hstring ControlCore::WorkingDirectory() const
|
|
{
|
|
return hstring{ _terminal->GetWorkingDirectory() };
|
|
}
|
|
|
|
bool ControlCore::BracketedPasteEnabled() const noexcept
|
|
{
|
|
return _terminal->IsXtermBracketedPasteModeEnabled();
|
|
}
|
|
|
|
Windows::Foundation::IReference<winrt::Windows::UI::Color> ControlCore::TabColor() noexcept
|
|
{
|
|
auto coreColor = _terminal->GetTabColor();
|
|
return coreColor.has_value() ? Windows::Foundation::IReference<winrt::Windows::UI::Color>{ static_cast<winrt::Windows::UI::Color>(coreColor.value()) } :
|
|
nullptr;
|
|
}
|
|
|
|
til::color ControlCore::BackgroundColor() const
|
|
{
|
|
return _terminal->GetRenderSettings().GetColorAlias(ColorAlias::DefaultBackground);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Gets the internal taskbar state value
|
|
// Return Value:
|
|
// - The taskbar state of this control
|
|
const size_t ControlCore::TaskbarState() const noexcept
|
|
{
|
|
return _terminal->GetTaskbarState();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Gets the internal taskbar progress value
|
|
// Return Value:
|
|
// - The taskbar progress of this control
|
|
const size_t ControlCore::TaskbarProgress() const noexcept
|
|
{
|
|
return _terminal->GetTaskbarProgress();
|
|
}
|
|
|
|
int ControlCore::ScrollOffset()
|
|
{
|
|
return _terminal->GetScrollOffset();
|
|
}
|
|
|
|
// Function Description:
|
|
// - Gets the height of the terminal in lines of text. This is just the
|
|
// height of the viewport.
|
|
// Return Value:
|
|
// - The height of the terminal in lines of text
|
|
int ControlCore::ViewHeight() const
|
|
{
|
|
return _terminal->GetViewport().Height();
|
|
}
|
|
|
|
// Function Description:
|
|
// - Gets the height of the terminal in lines of text. This includes the
|
|
// history AND the viewport.
|
|
// Return Value:
|
|
// - The height of the terminal in lines of text
|
|
int ControlCore::BufferHeight() const
|
|
{
|
|
return _terminal->GetBufferHeight();
|
|
}
|
|
|
|
void ControlCore::_terminalWarningBell()
|
|
{
|
|
// Since this can only ever be triggered by output from the connection,
|
|
// then the Terminal already has the write lock when calling this
|
|
// callback.
|
|
_WarningBellHandlers(*this, nullptr);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Called for the Terminal's TitleChanged callback. This will re-raise
|
|
// a new winrt TypedEvent that can be listened to.
|
|
// - The listeners to this event will re-query the control for the current
|
|
// value of Title().
|
|
// Arguments:
|
|
// - wstr: the new title of this terminal.
|
|
// Return Value:
|
|
// - <none>
|
|
void ControlCore::_terminalTitleChanged(std::wstring_view wstr)
|
|
{
|
|
// Since this can only ever be triggered by output from the connection,
|
|
// then the Terminal already has the write lock when calling this
|
|
// callback.
|
|
_TitleChangedHandlers(*this, winrt::make<TitleChangedEventArgs>(winrt::hstring{ wstr }));
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update the position and size of the scrollbar to match the given
|
|
// viewport top, viewport height, and buffer size.
|
|
// Additionally fires a ScrollPositionChanged event for anyone who's
|
|
// registered an event handler for us.
|
|
// Arguments:
|
|
// - viewTop: the top of the visible viewport, in rows. 0 indicates the top
|
|
// of the buffer.
|
|
// - viewHeight: the height of the viewport in rows.
|
|
// - bufferSize: the length of the buffer, in rows
|
|
void ControlCore::_terminalScrollPositionChanged(const int viewTop,
|
|
const int viewHeight,
|
|
const int bufferSize)
|
|
{
|
|
if (!_initializedTerminal.load(std::memory_order_relaxed))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Clear the regex pattern tree so the renderer does not try to render them while scrolling
|
|
// We're **NOT** taking the lock here unlike _scrollbarChangeHandler because
|
|
// we are already under lock (since this usually happens as a result of writing).
|
|
// TODO GH#9617: refine locking around pattern tree
|
|
_terminal->ClearPatternTree();
|
|
|
|
// Start the throttled update of our scrollbar.
|
|
auto update{ winrt::make<ScrollPositionChangedArgs>(viewTop,
|
|
viewHeight,
|
|
bufferSize) };
|
|
|
|
if (_inUnitTests) [[unlikely]]
|
|
{
|
|
_ScrollPositionChangedHandlers(*this, update);
|
|
}
|
|
else
|
|
{
|
|
const auto shared = _shared.lock_shared();
|
|
if (shared->updateScrollBar)
|
|
{
|
|
shared->updateScrollBar->Run(update);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ControlCore::_terminalCursorPositionChanged()
|
|
{
|
|
// When the buffer's cursor moves, start the throttled func to
|
|
// eventually dispatch a CursorPositionChanged event.
|
|
const auto shared = _shared.lock_shared();
|
|
if (shared->tsfTryRedrawCanvas)
|
|
{
|
|
shared->tsfTryRedrawCanvas->Run();
|
|
}
|
|
}
|
|
|
|
void ControlCore::_terminalTaskbarProgressChanged()
|
|
{
|
|
_TaskbarProgressChangedHandlers(*this, nullptr);
|
|
}
|
|
|
|
void ControlCore::_terminalShowWindowChanged(bool showOrHide)
|
|
{
|
|
auto showWindow = winrt::make_self<implementation::ShowWindowArgs>(showOrHide);
|
|
_ShowWindowChangedHandlers(*this, *showWindow);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Plays a single MIDI note, blocking for the duration.
|
|
// Arguments:
|
|
// - noteNumber - The MIDI note number to be played (0 - 127).
|
|
// - velocity - The force with which the note should be played (0 - 127).
|
|
// - duration - How long the note should be sustained (in microseconds).
|
|
void ControlCore::_terminalPlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration)
|
|
{
|
|
// The UI thread might try to acquire the console lock from time to time.
|
|
// --> Unlock it, so the UI doesn't hang while we're busy.
|
|
const auto suspension = _terminal->SuspendLock();
|
|
// This call will block for the duration, unless shutdown early.
|
|
_midiAudio.PlayNote(reinterpret_cast<HWND>(_owningHwnd), noteNumber, velocity, std::chrono::duration_cast<std::chrono::milliseconds>(duration));
|
|
}
|
|
|
|
bool ControlCore::HasSelection() const
|
|
{
|
|
return _terminal->IsSelectionActive();
|
|
}
|
|
|
|
bool ControlCore::CopyOnSelect() const
|
|
{
|
|
return _settings->CopyOnSelect();
|
|
}
|
|
|
|
Windows::Foundation::Collections::IVector<winrt::hstring> ControlCore::SelectedText(bool trimTrailingWhitespace) const
|
|
{
|
|
// RetrieveSelectedTextFromBuffer will lock while it's reading
|
|
const auto internalResult{ _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace).text };
|
|
|
|
auto result = winrt::single_threaded_vector<winrt::hstring>();
|
|
|
|
for (const auto& row : internalResult)
|
|
{
|
|
result.Append(winrt::hstring{ row });
|
|
}
|
|
return result;
|
|
}
|
|
|
|
::Microsoft::Console::Render::IRenderData* ControlCore::GetRenderData() const
|
|
{
|
|
return _terminal.get();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Search text in text buffer. This is triggered if the user click
|
|
// search button or press enter.
|
|
// Arguments:
|
|
// - text: the text to search
|
|
// - goForward: boolean that represents if the current search direction is forward
|
|
// - caseSensitive: boolean that represents if the current search is case sensitive
|
|
// Return Value:
|
|
// - <none>
|
|
void ControlCore::Search(const winrt::hstring& text,
|
|
const bool goForward,
|
|
const bool caseSensitive)
|
|
{
|
|
if (text.size() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const auto direction = goForward ?
|
|
Search::Direction::Forward :
|
|
Search::Direction::Backward;
|
|
|
|
const auto sensitivity = caseSensitive ?
|
|
Search::Sensitivity::CaseSensitive :
|
|
Search::Sensitivity::CaseInsensitive;
|
|
|
|
::Search search(*GetRenderData(), text.c_str(), direction, sensitivity);
|
|
auto lock = _terminal->LockForWriting();
|
|
const auto foundMatch{ search.FindNext() };
|
|
if (foundMatch)
|
|
{
|
|
_terminal->SetBlockSelection(false);
|
|
search.Select();
|
|
|
|
// this is used for search,
|
|
// DO NOT call _updateSelectionUI() here.
|
|
// We don't want to show the markers so manually tell it to clear it.
|
|
_renderer->TriggerSelection();
|
|
_UpdateSelectionMarkersHandlers(*this, winrt::make<implementation::UpdateSelectionMarkersEventArgs>(true));
|
|
}
|
|
|
|
// Raise a FoundMatch event, which the control will use to notify
|
|
// narrator if there was any results in the buffer
|
|
auto foundResults = winrt::make_self<implementation::FoundResultsArgs>(foundMatch);
|
|
_FoundMatchHandlers(*this, *foundResults);
|
|
}
|
|
|
|
void ControlCore::Close()
|
|
{
|
|
if (!_IsClosing())
|
|
{
|
|
_closing = true;
|
|
|
|
// Ensure Close() doesn't hang, waiting for MidiAudio to finish playing an hour long song.
|
|
_midiAudio.BeginSkip();
|
|
|
|
// Stop accepting new output and state changes before we disconnect everything.
|
|
_connectionOutputEventRevoker.revoke();
|
|
_connectionStateChangedRevoker.revoke();
|
|
_connection.Close();
|
|
}
|
|
}
|
|
|
|
void ControlCore::_rendererWarning(const HRESULT hr)
|
|
{
|
|
_RendererWarningHandlers(*this, winrt::make<RendererWarningArgs>(hr));
|
|
}
|
|
|
|
winrt::fire_and_forget ControlCore::_renderEngineSwapChainChanged(const HANDLE sourceHandle)
|
|
{
|
|
// `sourceHandle` is a weak ref to a HANDLE that's ultimately owned by the
|
|
// render engine's own unique_handle. We'll add another ref to it here.
|
|
// This will make sure that we always have a valid HANDLE to give to
|
|
// callers of our own SwapChainHandle method, even if the renderer is
|
|
// currently in the process of discarding this value and creating a new
|
|
// one. Callers should have already set up the SwapChainChanged
|
|
// callback, so this all works out.
|
|
|
|
winrt::handle duplicatedHandle;
|
|
const auto processHandle = GetCurrentProcess();
|
|
THROW_IF_WIN32_BOOL_FALSE(DuplicateHandle(processHandle, sourceHandle, processHandle, duplicatedHandle.put(), 0, FALSE, DUPLICATE_SAME_ACCESS));
|
|
|
|
const auto weakThis{ get_weak() };
|
|
|
|
// Concurrent read of _dispatcher is safe, because Detach() calls WaitForPaintCompletionAndDisable()
|
|
// which blocks until this call returns. _dispatcher will only be changed afterwards.
|
|
co_await wil::resume_foreground(_dispatcher);
|
|
|
|
if (auto core{ weakThis.get() })
|
|
{
|
|
// `this` is safe to use now
|
|
|
|
_lastSwapChainHandle = std::move(duplicatedHandle);
|
|
// Now bubble the event up to the control.
|
|
_SwapChainChangedHandlers(*this, winrt::box_value<uint64_t>(reinterpret_cast<uint64_t>(_lastSwapChainHandle.get())));
|
|
}
|
|
}
|
|
|
|
void ControlCore::_rendererBackgroundColorChanged()
|
|
{
|
|
_BackgroundColorChangedHandlers(*this, nullptr);
|
|
}
|
|
|
|
void ControlCore::_rendererTabColorChanged()
|
|
{
|
|
_TabColorChangedHandlers(*this, nullptr);
|
|
}
|
|
|
|
void ControlCore::BlinkAttributeTick()
|
|
{
|
|
auto lock = _terminal->LockForWriting();
|
|
|
|
auto& renderSettings = _terminal->GetRenderSettings();
|
|
renderSettings.ToggleBlinkRendition(*_renderer);
|
|
}
|
|
|
|
void ControlCore::BlinkCursor()
|
|
{
|
|
if (!_terminal->IsCursorBlinkingAllowed() &&
|
|
_terminal->IsCursorVisible())
|
|
{
|
|
return;
|
|
}
|
|
// SetCursorOn will take the write lock for you.
|
|
_terminal->SetCursorOn(!_terminal->IsCursorOn());
|
|
}
|
|
|
|
bool ControlCore::CursorOn() const
|
|
{
|
|
return _terminal->IsCursorOn();
|
|
}
|
|
|
|
void ControlCore::CursorOn(const bool isCursorOn)
|
|
{
|
|
_terminal->SetCursorOn(isCursorOn);
|
|
}
|
|
|
|
void ControlCore::ResumeRendering()
|
|
{
|
|
_renderer->ResetErrorStateAndResume();
|
|
}
|
|
|
|
bool ControlCore::IsVtMouseModeEnabled() const
|
|
{
|
|
return _terminal != nullptr && _terminal->IsTrackingMouseInput();
|
|
}
|
|
bool ControlCore::ShouldSendAlternateScroll(const unsigned int uiButton,
|
|
const int32_t delta) const
|
|
{
|
|
return _terminal != nullptr && _terminal->ShouldSendAlternateScroll(uiButton, delta);
|
|
}
|
|
|
|
Core::Point ControlCore::CursorPosition() const
|
|
{
|
|
// If we haven't been initialized yet, then fake it.
|
|
if (!_initializedTerminal.load(std::memory_order_relaxed))
|
|
{
|
|
return { 0, 0 };
|
|
}
|
|
|
|
auto lock = _terminal->LockForReading();
|
|
return _terminal->GetViewportRelativeCursorPosition().to_core_point();
|
|
}
|
|
|
|
// This one's really pushing the boundary of what counts as "encapsulation".
|
|
// It really belongs in the "Interactivity" layer, which doesn't yet exist.
|
|
// There's so many accesses to the selection in the Core though, that I just
|
|
// put this here. The Control shouldn't be futzing that much with the
|
|
// selection itself.
|
|
void ControlCore::LeftClickOnTerminal(const til::point terminalPosition,
|
|
const int numberOfClicks,
|
|
const bool altEnabled,
|
|
const bool shiftEnabled,
|
|
const bool isOnOriginalPosition,
|
|
bool& selectionNeedsToBeCopied)
|
|
{
|
|
auto lock = _terminal->LockForWriting();
|
|
// handle ALT key
|
|
_terminal->SetBlockSelection(altEnabled);
|
|
|
|
auto mode = ::Terminal::SelectionExpansion::Char;
|
|
if (numberOfClicks == 1)
|
|
{
|
|
mode = ::Terminal::SelectionExpansion::Char;
|
|
}
|
|
else if (numberOfClicks == 2)
|
|
{
|
|
mode = ::Terminal::SelectionExpansion::Word;
|
|
}
|
|
else if (numberOfClicks == 3)
|
|
{
|
|
mode = ::Terminal::SelectionExpansion::Line;
|
|
}
|
|
|
|
// Update the selection appropriately
|
|
|
|
// We reset the active selection if one of the conditions apply:
|
|
// - shift is not held
|
|
// - GH#9384: the position is the same as of the first click starting
|
|
// the selection (we need to reset selection on double-click or
|
|
// triple-click, so it captures the word or the line, rather than
|
|
// extending the selection)
|
|
if (HasSelection() && (!shiftEnabled || isOnOriginalPosition))
|
|
{
|
|
// Reset the selection
|
|
_terminal->ClearSelection();
|
|
selectionNeedsToBeCopied = false; // there's no selection, so there's nothing to update
|
|
}
|
|
|
|
if (shiftEnabled && HasSelection())
|
|
{
|
|
// If shift is pressed and there is a selection we extend it using
|
|
// the selection mode (expand the "end" selection point)
|
|
_terminal->SetSelectionEnd(terminalPosition, mode);
|
|
selectionNeedsToBeCopied = true;
|
|
}
|
|
else if (mode != ::Terminal::SelectionExpansion::Char || shiftEnabled)
|
|
{
|
|
// If we are handling a double / triple-click or shift+single click
|
|
// we establish selection using the selected mode
|
|
// (expand both "start" and "end" selection points)
|
|
_terminal->MultiClickSelection(terminalPosition, mode);
|
|
selectionNeedsToBeCopied = true;
|
|
}
|
|
_updateSelectionUI();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Updates the renderer's representation of the selection as well as the selection marker overlay in TermControl
|
|
void ControlCore::_updateSelectionUI()
|
|
{
|
|
_renderer->TriggerSelection();
|
|
// only show the markers if we're doing a keyboard selection or in mark mode
|
|
const bool showMarkers{ _terminal->SelectionMode() >= ::Microsoft::Terminal::Core::Terminal::SelectionInteractionMode::Keyboard };
|
|
_UpdateSelectionMarkersHandlers(*this, winrt::make<implementation::UpdateSelectionMarkersEventArgs>(!showMarkers));
|
|
}
|
|
|
|
void ControlCore::AttachUiaEngine(::Microsoft::Console::Render::IRenderEngine* const pEngine)
|
|
{
|
|
// _renderer will always exist since it's introduced in the ctor
|
|
_renderer->AddRenderEngine(pEngine);
|
|
}
|
|
void ControlCore::DetachUiaEngine(::Microsoft::Console::Render::IRenderEngine* const pEngine)
|
|
{
|
|
_renderer->RemoveRenderEngine(pEngine);
|
|
}
|
|
|
|
bool ControlCore::IsInReadOnlyMode() const
|
|
{
|
|
return _isReadOnly;
|
|
}
|
|
|
|
void ControlCore::ToggleReadOnlyMode()
|
|
{
|
|
_isReadOnly = !_isReadOnly;
|
|
}
|
|
|
|
void ControlCore::SetReadOnlyMode(const bool readOnlyState)
|
|
{
|
|
_isReadOnly = readOnlyState;
|
|
}
|
|
|
|
void ControlCore::_raiseReadOnlyWarning()
|
|
{
|
|
auto noticeArgs = winrt::make<NoticeEventArgs>(NoticeLevel::Info, RS_(L"TermControlReadOnly"));
|
|
_RaiseNoticeHandlers(*this, std::move(noticeArgs));
|
|
}
|
|
void ControlCore::_connectionOutputHandler(const hstring& hstr)
|
|
{
|
|
try
|
|
{
|
|
_terminal->Write(hstr);
|
|
|
|
// Start the throttled update of where our hyperlinks are.
|
|
const auto shared = _shared.lock_shared();
|
|
if (shared->updatePatternLocations)
|
|
{
|
|
(*shared->updatePatternLocations)();
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
// We're expecting to receive an exception here if the terminal
|
|
// is closed while we're blocked playing a MIDI note.
|
|
}
|
|
}
|
|
|
|
uint64_t ControlCore::SwapChainHandle() const
|
|
{
|
|
// This is only ever called by TermControl::AttachContent, which occurs
|
|
// when we're taking an existing core and moving it to a new control.
|
|
// Otherwise, we only ever use the value from the SwapChainChanged
|
|
// event.
|
|
return reinterpret_cast<uint64_t>(_lastSwapChainHandle.get());
|
|
}
|
|
|
|
// Method Description:
|
|
// - Clear the contents of the buffer. The region cleared is given by
|
|
// clearType:
|
|
// * Screen: Clear only the contents of the visible viewport, leaving the
|
|
// cursor row at the top of the viewport.
|
|
// * Scrollback: Clear the contents of the scrollback.
|
|
// * All: Do both - clear the visible viewport and the scrollback, leaving
|
|
// only the cursor row at the top of the viewport.
|
|
// Arguments:
|
|
// - clearType: The type of clear to perform.
|
|
// Return Value:
|
|
// - <none>
|
|
void ControlCore::ClearBuffer(Control::ClearBufferType clearType)
|
|
{
|
|
if (clearType == Control::ClearBufferType::Scrollback || clearType == Control::ClearBufferType::All)
|
|
{
|
|
_terminal->EraseScrollback();
|
|
}
|
|
|
|
if (clearType == Control::ClearBufferType::Screen || clearType == Control::ClearBufferType::All)
|
|
{
|
|
// Send a signal to conpty to clear the buffer.
|
|
if (auto conpty{ _connection.try_as<TerminalConnection::ConptyConnection>() })
|
|
{
|
|
// ConPTY will emit sequences to sync up our buffer with its new
|
|
// contents.
|
|
conpty.ClearBuffer();
|
|
}
|
|
}
|
|
}
|
|
|
|
hstring ControlCore::ReadEntireBuffer() const
|
|
{
|
|
auto terminalLock = _terminal->LockForWriting();
|
|
|
|
const auto& textBuffer = _terminal->GetTextBuffer();
|
|
|
|
std::wstring str;
|
|
const auto lastRow = textBuffer.GetLastNonSpaceCharacter().y;
|
|
for (auto rowIndex = 0; rowIndex <= lastRow; rowIndex++)
|
|
{
|
|
const auto& row = textBuffer.GetRowByOffset(rowIndex);
|
|
const auto rowText = row.GetText();
|
|
const auto strEnd = rowText.find_last_not_of(UNICODE_SPACE);
|
|
if (strEnd != decltype(rowText)::npos)
|
|
{
|
|
str.append(rowText.substr(0, strEnd + 1));
|
|
}
|
|
|
|
if (!row.WasWrapForced())
|
|
{
|
|
str.append(L"\r\n");
|
|
}
|
|
}
|
|
|
|
return hstring{ str };
|
|
}
|
|
|
|
Core::Scheme ControlCore::ColorScheme() const noexcept
|
|
{
|
|
Core::Scheme s;
|
|
|
|
// This part is definitely a hack.
|
|
//
|
|
// This function is usually used by the "Preview Color Scheme"
|
|
// functionality in TerminalPage. If we've got an unfocused appearance,
|
|
// then we've applied that appearance before this is even getting called
|
|
// (because the command palette is open with focus on top of us). If we
|
|
// return the _current_ colors now, we'll return out the _unfocused_
|
|
// colors. If we do that, and the user dismisses the command palette,
|
|
// then the scheme that will get restored is the _unfocused_ one, which
|
|
// is not what we want.
|
|
//
|
|
// So if that's the case, then let's grab the colors from the focused
|
|
// appearance as the scheme instead. We'll lose any current runtime
|
|
// changes to the color table, but those were already blown away when we
|
|
// switched to an unfocused appearance.
|
|
//
|
|
// IF WE DON'T HAVE AN UNFOCUSED APPEARANCE: then just ask the Terminal
|
|
// for its current color table. That way, we can restore those colors
|
|
// back.
|
|
if (HasUnfocusedAppearance())
|
|
{
|
|
s.Foreground = _settings->FocusedAppearance()->DefaultForeground();
|
|
s.Background = _settings->FocusedAppearance()->DefaultBackground();
|
|
|
|
s.CursorColor = _settings->FocusedAppearance()->CursorColor();
|
|
|
|
s.Black = _settings->FocusedAppearance()->GetColorTableEntry(0);
|
|
s.Red = _settings->FocusedAppearance()->GetColorTableEntry(1);
|
|
s.Green = _settings->FocusedAppearance()->GetColorTableEntry(2);
|
|
s.Yellow = _settings->FocusedAppearance()->GetColorTableEntry(3);
|
|
s.Blue = _settings->FocusedAppearance()->GetColorTableEntry(4);
|
|
s.Purple = _settings->FocusedAppearance()->GetColorTableEntry(5);
|
|
s.Cyan = _settings->FocusedAppearance()->GetColorTableEntry(6);
|
|
s.White = _settings->FocusedAppearance()->GetColorTableEntry(7);
|
|
s.BrightBlack = _settings->FocusedAppearance()->GetColorTableEntry(8);
|
|
s.BrightRed = _settings->FocusedAppearance()->GetColorTableEntry(9);
|
|
s.BrightGreen = _settings->FocusedAppearance()->GetColorTableEntry(10);
|
|
s.BrightYellow = _settings->FocusedAppearance()->GetColorTableEntry(11);
|
|
s.BrightBlue = _settings->FocusedAppearance()->GetColorTableEntry(12);
|
|
s.BrightPurple = _settings->FocusedAppearance()->GetColorTableEntry(13);
|
|
s.BrightCyan = _settings->FocusedAppearance()->GetColorTableEntry(14);
|
|
s.BrightWhite = _settings->FocusedAppearance()->GetColorTableEntry(15);
|
|
}
|
|
else
|
|
{
|
|
s = _terminal->GetColorScheme();
|
|
}
|
|
|
|
// This might be a tad bit of a hack. This event only gets called by set
|
|
// color scheme / preview color scheme, and in that case, we know the
|
|
// control _is_ focused.
|
|
s.SelectionBackground = _settings->FocusedAppearance()->SelectionBackground();
|
|
|
|
return s;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Apply the given color scheme to this control. We'll take the colors out
|
|
// of it and apply them to our focused appearance, and update the terminal
|
|
// buffer with the new color table.
|
|
// - This is here to support the Set Color Scheme action, and the ability to
|
|
// preview schemes in the control.
|
|
// Arguments:
|
|
// - scheme: the collection of colors to apply.
|
|
// Return Value:
|
|
// - <none>
|
|
void ControlCore::ColorScheme(const Core::Scheme& scheme)
|
|
{
|
|
auto l{ _terminal->LockForWriting() };
|
|
|
|
_settings->FocusedAppearance()->DefaultForeground(scheme.Foreground);
|
|
_settings->FocusedAppearance()->DefaultBackground(scheme.Background);
|
|
_settings->FocusedAppearance()->CursorColor(scheme.CursorColor);
|
|
_settings->FocusedAppearance()->SelectionBackground(scheme.SelectionBackground);
|
|
|
|
_settings->FocusedAppearance()->SetColorTableEntry(0, scheme.Black);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(1, scheme.Red);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(2, scheme.Green);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(3, scheme.Yellow);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(4, scheme.Blue);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(5, scheme.Purple);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(6, scheme.Cyan);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(7, scheme.White);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(8, scheme.BrightBlack);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(9, scheme.BrightRed);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(10, scheme.BrightGreen);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(11, scheme.BrightYellow);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(12, scheme.BrightBlue);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(13, scheme.BrightPurple);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(14, scheme.BrightCyan);
|
|
_settings->FocusedAppearance()->SetColorTableEntry(15, scheme.BrightWhite);
|
|
|
|
_terminal->ApplyScheme(scheme);
|
|
|
|
_renderEngine->SetSelectionBackground(til::color{ _settings->SelectionBackground() });
|
|
|
|
_renderer->TriggerRedrawAll(true);
|
|
}
|
|
|
|
bool ControlCore::HasUnfocusedAppearance() const
|
|
{
|
|
return _settings->HasUnfocusedAppearance();
|
|
}
|
|
|
|
void ControlCore::AdjustOpacity(const double opacityAdjust, const bool relative)
|
|
{
|
|
if (relative)
|
|
{
|
|
AdjustOpacity(opacityAdjust);
|
|
}
|
|
else
|
|
{
|
|
_setOpacity(opacityAdjust);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Notifies the attached PTY that the window has changed visibility state
|
|
// - NOTE: Most VT commands are generated in `TerminalDispatch` and sent to this
|
|
// class as the target for transmission. But since this message isn't
|
|
// coming in via VT parsing (and rather from a window state transition)
|
|
// we generate and send it here.
|
|
// Arguments:
|
|
// - visible: True for visible; false for not visible.
|
|
// Return Value:
|
|
// - <none>
|
|
void ControlCore::WindowVisibilityChanged(const bool showOrHide)
|
|
{
|
|
if (_initializedTerminal.load(std::memory_order_relaxed))
|
|
{
|
|
// show is true, hide is false
|
|
if (auto conpty{ _connection.try_as<TerminalConnection::ConptyConnection>() })
|
|
{
|
|
conpty.ShowHide(showOrHide);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - When the control gains focus, it needs to tell ConPTY about this.
|
|
// Usually, these sequences are reserved for applications that
|
|
// specifically request SET_FOCUS_EVENT_MOUSE, ?1004h. ConPTY uses this
|
|
// sequence REGARDLESS to communicate if the control was focused or not.
|
|
// - Even if a client application disables this mode, the Terminal & conpty
|
|
// should always request this from the hosting terminal (and just ignore
|
|
// internally to ConPTY).
|
|
// - Full support for this sequence is tracked in GH#11682.
|
|
// - This is related to work done for GH#2988.
|
|
void ControlCore::GotFocus()
|
|
{
|
|
_focusChanged(true);
|
|
}
|
|
|
|
// See GotFocus.
|
|
void ControlCore::LostFocus()
|
|
{
|
|
_focusChanged(false);
|
|
}
|
|
|
|
void ControlCore::_focusChanged(bool focused)
|
|
{
|
|
// GH#13461 - temporarily turn off read-only mode, send the focus event,
|
|
// then turn it back on. Even in focus mode, focus events are fine to
|
|
// send. We don't want to pop a warning every time the control is
|
|
// focused.
|
|
const auto previous = std::exchange(_isReadOnly, false);
|
|
const auto restore = wil::scope_exit([&]() { _isReadOnly = previous; });
|
|
_terminal->FocusChanged(focused);
|
|
}
|
|
|
|
bool ControlCore::_isBackgroundTransparent()
|
|
{
|
|
// If we're:
|
|
// * Not fully opaque
|
|
// * rendering on top of an image
|
|
//
|
|
// then the renderer should not render "default background" text with a
|
|
// fully opaque background. Doing that would cover up our nice
|
|
// transparency, or our acrylic, or our image.
|
|
return Opacity() < 1.0f || !_settings->BackgroundImage().empty() || _settings->UseBackgroundImageForWindow();
|
|
}
|
|
|
|
uint64_t ControlCore::OwningHwnd()
|
|
{
|
|
return _owningHwnd;
|
|
}
|
|
|
|
void ControlCore::OwningHwnd(uint64_t owner)
|
|
{
|
|
if (owner != _owningHwnd && _connection)
|
|
{
|
|
if (auto conpty{ _connection.try_as<TerminalConnection::ConptyConnection>() })
|
|
{
|
|
conpty.ReparentWindow(owner);
|
|
}
|
|
}
|
|
_owningHwnd = owner;
|
|
}
|
|
|
|
Windows::Foundation::Collections::IVector<Control::ScrollMark> ControlCore::ScrollMarks() const
|
|
{
|
|
auto internalMarks{ _terminal->GetScrollMarks() };
|
|
auto v = winrt::single_threaded_observable_vector<Control::ScrollMark>();
|
|
for (const auto& mark : internalMarks)
|
|
{
|
|
Control::ScrollMark m{};
|
|
|
|
// sneaky: always evaluate the color of the mark to a real value
|
|
// before shoving it into the optional. If the mark doesn't have a
|
|
// specific color set, we'll use the value from the color table
|
|
// that's appropriate for this category of mark. If we do have a
|
|
// color set, then great we'll use that. The TermControl can then
|
|
// always use the value in the Mark regardless if it was actually
|
|
// set or not.
|
|
m.Color = OptionalFromColor(_terminal->GetColorForMark(mark));
|
|
m.Start = mark.start.to_core_point();
|
|
m.End = mark.end.to_core_point();
|
|
|
|
v.Append(m);
|
|
}
|
|
|
|
return v;
|
|
}
|
|
|
|
void ControlCore::AddMark(const Control::ScrollMark& mark)
|
|
{
|
|
::ScrollMark m{};
|
|
|
|
if (mark.Color.HasValue)
|
|
{
|
|
m.color = til::color{ mark.Color.Color };
|
|
}
|
|
|
|
if (HasSelection())
|
|
{
|
|
m.start = til::point{ _terminal->GetSelectionAnchor() };
|
|
m.end = til::point{ _terminal->GetSelectionEnd() };
|
|
}
|
|
else
|
|
{
|
|
m.start = m.end = til::point{ _terminal->GetTextBuffer().GetCursor().GetPosition() };
|
|
}
|
|
|
|
// The version of this that only accepts a ScrollMark will automatically
|
|
// set the start & end to the cursor position.
|
|
_terminal->AddMark(m, m.start, m.end, true);
|
|
}
|
|
void ControlCore::ClearMark() { _terminal->ClearMark(); }
|
|
void ControlCore::ClearAllMarks() { _terminal->ClearAllMarks(); }
|
|
|
|
void ControlCore::ScrollToMark(const Control::ScrollToMarkDirection& direction)
|
|
{
|
|
const auto currentOffset = ScrollOffset();
|
|
const auto& marks{ _terminal->GetScrollMarks() };
|
|
|
|
std::optional<::ScrollMark> tgt;
|
|
|
|
switch (direction)
|
|
{
|
|
case ScrollToMarkDirection::Last:
|
|
{
|
|
int highest = currentOffset;
|
|
for (const auto& mark : marks)
|
|
{
|
|
const auto newY = mark.start.y;
|
|
if (newY > highest)
|
|
{
|
|
tgt = mark;
|
|
highest = newY;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ScrollToMarkDirection::First:
|
|
{
|
|
int lowest = currentOffset;
|
|
for (const auto& mark : marks)
|
|
{
|
|
const auto newY = mark.start.y;
|
|
if (newY < lowest)
|
|
{
|
|
tgt = mark;
|
|
lowest = newY;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ScrollToMarkDirection::Next:
|
|
{
|
|
int minDistance = INT_MAX;
|
|
for (const auto& mark : marks)
|
|
{
|
|
const auto delta = mark.start.y - currentOffset;
|
|
if (delta > 0 && delta < minDistance)
|
|
{
|
|
tgt = mark;
|
|
minDistance = delta;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ScrollToMarkDirection::Previous:
|
|
default:
|
|
{
|
|
int minDistance = INT_MAX;
|
|
for (const auto& mark : marks)
|
|
{
|
|
const auto delta = currentOffset - mark.start.y;
|
|
if (delta > 0 && delta < minDistance)
|
|
{
|
|
tgt = mark;
|
|
minDistance = delta;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
const auto viewHeight = ViewHeight();
|
|
const auto bufferSize = BufferHeight();
|
|
|
|
// UserScrollViewport, to update the Terminal about where the viewport should be
|
|
// then raise a _terminalScrollPositionChanged to inform the control to update the scrollbar.
|
|
if (tgt.has_value())
|
|
{
|
|
UserScrollViewport(tgt->start.y);
|
|
_terminalScrollPositionChanged(tgt->start.y, viewHeight, bufferSize);
|
|
}
|
|
else
|
|
{
|
|
if (direction == ScrollToMarkDirection::Last || direction == ScrollToMarkDirection::Next)
|
|
{
|
|
UserScrollViewport(BufferHeight());
|
|
_terminalScrollPositionChanged(BufferHeight(), viewHeight, bufferSize);
|
|
}
|
|
else if (direction == ScrollToMarkDirection::First || direction == ScrollToMarkDirection::Previous)
|
|
{
|
|
UserScrollViewport(0);
|
|
_terminalScrollPositionChanged(0, viewHeight, bufferSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ControlCore::_selectSpan(til::point_span s)
|
|
{
|
|
const auto bufferSize{ _terminal->GetTextBuffer().GetSize() };
|
|
bufferSize.DecrementInBounds(s.end);
|
|
|
|
auto lock = _terminal->LockForWriting();
|
|
_terminal->SelectNewRegion(s.start, s.end);
|
|
_renderer->TriggerSelection();
|
|
}
|
|
|
|
void ControlCore::SelectCommand(const bool goUp)
|
|
{
|
|
const til::point start = HasSelection() ? (goUp ? _terminal->GetSelectionAnchor() : _terminal->GetSelectionEnd()) :
|
|
_terminal->GetTextBuffer().GetCursor().GetPosition();
|
|
|
|
std::optional<::ScrollMark> nearest{ std::nullopt };
|
|
const auto& marks{ _terminal->GetScrollMarks() };
|
|
|
|
// Early return so we don't have to check for the validity of `nearest` below after the loop exits.
|
|
if (marks.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
static constexpr til::point worst{ til::CoordTypeMax, til::CoordTypeMax };
|
|
til::point bestDistance{ worst };
|
|
|
|
for (const auto& m : marks)
|
|
{
|
|
if (!m.HasCommand())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const auto distance = goUp ? start - m.end : m.end - start;
|
|
if ((distance > til::point{ 0, 0 }) && distance < bestDistance)
|
|
{
|
|
nearest = m;
|
|
bestDistance = distance;
|
|
}
|
|
}
|
|
|
|
if (nearest.has_value())
|
|
{
|
|
const auto start = nearest->end;
|
|
auto end = *nearest->commandEnd;
|
|
_selectSpan(til::point_span{ start, end });
|
|
}
|
|
}
|
|
|
|
void ControlCore::SelectOutput(const bool goUp)
|
|
{
|
|
const til::point start = HasSelection() ? (goUp ? _terminal->GetSelectionAnchor() : _terminal->GetSelectionEnd()) :
|
|
_terminal->GetTextBuffer().GetCursor().GetPosition();
|
|
|
|
std::optional<::ScrollMark> nearest{ std::nullopt };
|
|
const auto& marks{ _terminal->GetScrollMarks() };
|
|
|
|
static constexpr til::point worst{ til::CoordTypeMax, til::CoordTypeMax };
|
|
til::point bestDistance{ worst };
|
|
|
|
for (const auto& m : marks)
|
|
{
|
|
if (!m.HasOutput())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const auto distance = goUp ? start - *m.commandEnd : *m.commandEnd - start;
|
|
if ((distance > til::point{ 0, 0 }) && distance < bestDistance)
|
|
{
|
|
nearest = m;
|
|
bestDistance = distance;
|
|
}
|
|
}
|
|
|
|
if (nearest.has_value())
|
|
{
|
|
const auto start = *nearest->commandEnd;
|
|
auto end = *nearest->outputEnd;
|
|
_selectSpan(til::point_span{ start, end });
|
|
}
|
|
}
|
|
|
|
void ControlCore::ColorSelection(const Control::SelectionColor& fg, const Control::SelectionColor& bg, Core::MatchMode matchMode)
|
|
{
|
|
if (HasSelection())
|
|
{
|
|
const auto pForeground = winrt::get_self<implementation::SelectionColor>(fg);
|
|
const auto pBackground = winrt::get_self<implementation::SelectionColor>(bg);
|
|
|
|
TextColor foregroundAsTextColor;
|
|
TextColor backgroundAsTextColor;
|
|
|
|
if (pForeground)
|
|
{
|
|
foregroundAsTextColor = pForeground->AsTextColor();
|
|
}
|
|
|
|
if (pBackground)
|
|
{
|
|
backgroundAsTextColor = pBackground->AsTextColor();
|
|
}
|
|
|
|
TextAttribute attr;
|
|
attr.SetForeground(foregroundAsTextColor);
|
|
attr.SetBackground(backgroundAsTextColor);
|
|
|
|
_terminal->ColorSelection(attr, matchMode);
|
|
_terminal->ClearSelection();
|
|
if (matchMode != Core::MatchMode::None)
|
|
{
|
|
// ClearSelection will invalidate the selection area... but if we are
|
|
// coloring other matches, then we need to make sure those get redrawn,
|
|
// too.
|
|
_renderer->TriggerRedrawAll();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ControlCore::AnchorContextMenu(const til::point viewportRelativeCharacterPosition)
|
|
{
|
|
// viewportRelativeCharacterPosition is relative to the current
|
|
// viewport, so adjust for that:
|
|
_contextMenuBufferPosition = _terminal->GetViewport().Origin() + viewportRelativeCharacterPosition;
|
|
}
|
|
|
|
void ControlCore::_contextMenuSelectMark(
|
|
const til::point& pos,
|
|
bool (*filter)(const ::ScrollMark&),
|
|
til::point_span (*getSpan)(const ::ScrollMark&))
|
|
{
|
|
// Do nothing if the caller didn't give us a way to get the span to select for this mark.
|
|
if (!getSpan)
|
|
{
|
|
return;
|
|
}
|
|
const auto& marks{ _terminal->GetScrollMarks() };
|
|
for (auto&& m : marks)
|
|
{
|
|
// If the caller gave us a way to filter marks, check that now.
|
|
// This can be used to filter to only marks that have a command, or output.
|
|
if (filter && filter(m))
|
|
{
|
|
continue;
|
|
}
|
|
// If they clicked _anywhere_ in the mark...
|
|
const auto [markStart, markEnd] = m.GetExtent();
|
|
if (markStart <= pos &&
|
|
markEnd >= pos)
|
|
{
|
|
// ... select the part of the mark the caller told us about.
|
|
_selectSpan(getSpan(m));
|
|
// And quick bail
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ControlCore::ContextMenuSelectCommand()
|
|
{
|
|
_contextMenuSelectMark(
|
|
_contextMenuBufferPosition,
|
|
[](const ::ScrollMark& m) -> bool { return !m.HasCommand(); },
|
|
[](const ::ScrollMark& m) { return til::point_span{ m.end, *m.commandEnd }; });
|
|
}
|
|
void ControlCore::ContextMenuSelectOutput()
|
|
{
|
|
_contextMenuSelectMark(
|
|
_contextMenuBufferPosition,
|
|
[](const ::ScrollMark& m) -> bool { return !m.HasOutput(); },
|
|
[](const ::ScrollMark& m) { return til::point_span{ *m.commandEnd, *m.outputEnd }; });
|
|
}
|
|
|
|
bool ControlCore::_clickedOnMark(
|
|
const til::point& pos,
|
|
bool (*filter)(const ::ScrollMark&))
|
|
{
|
|
// Don't show this if the click was on the selection
|
|
if (_terminal->IsSelectionActive() &&
|
|
_terminal->GetSelectionAnchor() <= pos &&
|
|
_terminal->GetSelectionEnd() >= pos)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// DO show this if the click was on a mark with a command
|
|
const auto& marks{ _terminal->GetScrollMarks() };
|
|
for (auto&& m : marks)
|
|
{
|
|
if (filter && filter(m))
|
|
{
|
|
continue;
|
|
}
|
|
const auto [start, end] = m.GetExtent();
|
|
if (start <= pos &&
|
|
end >= pos)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Didn't click on a mark with a command - don't show.
|
|
return false;
|
|
}
|
|
|
|
// Method Description:
|
|
// * Don't show this if the click was on the _current_ selection
|
|
// * Don't show this if the click wasn't on a mark with at least a command
|
|
// * Otherwise yea, show it.
|
|
bool ControlCore::ShouldShowSelectCommand()
|
|
{
|
|
// Relies on the anchor set in AnchorContextMenu
|
|
return _clickedOnMark(_contextMenuBufferPosition,
|
|
[](const ::ScrollMark& m) -> bool { return !m.HasCommand(); });
|
|
}
|
|
|
|
// Method Description:
|
|
// * Same as ShouldShowSelectCommand, but with the mark needing output
|
|
bool ControlCore::ShouldShowSelectOutput()
|
|
{
|
|
// Relies on the anchor set in AnchorContextMenu
|
|
return _clickedOnMark(_contextMenuBufferPosition,
|
|
[](const ::ScrollMark& m) -> bool { return !m.HasOutput(); });
|
|
}
|
|
}
|