mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-12 00:07:24 -06:00
This adds an indirection for `_KeyHandler` so that `OnDirectKeyEvent` can call `_KeyHandler`. This allows us to consistently handle Alt-key-up events. Then I added custom handling for Alt+ddd (OEM), Alt+0ddd (ANSI), and Alt+'+'+xxxx (Unicode) sequences, due to the absence of Alt-key events with xaml islands and our TSF control. Closes #17327 ## Validation Steps Performed * Tested it according to https://conemu.github.io/en/AltNumpad.html * Unbind Alt+Space * Run `showkey -a` * Alt+Space generates `^[ ` * F7 generates `^[[18~` (cherry picked from commit 2fab9866b2bf09229886184052cd623b6581eaef) Service-Card-Id: PVTI_lADOAF3p4s4AmhmszgSCpCg Service-Version: 1.21
3967 lines
165 KiB
C++
3967 lines
165 KiB
C++
// Copyright (c) Microsoft Corporation.
|
||
// Licensed under the MIT license.
|
||
|
||
#include "pch.h"
|
||
#include "TermControl.h"
|
||
|
||
#include <LibraryResources.h>
|
||
|
||
#include "TermControlAutomationPeer.h"
|
||
#include "../../renderer/atlas/AtlasEngine.h"
|
||
#include "../../tsf/Handle.h"
|
||
|
||
#include "TermControl.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::UI::Xaml;
|
||
using namespace winrt::Windows::UI::Xaml::Input;
|
||
using namespace winrt::Windows::UI::Xaml::Automation::Peers;
|
||
using namespace winrt::Windows::UI::Core;
|
||
using namespace winrt::Windows::UI::ViewManagement;
|
||
using namespace winrt::Windows::UI::Input;
|
||
using namespace winrt::Windows::System;
|
||
using namespace winrt::Windows::ApplicationModel::DataTransfer;
|
||
using namespace winrt::Windows::Storage::Streams;
|
||
|
||
// 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.
|
||
// This is already throttled primarily in the ControlCore, with a timeout of 100ms. We're adding another smaller one here, as the (potentially x-proc) call will come in off the UI thread
|
||
constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(8);
|
||
|
||
// The minimum delay between updating the locations of regex patterns
|
||
constexpr const auto UpdatePatternLocationsInterval = std::chrono::milliseconds(500);
|
||
|
||
// The minimum delay between emitting warning bells
|
||
constexpr const auto TerminalWarningBellInterval = std::chrono::milliseconds(1000);
|
||
|
||
DEFINE_ENUM_FLAG_OPERATORS(winrt::Microsoft::Terminal::Control::CopyFormat);
|
||
DEFINE_ENUM_FLAG_OPERATORS(winrt::Microsoft::Terminal::Control::MouseButtonState);
|
||
|
||
static Microsoft::Console::TSF::Handle& GetTSFHandle()
|
||
{
|
||
// https://en.cppreference.com/w/cpp/language/storage_duration
|
||
// > Variables declared at block scope with the specifier static or thread_local
|
||
// > [...] are initialized the first time control passes through their declaration
|
||
// --> Lazy, per-(window-)thread initialization of the TSF handle
|
||
thread_local auto s_tsf = ::Microsoft::Console::TSF::Handle::Create();
|
||
return s_tsf;
|
||
}
|
||
|
||
namespace winrt::Microsoft::Terminal::Control::implementation
|
||
{
|
||
TsfDataProvider::TsfDataProvider(TermControl* termControl) noexcept :
|
||
_termControl{ termControl }
|
||
{
|
||
}
|
||
|
||
STDMETHODIMP TsfDataProvider::QueryInterface(REFIID, void**) noexcept
|
||
{
|
||
return E_NOTIMPL;
|
||
}
|
||
|
||
ULONG STDMETHODCALLTYPE TsfDataProvider::AddRef() noexcept
|
||
{
|
||
return 1;
|
||
}
|
||
|
||
ULONG STDMETHODCALLTYPE TsfDataProvider::Release() noexcept
|
||
{
|
||
return 1;
|
||
}
|
||
|
||
HWND TsfDataProvider::GetHwnd()
|
||
{
|
||
if (!_hwnd)
|
||
{
|
||
// WinUI's WinRT based TSF runs in its own window "Windows.UI.Input.InputSite.WindowClass" (..."great")
|
||
// and in order for us to snatch the focus away from that one we need to find its HWND.
|
||
// The way we do it here is by finding the existing, active TSF context and getting the HWND from it.
|
||
_hwnd = GetTSFHandle().FindWindowOfActiveTSF();
|
||
if (!_hwnd)
|
||
{
|
||
_hwnd = reinterpret_cast<HWND>(_termControl->OwningHwnd());
|
||
}
|
||
}
|
||
return _hwnd;
|
||
}
|
||
|
||
RECT TsfDataProvider::GetViewport()
|
||
{
|
||
const auto scaleFactor = static_cast<float>(DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel());
|
||
const auto globalOrigin = CoreWindow::GetForCurrentThread().Bounds();
|
||
const auto localOrigin = _termControl->TransformToVisual(nullptr).TransformPoint({});
|
||
const auto size = _termControl->ActualSize();
|
||
|
||
const auto left = globalOrigin.X + localOrigin.X;
|
||
const auto top = globalOrigin.Y + localOrigin.Y;
|
||
const auto right = left + size.x;
|
||
const auto bottom = top + size.y;
|
||
|
||
return {
|
||
lroundf(left * scaleFactor),
|
||
lroundf(top * scaleFactor),
|
||
lroundf(right * scaleFactor),
|
||
lroundf(bottom * scaleFactor),
|
||
};
|
||
}
|
||
|
||
RECT TsfDataProvider::GetCursorPosition()
|
||
{
|
||
const auto core = _getCore();
|
||
if (!core)
|
||
{
|
||
return {};
|
||
}
|
||
|
||
const auto scaleFactor = static_cast<float>(DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel());
|
||
const auto globalOrigin = CoreWindow::GetForCurrentThread().Bounds();
|
||
const auto localOrigin = _termControl->TransformToVisual(nullptr).TransformPoint({});
|
||
const auto padding = _termControl->GetPadding();
|
||
const auto cursorPosition = core->CursorPosition();
|
||
const auto fontSize = core->FontSize();
|
||
|
||
// fontSize is not in DIPs, so we need to first multiply by scaleFactor and then do the rest.
|
||
const auto left = (globalOrigin.X + localOrigin.X + static_cast<float>(padding.Left)) * scaleFactor + cursorPosition.X * fontSize.Width;
|
||
const auto top = (globalOrigin.Y + localOrigin.Y + static_cast<float>(padding.Top)) * scaleFactor + cursorPosition.Y * fontSize.Height;
|
||
const auto right = left + fontSize.Width;
|
||
const auto bottom = top + fontSize.Height;
|
||
|
||
return {
|
||
lroundf(left),
|
||
lroundf(top),
|
||
lroundf(right),
|
||
lroundf(bottom),
|
||
};
|
||
}
|
||
|
||
void TsfDataProvider::HandleOutput(std::wstring_view text)
|
||
{
|
||
const auto core = _getCore();
|
||
if (!core)
|
||
{
|
||
return;
|
||
}
|
||
core->SendInput(text);
|
||
}
|
||
|
||
::Microsoft::Console::Render::Renderer* TsfDataProvider::GetRenderer()
|
||
{
|
||
const auto core = _getCore();
|
||
if (!core)
|
||
{
|
||
return nullptr;
|
||
}
|
||
return core->GetRenderer();
|
||
}
|
||
|
||
ControlCore* TsfDataProvider::_getCore() const noexcept
|
||
{
|
||
return get_self<ControlCore>(_termControl->_core);
|
||
}
|
||
|
||
TermControl::TermControl(IControlSettings settings,
|
||
Control::IControlAppearance unfocusedAppearance,
|
||
TerminalConnection::ITerminalConnection connection) :
|
||
TermControl{ winrt::make<implementation::ControlInteractivity>(settings, unfocusedAppearance, connection) }
|
||
{
|
||
}
|
||
|
||
TermControl::TermControl(Control::ControlInteractivity content) :
|
||
_interactivity{ content },
|
||
_isInternalScrollBarUpdate{ false },
|
||
_autoScrollVelocity{ 0 },
|
||
_autoScrollingPointerPoint{ std::nullopt },
|
||
_lastAutoScrollUpdateTime{ std::nullopt },
|
||
_searchBox{ nullptr }
|
||
{
|
||
InitializeComponent();
|
||
|
||
_core = _interactivity.Core();
|
||
|
||
// This event is specifically triggered by the renderer thread, a BG thread. Use a weak ref here.
|
||
_revokers.RendererEnteredErrorState = _core.RendererEnteredErrorState(winrt::auto_revoke, { get_weak(), &TermControl::_RendererEnteredErrorState });
|
||
|
||
// IMPORTANT! Set this callback up sooner rather 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!
|
||
_revokers.RendererWarning = _core.RendererWarning(winrt::auto_revoke, { get_weak(), &TermControl::_RendererWarning });
|
||
// ALSO IMPORTANT: Make sure to set this callback up in the ctor, so
|
||
// that we won't miss any swap chain changes.
|
||
_revokers.SwapChainChanged = _core.SwapChainChanged(winrt::auto_revoke, { get_weak(), &TermControl::RenderEngineSwapChainChanged });
|
||
|
||
// These callbacks can only really be triggered by UI interactions. So
|
||
// they don't need weak refs - they can't be triggered unless we're
|
||
// alive.
|
||
_revokers.BackgroundColorChanged = _core.BackgroundColorChanged(winrt::auto_revoke, { get_weak(), &TermControl::_coreBackgroundColorChanged });
|
||
_revokers.FontSizeChanged = _core.FontSizeChanged(winrt::auto_revoke, { get_weak(), &TermControl::_coreFontSizeChanged });
|
||
_revokers.TransparencyChanged = _core.TransparencyChanged(winrt::auto_revoke, { get_weak(), &TermControl::_coreTransparencyChanged });
|
||
_revokers.RaiseNotice = _core.RaiseNotice(winrt::auto_revoke, { get_weak(), &TermControl::_coreRaisedNotice });
|
||
_revokers.HoveredHyperlinkChanged = _core.HoveredHyperlinkChanged(winrt::auto_revoke, { get_weak(), &TermControl::_hoveredHyperlinkChanged });
|
||
_revokers.OutputIdle = _core.OutputIdle(winrt::auto_revoke, { get_weak(), &TermControl::_coreOutputIdle });
|
||
_revokers.UpdateSelectionMarkers = _core.UpdateSelectionMarkers(winrt::auto_revoke, { get_weak(), &TermControl::_updateSelectionMarkers });
|
||
_revokers.coreOpenHyperlink = _core.OpenHyperlink(winrt::auto_revoke, { get_weak(), &TermControl::_HyperlinkHandler });
|
||
_revokers.interactivityOpenHyperlink = _interactivity.OpenHyperlink(winrt::auto_revoke, { get_weak(), &TermControl::_HyperlinkHandler });
|
||
_revokers.interactivityScrollPositionChanged = _interactivity.ScrollPositionChanged(winrt::auto_revoke, { get_weak(), &TermControl::_ScrollPositionChanged });
|
||
_revokers.ContextMenuRequested = _interactivity.ContextMenuRequested(winrt::auto_revoke, { get_weak(), &TermControl::_contextMenuHandler });
|
||
|
||
// "Bubbled" events - ones we want to handle, by raising our own event.
|
||
_revokers.TitleChanged = _core.TitleChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleTitleChanged });
|
||
_revokers.TabColorChanged = _core.TabColorChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleTabColorChanged });
|
||
_revokers.TaskbarProgressChanged = _core.TaskbarProgressChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleSetTaskbarProgress });
|
||
_revokers.ConnectionStateChanged = _core.ConnectionStateChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleConnectionStateChanged });
|
||
_revokers.ShowWindowChanged = _core.ShowWindowChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleShowWindowChanged });
|
||
_revokers.CloseTerminalRequested = _core.CloseTerminalRequested(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleCloseTerminalRequested });
|
||
_revokers.CompletionsChanged = _core.CompletionsChanged(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleCompletionsChanged });
|
||
_revokers.RestartTerminalRequested = _core.RestartTerminalRequested(winrt::auto_revoke, { get_weak(), &TermControl::_bubbleRestartTerminalRequested });
|
||
|
||
_revokers.PasteFromClipboard = _interactivity.PasteFromClipboard(winrt::auto_revoke, { get_weak(), &TermControl::_bubblePasteFromClipboard });
|
||
|
||
// Initialize the terminal only once the swapchainpanel is loaded - that
|
||
// way, we'll be able to query the real pixel size it got on layout
|
||
_layoutUpdatedRevoker = SwapChainPanel().LayoutUpdated(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) {
|
||
// This event fires every time the layout changes, but it is always the last one to fire
|
||
// in any layout change chain. That gives us great flexibility in finding the right point
|
||
// at which to initialize our renderer (and our terminal).
|
||
// Any earlier than the last layout update and we may not know the terminal's starting size.
|
||
if (_InitializeTerminal(InitializeReason::Create))
|
||
{
|
||
// Only let this succeed once.
|
||
_layoutUpdatedRevoker.revoke();
|
||
}
|
||
});
|
||
|
||
// Get our dispatcher. This will get us the same dispatcher as
|
||
// TermControl::Dispatcher().
|
||
auto dispatcher = winrt::Windows::System::DispatcherQueue::GetForCurrentThread();
|
||
|
||
// These three throttled functions are triggered by terminal output and interact with the UI.
|
||
// Since Close() is the point after which we are removed from the UI, but before the
|
||
// destructor has run, we MUST check control->_IsClosing() before actually doing anything.
|
||
_playWarningBell = std::make_shared<ThrottledFuncLeading>(
|
||
dispatcher,
|
||
TerminalWarningBellInterval,
|
||
[weakThis = get_weak()]() {
|
||
if (auto control{ weakThis.get() }; control && !control->_IsClosing())
|
||
{
|
||
control->WarningBell.raise(*control, nullptr);
|
||
}
|
||
});
|
||
|
||
_updateScrollBar = std::make_shared<ThrottledFuncTrailing<ScrollBarUpdate>>(
|
||
dispatcher,
|
||
ScrollBarUpdateInterval,
|
||
[weakThis = get_weak()](const auto& update) {
|
||
if (auto control{ weakThis.get() }; control && !control->_IsClosing())
|
||
{
|
||
control->_throttledUpdateScrollbar(update);
|
||
}
|
||
});
|
||
|
||
// These events might all be triggered by the connection, but that
|
||
// should be drained and closed before we complete destruction. So these
|
||
// are safe.
|
||
//
|
||
// NOTE: _ScrollPositionChanged has to be registered after we set up the
|
||
// _updateScrollBar func. Otherwise, we could get a callback from an
|
||
// attached content before we set up the throttled func, and that'll A/V
|
||
_revokers.coreScrollPositionChanged = _core.ScrollPositionChanged(winrt::auto_revoke, { get_weak(), &TermControl::_ScrollPositionChanged });
|
||
_revokers.WarningBell = _core.WarningBell(winrt::auto_revoke, { get_weak(), &TermControl::_coreWarningBell });
|
||
|
||
static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast<int>(1.0 / 30.0 * 1000000));
|
||
_autoScrollTimer.Interval(AutoScrollUpdateInterval);
|
||
_autoScrollTimer.Tick({ get_weak(), &TermControl::_UpdateAutoScroll });
|
||
|
||
_ApplyUISettings();
|
||
|
||
_originalPrimaryElements = winrt::single_threaded_observable_vector<Controls::ICommandBarElement>();
|
||
_originalSecondaryElements = winrt::single_threaded_observable_vector<Controls::ICommandBarElement>();
|
||
_originalSelectedPrimaryElements = winrt::single_threaded_observable_vector<Controls::ICommandBarElement>();
|
||
_originalSelectedSecondaryElements = winrt::single_threaded_observable_vector<Controls::ICommandBarElement>();
|
||
for (const auto& e : ContextMenu().PrimaryCommands())
|
||
{
|
||
_originalPrimaryElements.Append(e);
|
||
}
|
||
for (const auto& e : ContextMenu().SecondaryCommands())
|
||
{
|
||
_originalSecondaryElements.Append(e);
|
||
}
|
||
for (const auto& e : SelectionContextMenu().PrimaryCommands())
|
||
{
|
||
_originalSelectedPrimaryElements.Append(e);
|
||
}
|
||
for (const auto& e : SelectionContextMenu().SecondaryCommands())
|
||
{
|
||
_originalSelectedSecondaryElements.Append(e);
|
||
}
|
||
ContextMenu().Closed([weakThis = get_weak()](auto&&, auto&&) {
|
||
if (auto control{ weakThis.get() }; control && !control->_IsClosing())
|
||
{
|
||
const auto& menu{ control->ContextMenu() };
|
||
menu.PrimaryCommands().Clear();
|
||
menu.SecondaryCommands().Clear();
|
||
for (const auto& e : control->_originalPrimaryElements)
|
||
{
|
||
menu.PrimaryCommands().Append(e);
|
||
}
|
||
for (const auto& e : control->_originalSecondaryElements)
|
||
{
|
||
menu.SecondaryCommands().Append(e);
|
||
}
|
||
}
|
||
});
|
||
SelectionContextMenu().Closed([weakThis = get_weak()](auto&&, auto&&) {
|
||
if (auto control{ weakThis.get() }; control && !control->_IsClosing())
|
||
{
|
||
const auto& menu{ control->SelectionContextMenu() };
|
||
menu.PrimaryCommands().Clear();
|
||
menu.SecondaryCommands().Clear();
|
||
for (const auto& e : control->_originalSelectedPrimaryElements)
|
||
{
|
||
menu.PrimaryCommands().Append(e);
|
||
}
|
||
for (const auto& e : control->_originalSelectedSecondaryElements)
|
||
{
|
||
menu.SecondaryCommands().Append(e);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Function Description:
|
||
// - Static helper for building a new TermControl from an already existing
|
||
// content. We'll attach the existing swapchain to this new control's
|
||
// SwapChainPanel. The IKeyBindings might belong to a non-agile object on
|
||
// a new thread, so we'll hook up the core to these new bindings.
|
||
// Arguments:
|
||
// - content: The preexisting ControlInteractivity to connect to.
|
||
// - keybindings: The new IKeyBindings instance to use for this control.
|
||
// Return Value:
|
||
// - The newly constructed TermControl.
|
||
Control::TermControl TermControl::NewControlByAttachingContent(Control::ControlInteractivity content,
|
||
const Microsoft::Terminal::Control::IKeyBindings& keyBindings)
|
||
{
|
||
const auto term{ winrt::make_self<TermControl>(content) };
|
||
term->_initializeForAttach(keyBindings);
|
||
return *term;
|
||
}
|
||
|
||
void TermControl::_initializeForAttach(const Microsoft::Terminal::Control::IKeyBindings& keyBindings)
|
||
{
|
||
_AttachDxgiSwapChainToXaml(reinterpret_cast<HANDLE>(_core.SwapChainHandle()));
|
||
_interactivity.AttachToNewControl(keyBindings);
|
||
|
||
// Initialize the terminal only once the swapchainpanel is loaded - that
|
||
// way, we'll be able to query the real pixel size it got on layout
|
||
auto r = SwapChainPanel().LayoutUpdated(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) {
|
||
// Replace the normal initialize routine with one that will allow up
|
||
// to complete initialization even though the Core was already
|
||
// initialized.
|
||
if (_InitializeTerminal(InitializeReason::Reattach))
|
||
{
|
||
// Only let this succeed once.
|
||
_layoutUpdatedRevoker.revoke();
|
||
}
|
||
});
|
||
_layoutUpdatedRevoker.swap(r);
|
||
}
|
||
|
||
uint64_t TermControl::ContentId() const
|
||
{
|
||
return _interactivity.Id();
|
||
}
|
||
|
||
TerminalConnection::ITerminalConnection TermControl::Connection()
|
||
{
|
||
return _core.Connection();
|
||
}
|
||
void TermControl::Connection(const TerminalConnection::ITerminalConnection& newConnection)
|
||
{
|
||
_core.Connection(newConnection);
|
||
}
|
||
|
||
void TermControl::_throttledUpdateScrollbar(const ScrollBarUpdate& update)
|
||
{
|
||
if (!_initializedTerminal)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Assumptions:
|
||
// * we're already not closing
|
||
// * caller already checked weak ptr to make sure we're still alive
|
||
|
||
_isInternalScrollBarUpdate = true;
|
||
|
||
auto scrollBar = ScrollBar();
|
||
if (update.newValue)
|
||
{
|
||
scrollBar.Value(*update.newValue);
|
||
}
|
||
scrollBar.Maximum(update.newMaximum);
|
||
scrollBar.Minimum(update.newMinimum);
|
||
scrollBar.ViewportSize(update.newViewportSize);
|
||
// scroll one full screen worth at a time when the scroll bar is clicked
|
||
scrollBar.LargeChange(std::max(update.newViewportSize - 1, 0.));
|
||
|
||
_isInternalScrollBarUpdate = false;
|
||
|
||
if (_showMarksInScrollbar)
|
||
{
|
||
const auto scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel();
|
||
const auto scrollBarWidthInDIP = scrollBar.ActualWidth();
|
||
const auto scrollBarHeightInDIP = scrollBar.ActualHeight();
|
||
const auto scrollBarWidthInPx = gsl::narrow_cast<int32_t>(lrint(scrollBarWidthInDIP * scaleFactor));
|
||
const auto scrollBarHeightInPx = gsl::narrow_cast<int32_t>(lrint(scrollBarHeightInDIP * scaleFactor));
|
||
|
||
const auto canvas = FindName(L"ScrollBarCanvas").as<Controls::Image>();
|
||
auto source = canvas.Source().try_as<Media::Imaging::WriteableBitmap>();
|
||
|
||
if (!source || scrollBarWidthInPx != source.PixelWidth() || scrollBarHeightInPx != source.PixelHeight())
|
||
{
|
||
source = Media::Imaging::WriteableBitmap{ scrollBarWidthInPx, scrollBarHeightInPx };
|
||
canvas.Source(source);
|
||
canvas.Width(scrollBarWidthInDIP);
|
||
canvas.Height(scrollBarHeightInDIP);
|
||
}
|
||
|
||
const auto buffer = source.PixelBuffer();
|
||
const auto data = buffer.data();
|
||
const auto stride = scrollBarWidthInPx * sizeof(til::color);
|
||
|
||
// The bitmap has the size of the entire scrollbar, but we want the marks to only show in the range the "thumb"
|
||
// (the scroll indicator) can move. That's why we need to add an offset to the start of the drawable bitmap area
|
||
// (to offset the decrease button) and subtract twice that (to offset the increase button as well).
|
||
//
|
||
// The WinUI standard scrollbar defines a Margin="2,0,2,0" for the "VerticalPanningThumb" and a Padding="0,4,0,0"
|
||
// for the "VerticalDecrementTemplate" (and similar for the increment), but it seems neither of those is correct,
|
||
// because a padding for 3 DIPs seem to be the exact right amount to add.
|
||
const auto increaseDecreaseButtonHeight = scrollBarWidthInPx + lround(3 * scaleFactor);
|
||
const auto drawableDataStart = data + stride * increaseDecreaseButtonHeight;
|
||
const auto drawableRange = scrollBarHeightInPx - 2 * increaseDecreaseButtonHeight;
|
||
|
||
// Protect the remaining code against negative offsets. This normally can't happen
|
||
// and this code just exists so it doesn't crash if I'm ever wrong about this.
|
||
// (The window has a min. size that ensures that there's always a scrollbar thumb.)
|
||
if (drawableRange < 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// The scrollbar bitmap is divided into 3 evenly sized stripes:
|
||
// Left: Regular marks
|
||
// Center: nothing
|
||
// Right: Search marks
|
||
const auto pipWidth = (scrollBarWidthInPx + 1) / 3;
|
||
const auto pipHeight = lround(1 * scaleFactor);
|
||
|
||
const auto maxOffsetY = drawableRange - pipHeight;
|
||
const auto offsetScale = maxOffsetY / gsl::narrow_cast<float>(update.newMaximum + update.newViewportSize);
|
||
// A helper to turn a TextBuffer row offset into a bitmap offset.
|
||
const auto dataAt = [&](til::CoordType row) [[msvc::forceinline]] {
|
||
const auto y = std::clamp<long>(lrintf(row * offsetScale), 0, maxOffsetY);
|
||
return drawableDataStart + stride * y;
|
||
};
|
||
// A helper to draw a single pip (mark) at the given location.
|
||
const auto drawPip = [&](uint8_t* beg, til::color color) [[msvc::forceinline]] {
|
||
const auto end = beg + pipHeight * stride;
|
||
for (; beg < end; beg += stride)
|
||
{
|
||
// a til::color does NOT have the same RGBA format as the bitmap.
|
||
#pragma warning(suppress : 26490) // Don't use reinterpret_cast (type.1).
|
||
const DWORD c = 0xff << 24 | color.r << 16 | color.g << 8 | color.b;
|
||
std::fill_n(reinterpret_cast<DWORD*>(beg), pipWidth, c);
|
||
}
|
||
};
|
||
|
||
memset(data, 0, buffer.Length());
|
||
|
||
if (const auto marks = _core.ScrollMarks())
|
||
{
|
||
for (const auto& m : marks)
|
||
{
|
||
const auto row = m.Row;
|
||
const til::color color{ m.Color.Color };
|
||
const auto base = dataAt(row);
|
||
drawPip(base, color);
|
||
}
|
||
}
|
||
|
||
if (_searchBox && _searchBox->IsOpen())
|
||
{
|
||
const auto core = winrt::get_self<ControlCore>(_core);
|
||
const auto& searchMatches = core->SearchResultRows();
|
||
const auto color = core->ForegroundColor();
|
||
const auto rightAlignedOffset = (scrollBarWidthInPx - pipWidth) * sizeof(til::color);
|
||
til::CoordType lastRow = til::CoordTypeMin;
|
||
|
||
for (const auto& span : searchMatches)
|
||
{
|
||
if (lastRow != span.start.y)
|
||
{
|
||
lastRow = span.start.y;
|
||
const auto base = dataAt(lastRow) + rightAlignedOffset;
|
||
drawPip(base, color);
|
||
}
|
||
}
|
||
}
|
||
|
||
source.Invalidate();
|
||
canvas.Visibility(Visibility::Visible);
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Loads the search box from the xaml UI and focuses it.
|
||
void TermControl::CreateSearchBoxControl()
|
||
{
|
||
// Lazy load the search box control.
|
||
if (auto loadedSearchBox{ FindName(L"SearchBox") })
|
||
{
|
||
if (auto searchBox{ loadedSearchBox.try_as<::winrt::Microsoft::Terminal::Control::SearchBoxControl>() })
|
||
{
|
||
// get at its private implementation
|
||
_searchBox.copy_from(winrt::get_self<implementation::SearchBoxControl>(searchBox));
|
||
|
||
// If a text is selected inside terminal, use it to populate the search box.
|
||
// If the search box already contains a value, it will be overridden.
|
||
if (_core.HasSelection())
|
||
{
|
||
// Currently we populate the search box only if a single line is selected.
|
||
// Empirically, multi-line selection works as well on sample scenarios,
|
||
// but since code paths differ, extra work is required to ensure correctness.
|
||
if (!_core.HasMultiLineSelection())
|
||
{
|
||
_core.SnapSearchResultToSelection(true);
|
||
const auto selectedLine{ _core.SelectedText(true) };
|
||
_searchBox->PopulateTextbox(selectedLine);
|
||
}
|
||
}
|
||
|
||
_searchBox->Open([weakThis = get_weak()]() {
|
||
if (const auto self = weakThis.get(); self && !self->_IsClosing())
|
||
{
|
||
self->_searchBox->SetFocusOnTextbox();
|
||
self->_refreshSearch();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// This is called when a Find Next/Previous Match action is triggered.
|
||
void TermControl::SearchMatch(const bool goForward)
|
||
{
|
||
if (_IsClosing())
|
||
{
|
||
return;
|
||
}
|
||
if (!_searchBox || !_searchBox->IsOpen())
|
||
{
|
||
CreateSearchBoxControl();
|
||
}
|
||
else
|
||
{
|
||
_handleSearchResults(_core.Search(_searchBox->Text(), goForward, _searchBox->CaseSensitive(), false));
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// Find if search box text edit currently is in focus
|
||
// Return Value:
|
||
// - true, if search box text edit is in focus
|
||
bool TermControl::SearchBoxEditInFocus() const
|
||
{
|
||
if (!_searchBox)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return _searchBox->TextBox().FocusState() == FocusState::Keyboard;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Search text in text buffer. This is triggered if the user clicks the
|
||
// search button, presses enter, or changes the search criteria.
|
||
// 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 TermControl::_Search(const winrt::hstring& text,
|
||
const bool goForward,
|
||
const bool caseSensitive)
|
||
{
|
||
if (_searchBox && _searchBox->IsOpen())
|
||
{
|
||
_handleSearchResults(_core.Search(text, goForward, caseSensitive, false));
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - The handler for the "search criteria changed" event. Initiates a new search.
|
||
// Arguments:
|
||
// - text: the text to search
|
||
// - goForward: indicates whether the search should be performed forward (if set to true) or backward
|
||
// - caseSensitive: boolean that represents if the current search is case sensitive
|
||
// Return Value:
|
||
// - <none>
|
||
void TermControl::_SearchChanged(const winrt::hstring& text,
|
||
const bool goForward,
|
||
const bool caseSensitive)
|
||
{
|
||
if (_searchBox && _searchBox->IsOpen())
|
||
{
|
||
// We only want to update the search results based on the new text. Set
|
||
// `resetOnly` to true so we don't accidentally update the current match index.
|
||
const auto result = _core.Search(text, goForward, caseSensitive, true);
|
||
_handleSearchResults(result);
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - The handler for the close button or pressing "Esc" when focusing on the
|
||
// search dialog.
|
||
// Arguments:
|
||
// - IInspectable: not used
|
||
// - RoutedEventArgs: not used
|
||
// Return Value:
|
||
// - <none>
|
||
void TermControl::_CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& /*sender*/,
|
||
const RoutedEventArgs& /*args*/)
|
||
{
|
||
_searchBox->Close();
|
||
_core.ClearSearch();
|
||
|
||
// Clear search highlights scroll marks (by triggering an update after closing the search box)
|
||
if (_showMarksInScrollbar)
|
||
{
|
||
const auto scrollBar = ScrollBar();
|
||
ScrollBarUpdate update{
|
||
.newValue = scrollBar.Value(),
|
||
.newMaximum = scrollBar.Maximum(),
|
||
.newMinimum = scrollBar.Minimum(),
|
||
.newViewportSize = scrollBar.ViewportSize(),
|
||
};
|
||
_updateScrollBar->Run(update);
|
||
}
|
||
|
||
// Set focus back to terminal control
|
||
this->Focus(FocusState::Programmatic);
|
||
}
|
||
|
||
winrt::fire_and_forget TermControl::UpdateControlSettings(IControlSettings settings)
|
||
{
|
||
return UpdateControlSettings(settings, _core.UnfocusedAppearance());
|
||
}
|
||
// Method Description:
|
||
// - Given Settings having been updated, applies the settings to the current terminal.
|
||
// Return Value:
|
||
// - <none>
|
||
winrt::fire_and_forget TermControl::UpdateControlSettings(IControlSettings settings,
|
||
IControlAppearance unfocusedAppearance)
|
||
{
|
||
auto weakThis{ get_weak() };
|
||
|
||
// Dispatch a call to the UI thread to apply the new settings to the
|
||
// terminal.
|
||
co_await wil::resume_foreground(Dispatcher());
|
||
|
||
if (auto strongThis{ weakThis.get() })
|
||
{
|
||
_core.UpdateSettings(settings, unfocusedAppearance);
|
||
|
||
_UpdateSettingsFromUIThread();
|
||
|
||
_UpdateAppearanceFromUIThread(_focused ? _core.FocusedAppearance() : _core.UnfocusedAppearance());
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Dispatches a call to the UI thread and updates the appearance
|
||
// Arguments:
|
||
// - newAppearance: the new appearance to set
|
||
winrt::fire_and_forget TermControl::UpdateAppearance(IControlAppearance newAppearance)
|
||
{
|
||
auto weakThis{ get_weak() };
|
||
|
||
// Dispatch a call to the UI thread
|
||
co_await wil::resume_foreground(Dispatcher());
|
||
|
||
if (auto strongThis{ weakThis.get() })
|
||
{
|
||
_UpdateAppearanceFromUIThread(newAppearance);
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Updates the settings of the current terminal.
|
||
// - This method is separate from UpdateSettings because there is an apparent optimizer
|
||
// issue that causes one of our hstring -> wstring_view conversions to result in garbage,
|
||
// but only from a coroutine context. See GH#8723.
|
||
// - INVARIANT: This method must be called from the UI thread.
|
||
// Arguments:
|
||
// - newSettings: the new settings to set
|
||
void TermControl::_UpdateSettingsFromUIThread()
|
||
{
|
||
if (_IsClosing())
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Update our control settings
|
||
_ApplyUISettings();
|
||
}
|
||
|
||
// Method Description:
|
||
// - Updates the appearance
|
||
// - INVARIANT: This method must be called from the UI thread.
|
||
// Arguments:
|
||
// - newAppearance: the new appearance to set
|
||
void TermControl::_UpdateAppearanceFromUIThread(Control::IControlAppearance newAppearance)
|
||
{
|
||
if (_IsClosing())
|
||
{
|
||
return;
|
||
}
|
||
|
||
_SetBackgroundImage(newAppearance);
|
||
|
||
// Update our control settings
|
||
const auto bg = newAppearance.DefaultBackground();
|
||
|
||
// In the future, this might need to be changed to a
|
||
// _InitializeBackgroundBrush call instead, because we may need to
|
||
// switch from a solid color brush to an acrylic one.
|
||
_changeBackgroundColor(bg);
|
||
|
||
// Update selection markers
|
||
Windows::UI::Xaml::Media::SolidColorBrush cursorColorBrush{ til::color{ newAppearance.CursorColor() } };
|
||
SelectionStartMarker().Fill(cursorColorBrush);
|
||
SelectionEndMarker().Fill(cursorColorBrush);
|
||
|
||
_core.ApplyAppearance(_focused);
|
||
}
|
||
|
||
// 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 TermControl::SendInput(const winrt::hstring& wstr)
|
||
{
|
||
// only broadcast if there's an actual listener. Saves the overhead of some object creation.
|
||
if (StringSent)
|
||
{
|
||
StringSent.raise(*this, winrt::make<StringSentEventArgs>(wstr));
|
||
}
|
||
|
||
RawWriteString(wstr);
|
||
}
|
||
void TermControl::ClearBuffer(Control::ClearBufferType clearType)
|
||
{
|
||
_core.ClearBuffer(clearType);
|
||
}
|
||
|
||
void TermControl::ToggleShaderEffects()
|
||
{
|
||
_core.ToggleShaderEffects();
|
||
}
|
||
|
||
// Method Description:
|
||
// - Style our UI elements based on the values in our settings, and set up
|
||
// other control-specific settings. This method will be called whenever
|
||
// the settings are reloaded.
|
||
// * Calls _InitializeBackgroundBrush to set up the Xaml brush responsible
|
||
// for the control's background
|
||
// * Calls _BackgroundColorChanged to style the background of the control
|
||
// - Core settings will be passed to the terminal in _InitializeTerminal
|
||
// Arguments:
|
||
// - <none>
|
||
// Return Value:
|
||
// - <none>
|
||
void TermControl::_ApplyUISettings()
|
||
{
|
||
_InitializeBackgroundBrush();
|
||
|
||
// settings might be out-of-proc in the future
|
||
auto settings{ _core.Settings() };
|
||
|
||
// Apply padding as swapChainPanel's margin
|
||
const auto newMargin = ParseThicknessFromPadding(settings.Padding());
|
||
SwapChainPanel().Margin(newMargin);
|
||
|
||
// Apply settings for scrollbar
|
||
if (settings.ScrollState() == ScrollbarState::Hidden)
|
||
{
|
||
// In the scenario where the user has turned off the OS setting to automatically hide scrollbars, the
|
||
// Terminal scrollbar would still be visible; so, we need to set the control's visibility accordingly to
|
||
// achieve the intended effect.
|
||
ScrollBar().IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::None);
|
||
ScrollBar().Visibility(Visibility::Collapsed);
|
||
}
|
||
else // (default or Visible)
|
||
{
|
||
// Default behavior
|
||
ScrollBar().IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator);
|
||
ScrollBar().Visibility(Visibility::Visible);
|
||
}
|
||
|
||
_interactivity.UpdateSettings();
|
||
if (_automationPeer)
|
||
{
|
||
_automationPeer.SetControlPadding(Core::Padding{
|
||
static_cast<float>(newMargin.Left),
|
||
static_cast<float>(newMargin.Top),
|
||
static_cast<float>(newMargin.Right),
|
||
static_cast<float>(newMargin.Bottom),
|
||
});
|
||
}
|
||
|
||
_showMarksInScrollbar = settings.ShowMarks();
|
||
// Hide all scrollbar marks since they might be disabled now.
|
||
if (const auto canvas = ScrollBarCanvas())
|
||
{
|
||
canvas.Visibility(Visibility::Collapsed);
|
||
}
|
||
// When we hot reload the settings, the core will send us a scrollbar
|
||
// update. If we enabled scrollbar marks, then great, when we handle
|
||
// that message, we'll redraw them.
|
||
}
|
||
|
||
// Method Description:
|
||
// - Sets background image and applies its settings (stretch, opacity and alignment)
|
||
// - Checks path validity
|
||
// Arguments:
|
||
// - newAppearance
|
||
// Return Value:
|
||
// - <none>
|
||
void TermControl::_SetBackgroundImage(const IControlAppearance& newAppearance)
|
||
{
|
||
if (newAppearance.BackgroundImage().empty() || _core.Settings().UseBackgroundImageForWindow())
|
||
{
|
||
BackgroundImage().Source(nullptr);
|
||
return;
|
||
}
|
||
|
||
Windows::Foundation::Uri imageUri{ nullptr };
|
||
try
|
||
{
|
||
imageUri = Windows::Foundation::Uri{ newAppearance.BackgroundImage() };
|
||
}
|
||
catch (...)
|
||
{
|
||
LOG_CAUGHT_EXCEPTION();
|
||
BackgroundImage().Source(nullptr);
|
||
return;
|
||
}
|
||
|
||
// Check if the image brush is already pointing to the image
|
||
// in the modified settings; if it isn't (or isn't there),
|
||
// set a new image source for the brush
|
||
auto imageSource = BackgroundImage().Source().try_as<Media::Imaging::BitmapImage>();
|
||
|
||
if (imageSource == nullptr ||
|
||
imageSource.UriSource() == nullptr ||
|
||
!imageSource.UriSource().Equals(imageUri))
|
||
{
|
||
// Note that BitmapImage handles the image load asynchronously,
|
||
// which is especially important since the image
|
||
// may well be both large and somewhere out on the
|
||
// internet.
|
||
Media::Imaging::BitmapImage image(imageUri);
|
||
BackgroundImage().Source(image);
|
||
}
|
||
|
||
// Apply stretch, opacity and alignment settings
|
||
BackgroundImage().Stretch(newAppearance.BackgroundImageStretchMode());
|
||
BackgroundImage().Opacity(newAppearance.BackgroundImageOpacity());
|
||
BackgroundImage().HorizontalAlignment(newAppearance.BackgroundImageHorizontalAlignment());
|
||
BackgroundImage().VerticalAlignment(newAppearance.BackgroundImageVerticalAlignment());
|
||
}
|
||
|
||
// Method Description:
|
||
// - Set up each layer's brush used to display the control's background.
|
||
// - Respects the settings for acrylic, background image and opacity from
|
||
// _settings.
|
||
// * If acrylic is not enabled, setup a solid color background, otherwise
|
||
// use bgcolor as acrylic's tint
|
||
// - Avoids image flickering and acrylic brush redraw if settings are changed
|
||
// but the appropriate brush is still in place.
|
||
// Arguments:
|
||
// - <none>
|
||
// Return Value:
|
||
// - <none>
|
||
void TermControl::_InitializeBackgroundBrush()
|
||
{
|
||
auto settings{ _core.Settings() };
|
||
auto bgColor = til::color{ _core.FocusedAppearance().DefaultBackground() };
|
||
|
||
auto transparentBg = settings.UseBackgroundImageForWindow();
|
||
if (transparentBg)
|
||
{
|
||
bgColor = Windows::UI::Colors::Transparent();
|
||
}
|
||
// GH#11743: Make sure to use the Core's current UseAcrylic value, not
|
||
// the one from the settings. The Core's runtime UseAcrylic may have
|
||
// changed from what was in the original settings.
|
||
if (_core.UseAcrylic() && !transparentBg)
|
||
{
|
||
// See if we've already got an acrylic background brush
|
||
// to avoid the flicker when setting up a new one
|
||
auto acrylic = RootGrid().Background().try_as<Media::AcrylicBrush>();
|
||
|
||
// Instantiate a brush if there's not already one there
|
||
if (acrylic == nullptr)
|
||
{
|
||
acrylic = Media::AcrylicBrush{};
|
||
}
|
||
|
||
const auto backdropStyle =
|
||
_core.Settings().EnableUnfocusedAcrylic() ? Media::AcrylicBackgroundSource::Backdrop : Media::AcrylicBackgroundSource::HostBackdrop;
|
||
acrylic.BackgroundSource(backdropStyle);
|
||
|
||
// see GH#1082: Initialize background color so we don't get a
|
||
// fade/flash when _BackgroundColorChanged is called
|
||
acrylic.FallbackColor(bgColor);
|
||
acrylic.TintColor(bgColor);
|
||
|
||
// Apply brush settings
|
||
acrylic.TintOpacity(_core.Opacity());
|
||
|
||
// Apply brush to control if it's not already there
|
||
if (RootGrid().Background() != acrylic)
|
||
{
|
||
RootGrid().Background(acrylic);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Media::SolidColorBrush solidColor{};
|
||
solidColor.Opacity(_core.Opacity());
|
||
solidColor.Color(bgColor);
|
||
|
||
RootGrid().Background(solidColor);
|
||
}
|
||
|
||
BackgroundBrush(RootGrid().Background());
|
||
}
|
||
|
||
// Method Description:
|
||
// - Handler for the core's BackgroundColorChanged event. Updates the color
|
||
// of our background brush to match.
|
||
// - Hops over to the UI thread to do this work.
|
||
// Arguments:
|
||
// <unused>
|
||
// Return Value:
|
||
// - <none>
|
||
winrt::fire_and_forget TermControl::_coreBackgroundColorChanged(const IInspectable& /*sender*/,
|
||
const IInspectable& /*args*/)
|
||
{
|
||
auto weakThis{ get_weak() };
|
||
co_await wil::resume_foreground(Dispatcher());
|
||
if (auto control{ weakThis.get() })
|
||
{
|
||
til::color newBgColor{ _core.BackgroundColor() };
|
||
_changeBackgroundColor(newBgColor);
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Update the color of the background brush we're using. This does _not_
|
||
// update the opacity, or what type of brush it is.
|
||
// - INVARIANT: This needs to be called on the UI thread.
|
||
// Arguments:
|
||
// - bg: the new color to use as the background color.
|
||
void TermControl::_changeBackgroundColor(til::color bg)
|
||
{
|
||
auto transparent_bg = _core.Settings().UseBackgroundImageForWindow();
|
||
if (transparent_bg)
|
||
{
|
||
bg = Windows::UI::Colors::Transparent();
|
||
}
|
||
|
||
if (auto acrylic = RootGrid().Background().try_as<Media::AcrylicBrush>())
|
||
{
|
||
acrylic.FallbackColor(bg);
|
||
acrylic.TintColor(bg);
|
||
}
|
||
else if (auto solidColor = RootGrid().Background().try_as<Media::SolidColorBrush>())
|
||
{
|
||
solidColor.Color(bg);
|
||
}
|
||
|
||
BackgroundBrush(RootGrid().Background());
|
||
|
||
// Don't use the normal BackgroundBrush() Observable Property setter
|
||
// here. (e.g. `BackgroundBrush()`). The one from the macro will
|
||
// automatically ignore changes where the value doesn't _actually_
|
||
// change. In our case, most of the time when changing the colors of the
|
||
// background, the _Brush_ itself doesn't change, we simply change the
|
||
// Color() of the brush. This results in the event not getting bubbled
|
||
// up.
|
||
//
|
||
// Firing it manually makes sure it does.
|
||
_BackgroundBrush = RootGrid().Background();
|
||
PropertyChanged.raise(*this, Data::PropertyChangedEventArgs{ L"BackgroundBrush" });
|
||
|
||
_isBackgroundLight = _isColorLight(bg);
|
||
}
|
||
|
||
bool TermControl::_isColorLight(til::color bg) noexcept
|
||
{
|
||
// Checks if the current background color is light enough
|
||
// to need a dark version of the visual bell indicator
|
||
// This is a poor man's Rec. 601 luma.
|
||
const auto l = 30 * bg.r + 59 * bg.g + 11 * bg.b;
|
||
return l > 12750;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Update the opacity of the background brush we're using. This does _not_
|
||
// update the color, or what type of brush it is.
|
||
// - INVARIANT: This needs to be called on the UI thread.
|
||
void TermControl::_changeBackgroundOpacity()
|
||
{
|
||
const auto opacity{ _core.Opacity() };
|
||
const auto useAcrylic{ _core.UseAcrylic() };
|
||
auto changed = false;
|
||
// GH#11743, #11619: If we're changing whether or not acrylic is used,
|
||
// then just entirely reinitialize the brush. The primary way that this
|
||
// happens is on Windows 10, where we need to enable acrylic when the
|
||
// user asks for <100% opacity. Even when we remove this Windows 10
|
||
// fallback, we may still need this for something like changing if
|
||
// acrylic is enabled at runtime (GH#2531)
|
||
if (auto acrylic = RootGrid().Background().try_as<Media::AcrylicBrush>())
|
||
{
|
||
if (!useAcrylic)
|
||
{
|
||
_InitializeBackgroundBrush();
|
||
return;
|
||
}
|
||
changed = acrylic.TintOpacity() != opacity;
|
||
acrylic.TintOpacity(opacity);
|
||
}
|
||
else if (auto solidColor = RootGrid().Background().try_as<Media::SolidColorBrush>())
|
||
{
|
||
if (useAcrylic)
|
||
{
|
||
_InitializeBackgroundBrush();
|
||
return;
|
||
}
|
||
changed = solidColor.Opacity() != opacity;
|
||
solidColor.Opacity(opacity);
|
||
}
|
||
// Send a BG brush changed event, so you can mouse wheel the
|
||
// transparency of the titlebar too.
|
||
if (changed)
|
||
{
|
||
PropertyChanged.raise(*this, Data::PropertyChangedEventArgs{ L"BackgroundBrush" });
|
||
}
|
||
}
|
||
|
||
TermControl::~TermControl()
|
||
{
|
||
Close();
|
||
}
|
||
|
||
// Method Description:
|
||
// - Creates an automation peer for the Terminal Control, enabling accessibility on our control.
|
||
// Arguments:
|
||
// - None
|
||
// Return Value:
|
||
// - The automation peer for our control
|
||
Windows::UI::Xaml::Automation::Peers::AutomationPeer TermControl::OnCreateAutomationPeer()
|
||
{
|
||
// MSFT 33353327: We're purposefully not using _initializedTerminal to ensure we're fully initialized.
|
||
// Doing so makes us return nullptr when XAML requests an automation peer.
|
||
// Instead, we need to give XAML an automation peer, then fix it later.
|
||
if (!_IsClosing() && !_detached)
|
||
{
|
||
// It's unexpected that interactivity is null even when we're not closing or in detached state.
|
||
THROW_HR_IF_NULL(E_UNEXPECTED, _interactivity);
|
||
|
||
// create a custom automation peer with this code pattern:
|
||
// (https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/custom-automation-peers)
|
||
if (const auto& interactivityAutoPeer{ _interactivity.OnCreateAutomationPeer() })
|
||
{
|
||
const auto margins{ SwapChainPanel().Margin() };
|
||
const Core::Padding padding{
|
||
static_cast<float>(margins.Left),
|
||
static_cast<float>(margins.Top),
|
||
static_cast<float>(margins.Right),
|
||
static_cast<float>(margins.Bottom),
|
||
};
|
||
_automationPeer = winrt::make<implementation::TermControlAutomationPeer>(get_strong(), padding, interactivityAutoPeer);
|
||
return _automationPeer;
|
||
}
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
// This is needed for TermControlAutomationPeer. We probably could find a
|
||
// clever way around asking the core for this.
|
||
til::point TermControl::GetFontSize() const
|
||
{
|
||
return { til::math::rounding, _core.FontSize().Width, _core.FontSize().Height };
|
||
}
|
||
|
||
const Windows::UI::Xaml::Thickness TermControl::GetPadding()
|
||
{
|
||
return SwapChainPanel().Margin();
|
||
}
|
||
|
||
TerminalConnection::ConnectionState TermControl::ConnectionState() const
|
||
{
|
||
return _core.ConnectionState();
|
||
}
|
||
|
||
void TermControl::RenderEngineSwapChainChanged(IInspectable /*sender*/, IInspectable args)
|
||
{
|
||
// This event comes in on the UI thread
|
||
HANDLE h = reinterpret_cast<HANDLE>(winrt::unbox_value<uint64_t>(args));
|
||
_AttachDxgiSwapChainToXaml(h);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Called when the renderer triggers a warning. It might do this when it
|
||
// fails to find a shader file, or fails to compile a shader. We'll take
|
||
// that renderer warning, and display a dialog to the user with and
|
||
// appropriate error message. WE'll display the dialog with our
|
||
// RaiseNotice event.
|
||
// Arguments:
|
||
// - hr: an HRESULT describing the warning
|
||
// Return Value:
|
||
// - <none>
|
||
winrt::fire_and_forget TermControl::_RendererWarning(IInspectable /*sender*/, Control::RendererWarningArgs args)
|
||
{
|
||
auto weakThis{ get_weak() };
|
||
co_await wil::resume_foreground(Dispatcher());
|
||
|
||
const auto control = weakThis.get();
|
||
if (!control)
|
||
{
|
||
co_return;
|
||
}
|
||
|
||
// HRESULT is a signed 32-bit integer which would result in a hex output like "-0x7766FFF4",
|
||
// but canonically HRESULTs are displayed unsigned as "0x8899000C". See GH#11556.
|
||
const auto hr = std::bit_cast<uint32_t>(args.Result());
|
||
const auto parameter = args.Parameter();
|
||
winrt::hstring message;
|
||
|
||
switch (hr)
|
||
{
|
||
case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):
|
||
case HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND):
|
||
message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PixelShaderNotFound") }, parameter) };
|
||
break;
|
||
case D2DERR_SHADER_COMPILE_FAILED:
|
||
message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PixelShaderCompileFailed") }) };
|
||
break;
|
||
case DWRITE_E_NOFONT:
|
||
message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"RendererErrorFontNotFound") }, parameter) };
|
||
break;
|
||
case ATLAS_ENGINE_ERROR_MAC_TYPE:
|
||
message = RS_(L"RendererErrorMacType");
|
||
break;
|
||
default:
|
||
{
|
||
wchar_t buf[512];
|
||
const auto len = FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), &buf[0], ARRAYSIZE(buf), nullptr);
|
||
const std::wstring_view msg{ &buf[0], len };
|
||
message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"RendererErrorOther") }, hr, msg) };
|
||
break;
|
||
}
|
||
}
|
||
|
||
auto noticeArgs = winrt::make<NoticeEventArgs>(NoticeLevel::Warning, std::move(message));
|
||
control->RaiseNotice.raise(*control, std::move(noticeArgs));
|
||
}
|
||
|
||
void TermControl::_AttachDxgiSwapChainToXaml(HANDLE swapChainHandle)
|
||
{
|
||
auto nativePanel = SwapChainPanel().as<ISwapChainPanelNative2>();
|
||
nativePanel->SetSwapChainHandle(swapChainHandle);
|
||
}
|
||
|
||
bool TermControl::_InitializeTerminal(const InitializeReason reason)
|
||
{
|
||
if (_initializedTerminal)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const auto panelWidth = static_cast<float>(SwapChainPanel().ActualWidth());
|
||
const auto panelHeight = static_cast<float>(SwapChainPanel().ActualHeight());
|
||
const auto panelScaleX = SwapChainPanel().CompositionScaleX();
|
||
const auto panelScaleY = SwapChainPanel().CompositionScaleY();
|
||
|
||
const auto windowWidth = panelWidth * panelScaleX;
|
||
const auto windowHeight = panelHeight * panelScaleY;
|
||
|
||
if (windowWidth == 0 || windowHeight == 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// If we're re-attaching an existing content, then we want to proceed even though the Terminal was already initialized.
|
||
if (reason == InitializeReason::Create)
|
||
{
|
||
const auto coreInitialized = _core.Initialize(panelWidth,
|
||
panelHeight,
|
||
panelScaleX);
|
||
if (!coreInitialized)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
_interactivity.Initialize();
|
||
|
||
if (!_restorePath.empty())
|
||
{
|
||
_restoreInBackground();
|
||
}
|
||
else
|
||
{
|
||
_core.Connection().Start();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_core.SizeOrScaleChanged(panelWidth, panelHeight, panelScaleX);
|
||
}
|
||
|
||
_core.EnablePainting();
|
||
|
||
auto bufferHeight = _core.BufferHeight();
|
||
|
||
ScrollBar().Maximum(0);
|
||
ScrollBar().Minimum(0);
|
||
ScrollBar().Value(0);
|
||
ScrollBar().ViewportSize(bufferHeight);
|
||
ScrollBar().LargeChange(bufferHeight); // scroll one "screenful" at a time when the scroll bar is clicked
|
||
|
||
// Set up blinking cursor
|
||
int blinkTime = GetCaretBlinkTime();
|
||
if (blinkTime != INFINITE)
|
||
{
|
||
// Create a timer
|
||
_cursorTimer.Interval(std::chrono::milliseconds(blinkTime));
|
||
_cursorTimer.Tick({ get_weak(), &TermControl::_CursorTimerTick });
|
||
// As of GH#6586, don't start the cursor timer immediately, and
|
||
// don't show the cursor initially. We'll show the cursor and start
|
||
// the timer when the control is first focused.
|
||
//
|
||
// As of GH#11411, turn on the cursor if we've already been marked
|
||
// as focused. We suspect that it's possible for the Focused event
|
||
// to fire before the LayoutUpdated. In that case, the
|
||
// _GotFocusHandler would mark us _focused, but find that a
|
||
// _cursorTimer doesn't exist, and it would never turn on the
|
||
// cursor. To mitigate, we'll initialize the cursor's 'on' state
|
||
// with `_focused` here.
|
||
_core.CursorOn(_focused || _displayCursorWhileBlurred());
|
||
if (_displayCursorWhileBlurred())
|
||
{
|
||
_cursorTimer.Start();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_cursorTimer.Destroy();
|
||
}
|
||
|
||
// Set up blinking attributes
|
||
auto animationsEnabled = TRUE;
|
||
SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &animationsEnabled, 0);
|
||
if (animationsEnabled && blinkTime != INFINITE)
|
||
{
|
||
// Create a timer
|
||
_blinkTimer.Interval(std::chrono::milliseconds(blinkTime));
|
||
_blinkTimer.Tick({ get_weak(), &TermControl::_BlinkTimerTick });
|
||
_blinkTimer.Start();
|
||
}
|
||
else
|
||
{
|
||
// The user has disabled blinking
|
||
_blinkTimer.Destroy();
|
||
}
|
||
|
||
// Now that the renderer is set up, update the appearance for initialization
|
||
_UpdateAppearanceFromUIThread(_core.FocusedAppearance());
|
||
|
||
_initializedTerminal = true;
|
||
|
||
// MSFT 33353327: If the AutomationPeer was created before we were done initializing,
|
||
// make sure it's properly set up now.
|
||
if (_automationPeer)
|
||
{
|
||
_automationPeer.UpdateControlBounds();
|
||
const auto margins{ GetPadding() };
|
||
_automationPeer.SetControlPadding(Core::Padding{
|
||
static_cast<float>(margins.Left),
|
||
static_cast<float>(margins.Top),
|
||
static_cast<float>(margins.Right),
|
||
static_cast<float>(margins.Bottom),
|
||
});
|
||
}
|
||
|
||
// Likewise, run the event handlers outside of lock (they could
|
||
// be reentrant)
|
||
Initialized.raise(*this, nullptr);
|
||
return true;
|
||
}
|
||
|
||
winrt::fire_and_forget TermControl::_restoreInBackground()
|
||
{
|
||
const auto path = std::exchange(_restorePath, {});
|
||
const auto weakSelf = get_weak();
|
||
winrt::apartment_context uiThread;
|
||
|
||
try
|
||
{
|
||
co_await winrt::resume_background();
|
||
|
||
const auto self = weakSelf.get();
|
||
if (!self)
|
||
{
|
||
co_return;
|
||
}
|
||
|
||
winrt::get_self<ControlCore>(_core)->RestoreFromPath(path.c_str());
|
||
}
|
||
CATCH_LOG();
|
||
|
||
try
|
||
{
|
||
co_await uiThread;
|
||
|
||
const auto self = weakSelf.get();
|
||
if (!self)
|
||
{
|
||
co_return;
|
||
}
|
||
|
||
if (const auto connection = _core.Connection())
|
||
{
|
||
connection.Start();
|
||
}
|
||
}
|
||
CATCH_LOG();
|
||
}
|
||
|
||
void TermControl::_CharacterHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
|
||
const Input::CharacterReceivedRoutedEventArgs& e)
|
||
{
|
||
if (_IsClosing())
|
||
{
|
||
return;
|
||
}
|
||
|
||
HidePointerCursor.raise(*this, nullptr);
|
||
|
||
const auto ch = e.Character();
|
||
const auto keyStatus = e.KeyStatus();
|
||
const auto scanCode = gsl::narrow_cast<WORD>(keyStatus.ScanCode);
|
||
auto modifiers = _GetPressedModifierKeys();
|
||
|
||
if (keyStatus.IsExtendedKey)
|
||
{
|
||
modifiers |= ControlKeyStates::EnhancedKey;
|
||
}
|
||
|
||
// Broadcast the character to all listeners
|
||
// only broadcast if there's an actual listener. Saves the overhead of some object creation.
|
||
if (CharSent)
|
||
{
|
||
auto charSentArgs = winrt::make<CharSentEventArgs>(ch, scanCode, modifiers);
|
||
CharSent.raise(*this, charSentArgs);
|
||
}
|
||
|
||
const auto handled = RawWriteChar(ch, scanCode, modifiers);
|
||
|
||
e.Handled(handled);
|
||
}
|
||
|
||
bool TermControl::RawWriteChar(const wchar_t character,
|
||
const WORD scanCode,
|
||
const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers)
|
||
{
|
||
return _core.SendCharEvent(character, scanCode, modifiers);
|
||
}
|
||
|
||
void TermControl::RawWriteString(const winrt::hstring& text)
|
||
{
|
||
_core.SendInput(text);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Manually handles key events for certain keys that can't be passed to us
|
||
// normally. Namely, the keys we're concerned with are F7 down and Alt up.
|
||
// Return value:
|
||
// - Whether the key was handled.
|
||
bool TermControl::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down)
|
||
{
|
||
const auto modifiers{ _GetPressedModifierKeys() };
|
||
return _KeyHandler(gsl::narrow_cast<WORD>(vkey), gsl::narrow_cast<WORD>(scanCode), modifiers, down);
|
||
}
|
||
|
||
void TermControl::_KeyDownHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
|
||
const Input::KeyRoutedEventArgs& e)
|
||
{
|
||
_KeyHandler(e, true);
|
||
}
|
||
|
||
void TermControl::_KeyUpHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
|
||
const Input::KeyRoutedEventArgs& e)
|
||
{
|
||
_KeyHandler(e, false);
|
||
}
|
||
|
||
void TermControl::_KeyHandler(const Input::KeyRoutedEventArgs& e, const bool keyDown)
|
||
{
|
||
const auto keyStatus = e.KeyStatus();
|
||
const auto vkey = gsl::narrow_cast<WORD>(e.OriginalKey());
|
||
const auto scanCode = gsl::narrow_cast<WORD>(keyStatus.ScanCode);
|
||
auto modifiers = _GetPressedModifierKeys();
|
||
|
||
if (keyStatus.IsExtendedKey)
|
||
{
|
||
modifiers |= ControlKeyStates::EnhancedKey;
|
||
}
|
||
|
||
e.Handled(_KeyHandler(vkey, scanCode, modifiers, keyDown));
|
||
}
|
||
|
||
bool TermControl::_KeyHandler(WORD vkey, WORD scanCode, ControlKeyStates modifiers, bool keyDown)
|
||
{
|
||
// If the current focused element is a child element of searchbox,
|
||
// we do not send this event up to terminal
|
||
if (_searchBox && _searchBox->ContainsFocus())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// GH#11076:
|
||
// For some weird reason we sometimes receive a WM_KEYDOWN
|
||
// message without vkey or scanCode if a user drags a tab.
|
||
// The KeyChord constructor has a debug assertion ensuring that all KeyChord
|
||
// either have a valid vkey/scanCode. This is important, because this prevents
|
||
// accidental insertion of invalid KeyChords into classes like ActionMap.
|
||
if (!vkey && !scanCode)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// Mark the event as handled and do nothing if we're closing, or the key
|
||
// was the Windows key.
|
||
//
|
||
// NOTE: for key combos like CTRL + C, two events are fired (one for
|
||
// CTRL, one for 'C'). Since it's possible the terminal is in
|
||
// win32-input-mode, then we'll send all these keystrokes to the
|
||
// terminal - it's smart enough to ignore the keys it doesn't care
|
||
// about.
|
||
if (_IsClosing() || vkey == VK_LWIN || vkey == VK_RWIN)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// Short-circuit isReadOnly check to avoid warning dialog
|
||
if (_core.IsInReadOnlyMode())
|
||
{
|
||
return !keyDown || _TryHandleKeyBinding(vkey, scanCode, modifiers);
|
||
}
|
||
|
||
// Our custom TSF input control doesn't receive Alt+Numpad inputs,
|
||
// and we don't receive any via WM_CHAR as a xaml island app either.
|
||
// So, we simply implement our own Alt-Numpad handling here.
|
||
//
|
||
// This handles the case where the Alt key is released.
|
||
// We'll flush any ongoing composition in that case.
|
||
if (vkey == VK_MENU && !keyDown && _altNumpadState.active)
|
||
{
|
||
auto& s = _altNumpadState;
|
||
auto encoding = s.encoding;
|
||
wchar_t buf[4]{};
|
||
size_t buf_len = 0;
|
||
|
||
if (encoding == AltNumpadEncoding::Unicode)
|
||
{
|
||
// UTF-32 -> UTF-16
|
||
if (s.accumulator <= 0xffff)
|
||
{
|
||
buf[buf_len++] = static_cast<uint16_t>(s.accumulator);
|
||
}
|
||
else
|
||
{
|
||
buf[buf_len++] = static_cast<uint16_t>((s.accumulator >> 10) + 0xd7c0);
|
||
buf[buf_len++] = static_cast<uint16_t>((s.accumulator & 0x3ff) | 0xdc00);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
const auto ansi = encoding == AltNumpadEncoding::ANSI;
|
||
const auto acp = GetACP();
|
||
auto codepage = ansi ? acp : CP_OEMCP;
|
||
|
||
// Alt+Numpad inputs are always a single codepoint, be it UTF-32 or ANSI.
|
||
// Since DBCS code pages by definition are >1 codepoint, we can't encode those.
|
||
// Traditionally, the OS uses the Latin1 or IBM code page instead.
|
||
if (acp == CP_JAPANESE ||
|
||
acp == CP_CHINESE_SIMPLIFIED ||
|
||
acp == CP_KOREAN ||
|
||
acp == CP_CHINESE_TRADITIONAL ||
|
||
acp == CP_UTF8)
|
||
{
|
||
codepage = ansi ? 1252 : 437;
|
||
}
|
||
|
||
// The OS code seemed to also simply cut off the last byte in the accumulator.
|
||
const auto ch = gsl::narrow_cast<char>(s.accumulator & 0xff);
|
||
const auto len = MultiByteToWideChar(codepage, 0, &ch, 1, &buf[0], 2);
|
||
buf_len = gsl::narrow_cast<size_t>(std::max(0, len));
|
||
}
|
||
|
||
if (buf_len != 0)
|
||
{
|
||
// WinRT always needs null-terminated strings, because HSTRING is dumb.
|
||
// If it encounters a string that isn't, cppwinrt will abort().
|
||
// It should already be null-terminated, but let's make sure to not crash.
|
||
buf[buf_len] = L'\0';
|
||
_core.SendInput(std::wstring_view{ &buf[0], buf_len });
|
||
}
|
||
|
||
s = {};
|
||
return true;
|
||
}
|
||
// As a continuation of the above, this handles the key-down case.
|
||
if (modifiers.IsAltPressed())
|
||
{
|
||
// The OS code seems to reset the composition if shift is pressed, but I couldn't
|
||
// figure out how exactly it worked. We'll simply ignore any such inputs.
|
||
static constexpr DWORD permittedModifiers =
|
||
RIGHT_ALT_PRESSED |
|
||
LEFT_ALT_PRESSED |
|
||
NUMLOCK_ON |
|
||
SCROLLLOCK_ON |
|
||
CAPSLOCK_ON;
|
||
|
||
if (keyDown && (modifiers.Value() & ~permittedModifiers) == 0)
|
||
{
|
||
auto& s = _altNumpadState;
|
||
|
||
if (vkey == VK_ADD)
|
||
{
|
||
// Alt '+' <number> is used to input Unicode code points.
|
||
// Every time you press + it resets the entire state
|
||
// in the original OS implementation as well.
|
||
s.encoding = AltNumpadEncoding::Unicode;
|
||
s.accumulator = 0;
|
||
s.active = true;
|
||
}
|
||
else if (vkey == VK_NUMPAD0 && s.encoding == AltNumpadEncoding::OEM && s.accumulator == 0)
|
||
{
|
||
// Alt '0' <number> is used to input ANSI code points.
|
||
// Otherwise, they're OEM codepoints.
|
||
s.encoding = AltNumpadEncoding::ANSI;
|
||
s.active = true;
|
||
}
|
||
else
|
||
{
|
||
// Otherwise, append the pressed key to the accumulator.
|
||
const uint32_t base = s.encoding == AltNumpadEncoding::Unicode ? 16 : 10;
|
||
uint32_t add = 0xffffff;
|
||
|
||
if (vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9)
|
||
{
|
||
add = vkey - VK_NUMPAD0;
|
||
}
|
||
else if (vkey >= 'A' && vkey <= 'F')
|
||
{
|
||
add = vkey - 'A' + 10;
|
||
}
|
||
|
||
// Pressing Alt + <not a number> should not activate the Alt+Numpad input, however.
|
||
if (add < base)
|
||
{
|
||
s.accumulator = std::min(s.accumulator * base + add, 0x10FFFFu);
|
||
s.active = true;
|
||
}
|
||
}
|
||
|
||
// If someone pressed Alt + <not a number>, we'll skip the early
|
||
// return and send the Alt key combination as per usual.
|
||
if (s.active)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// Unless I didn't code the above correctly, active == false should imply
|
||
// that _altNumpadState is in the (default constructed) base state.
|
||
assert(s.encoding == AltNumpadEncoding::OEM);
|
||
assert(s.accumulator == 0);
|
||
}
|
||
}
|
||
else if (_altNumpadState.active)
|
||
{
|
||
// If the user Alt+Tabbed in the middle of an Alt+Numpad sequence, we'll not receive a key-up event for
|
||
// the Alt key. There are several ways to detect this. Here, we simply check if the user typed another
|
||
// character, it's not an alt-up event, and we still have an ongoing composition.
|
||
_altNumpadState = {};
|
||
}
|
||
|
||
// GH#2235: Terminal::Settings hasn't been modified to differentiate
|
||
// between AltGr and Ctrl+Alt yet.
|
||
// -> Don't check for key bindings if this is an AltGr key combination.
|
||
//
|
||
// GH#4999: Only process keybindings on the keydown. If we don't check
|
||
// this at all, we'll process the keybinding twice. If we only process
|
||
// keybindings on the keyUp, then we'll still send the keydown to the
|
||
// connected terminal application, and something like ctrl+shift+T will
|
||
// emit a ^T to the pipe.
|
||
if (!modifiers.IsAltGrPressed() &&
|
||
keyDown &&
|
||
_TryHandleKeyBinding(vkey, scanCode, modifiers))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (_TrySendKeyEvent(vkey, scanCode, modifiers, keyDown))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// Manually prevent keyboard navigation with tab. We want to send tab to
|
||
// the terminal, and we don't want to be able to escape focus of the
|
||
// control with tab.
|
||
return vkey == VK_TAB;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Attempt to handle this key combination as a key binding
|
||
// Arguments:
|
||
// - vkey: The vkey of the key pressed.
|
||
// - scanCode: The scan code of the key pressed.
|
||
// - modifiers: The ControlKeyStates representing the modifier key states.
|
||
bool TermControl::_TryHandleKeyBinding(const WORD vkey, const WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers) const
|
||
{
|
||
// Mark mode has a specific set of pre-defined key bindings.
|
||
// If we're in mark mode, we should be prioritizing those over
|
||
// the custom defined key bindings.
|
||
if (_core.TryMarkModeKeybinding(vkey, modifiers))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
auto bindings = _core.Settings().KeyBindings();
|
||
if (!bindings)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
auto success = bindings.TryKeyChord({
|
||
modifiers.IsCtrlPressed(),
|
||
modifiers.IsAltPressed(),
|
||
modifiers.IsShiftPressed(),
|
||
modifiers.IsWinPressed(),
|
||
vkey,
|
||
scanCode,
|
||
});
|
||
if (!success)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// Let's assume the user has bound the dead key "^" to a sendInput command that sends "b".
|
||
// If the user presses the two keys "^a" it'll produce "bâ", despite us marking the key event as handled.
|
||
// The following is used to manually "consume" such dead keys and clear them from the keyboard state.
|
||
_ClearKeyboardState(vkey, scanCode);
|
||
return true;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Discards currently pressed dead keys.
|
||
// Arguments:
|
||
// - vkey: The vkey of the key pressed.
|
||
// - scanCode: The scan code of the key pressed.
|
||
void TermControl::_ClearKeyboardState(const WORD vkey, const WORD scanCode) noexcept
|
||
{
|
||
std::array<BYTE, 256> keyState;
|
||
if (!GetKeyboardState(keyState.data()))
|
||
{
|
||
return;
|
||
}
|
||
|
||
// As described in "Sometimes you *want* to interfere with the keyboard's state buffer":
|
||
// http://archives.miloush.net/michkap/archive/2006/09/10/748775.html
|
||
// > "The key here is to keep trying to pass stuff to ToUnicode until -1 is not returned."
|
||
std::array<wchar_t, 16> buffer;
|
||
while (ToUnicodeEx(vkey, scanCode, keyState.data(), buffer.data(), gsl::narrow_cast<int>(buffer.size()), 0b1, nullptr) < 0)
|
||
{
|
||
}
|
||
}
|
||
|
||
// 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.
|
||
// - states: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states.
|
||
// - keyDown: If true, the key was pressed, otherwise the key was released.
|
||
bool TermControl::_TrySendKeyEvent(const WORD vkey,
|
||
const WORD scanCode,
|
||
const ControlKeyStates modifiers,
|
||
const bool keyDown)
|
||
{
|
||
// Broadcast the key to all listeners
|
||
// only broadcast if there's an actual listener. Saves the overhead of some object creation.
|
||
if (KeySent)
|
||
{
|
||
auto keySentArgs = winrt::make<KeySentEventArgs>(vkey, scanCode, modifiers, keyDown);
|
||
KeySent.raise(*this, keySentArgs);
|
||
}
|
||
|
||
return RawWriteKeyEvent(vkey, scanCode, modifiers, keyDown);
|
||
}
|
||
|
||
bool TermControl::RawWriteKeyEvent(const WORD vkey,
|
||
const WORD scanCode,
|
||
const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers,
|
||
const bool keyDown)
|
||
{
|
||
const auto window = CoreWindow::GetForCurrentThread();
|
||
|
||
// 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.
|
||
const auto handled = vkey ?
|
||
_core.TrySendKeyEvent(vkey,
|
||
scanCode,
|
||
modifiers,
|
||
keyDown) :
|
||
true;
|
||
|
||
if (vkey && keyDown && _automationPeer)
|
||
{
|
||
get_self<TermControlAutomationPeer>(_automationPeer)->RecordKeyEvent(vkey);
|
||
}
|
||
|
||
if (_cursorTimer)
|
||
{
|
||
// Manually show the cursor when a key is pressed. Restarting
|
||
// the timer prevents flickering.
|
||
_core.CursorOn(_core.SelectionMode() != SelectionInteractionMode::Mark);
|
||
_cursorTimer.Start();
|
||
}
|
||
|
||
return handled;
|
||
}
|
||
|
||
// Method Description:
|
||
// - handle a tap event by taking focus
|
||
// Arguments:
|
||
// - sender: the XAML element responding to the tap event
|
||
// - args: event data
|
||
void TermControl::_TappedHandler(const IInspectable& /*sender*/, const TappedRoutedEventArgs& e)
|
||
{
|
||
Focus(FocusState::Pointer);
|
||
e.Handled(true);
|
||
}
|
||
|
||
// Method Description:
|
||
// - handle a mouse click event. Begin selection process.
|
||
// Arguments:
|
||
// - sender: the XAML element responding to the pointer input
|
||
// - args: event data
|
||
void TermControl::_PointerPressedHandler(const Windows::Foundation::IInspectable& sender,
|
||
const Input::PointerRoutedEventArgs& args)
|
||
{
|
||
if (_IsClosing())
|
||
{
|
||
return;
|
||
}
|
||
|
||
RestorePointerCursor.raise(*this, nullptr);
|
||
|
||
_CapturePointer(sender, args);
|
||
|
||
const auto ptr = args.Pointer();
|
||
const auto point = args.GetCurrentPoint(*this);
|
||
const auto type = ptr.PointerDeviceType();
|
||
|
||
// We also TryShow in GotFocusHandler, but this call is specifically
|
||
// for the case where the Terminal is in focus but the user closed the
|
||
// on-screen keyboard. This lets the user simply tap on the terminal
|
||
// again to bring it up.
|
||
InputPane::GetForCurrentView().TryShow();
|
||
|
||
if (!_focused)
|
||
{
|
||
Focus(FocusState::Pointer);
|
||
}
|
||
|
||
// Mark that this pointer event actually started within our bounds.
|
||
// We'll need this later, for PointerMoved events.
|
||
_pointerPressedInBounds = true;
|
||
|
||
if (type == Windows::Devices::Input::PointerDeviceType::Touch)
|
||
{
|
||
const auto contactRect = point.Properties().ContactRect();
|
||
auto anchor = til::point{ til::math::rounding, contactRect.X, contactRect.Y };
|
||
_interactivity.TouchPressed(anchor.to_core_point());
|
||
}
|
||
else
|
||
{
|
||
const auto cursorPosition = point.Position();
|
||
_interactivity.PointerPressed(TermControl::GetPressedMouseButtons(point),
|
||
TermControl::GetPointerUpdateKind(point),
|
||
point.Timestamp(),
|
||
ControlKeyStates{ args.KeyModifiers() },
|
||
_toTerminalOrigin(cursorPosition).to_core_point());
|
||
}
|
||
|
||
args.Handled(true);
|
||
}
|
||
|
||
// Method Description:
|
||
// - handle a mouse moved event. Specifically handling mouse drag to update selection process.
|
||
// Arguments:
|
||
// - sender: not used
|
||
// - args: event data
|
||
void TermControl::_PointerMovedHandler(const Windows::Foundation::IInspectable& /*sender*/,
|
||
const Input::PointerRoutedEventArgs& args)
|
||
{
|
||
if (_IsClosing())
|
||
{
|
||
return;
|
||
}
|
||
|
||
RestorePointerCursor.raise(*this, nullptr);
|
||
|
||
const auto ptr = args.Pointer();
|
||
const auto point = args.GetCurrentPoint(*this);
|
||
const auto cursorPosition = point.Position();
|
||
const auto pixelPosition = _toTerminalOrigin(cursorPosition);
|
||
const auto type = ptr.PointerDeviceType();
|
||
|
||
if (!_focused && _core.Settings().FocusFollowMouse())
|
||
{
|
||
FocusFollowMouseRequested.raise(*this, nullptr);
|
||
}
|
||
|
||
if (type == Windows::Devices::Input::PointerDeviceType::Mouse ||
|
||
type == Windows::Devices::Input::PointerDeviceType::Pen)
|
||
{
|
||
_interactivity.PointerMoved(TermControl::GetPressedMouseButtons(point),
|
||
TermControl::GetPointerUpdateKind(point),
|
||
ControlKeyStates(args.KeyModifiers()),
|
||
_focused,
|
||
pixelPosition.to_core_point(),
|
||
_pointerPressedInBounds);
|
||
|
||
// GH#9109 - Only start an auto-scroll when the drag actually
|
||
// started within our bounds. Otherwise, someone could start a drag
|
||
// outside the terminal control, drag into the padding, and trick us
|
||
// into starting to scroll.
|
||
if (_focused && _pointerPressedInBounds && point.Properties().IsLeftButtonPressed())
|
||
{
|
||
// We want to find the distance relative to the bounds of the
|
||
// SwapChainPanel, not the entire control. If they drag out of
|
||
// the bounds of the text, into the padding, we still what that
|
||
// to auto-scroll
|
||
const auto cursorBelowBottomDist = cursorPosition.Y - SwapChainPanel().Margin().Top - SwapChainPanel().ActualHeight();
|
||
const auto cursorAboveTopDist = -1 * cursorPosition.Y + SwapChainPanel().Margin().Top;
|
||
|
||
constexpr auto MinAutoScrollDist = 2.0; // Arbitrary value
|
||
auto newAutoScrollVelocity = 0.0;
|
||
if (cursorBelowBottomDist > MinAutoScrollDist)
|
||
{
|
||
newAutoScrollVelocity = _GetAutoScrollSpeed(cursorBelowBottomDist);
|
||
}
|
||
else if (cursorAboveTopDist > MinAutoScrollDist)
|
||
{
|
||
newAutoScrollVelocity = -1.0 * _GetAutoScrollSpeed(cursorAboveTopDist);
|
||
}
|
||
|
||
if (newAutoScrollVelocity != 0)
|
||
{
|
||
_TryStartAutoScroll(point, newAutoScrollVelocity);
|
||
}
|
||
else
|
||
{
|
||
_TryStopAutoScroll(ptr.PointerId());
|
||
}
|
||
}
|
||
}
|
||
else if (type == Windows::Devices::Input::PointerDeviceType::Touch)
|
||
{
|
||
const auto contactRect = point.Properties().ContactRect();
|
||
til::point newTouchPoint{ til::math::rounding, contactRect.X, contactRect.Y };
|
||
|
||
_interactivity.TouchMoved(newTouchPoint.to_core_point(), _focused);
|
||
}
|
||
|
||
args.Handled(true);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Event handler for the PointerReleased event. We use this to de-anchor
|
||
// touch events, to stop scrolling via touch.
|
||
// Arguments:
|
||
// - sender: the XAML element responding to the pointer input
|
||
// - args: event data
|
||
void TermControl::_PointerReleasedHandler(const Windows::Foundation::IInspectable& sender,
|
||
const Input::PointerRoutedEventArgs& args)
|
||
{
|
||
if (_IsClosing())
|
||
{
|
||
return;
|
||
}
|
||
|
||
_pointerPressedInBounds = false;
|
||
|
||
const auto ptr = args.Pointer();
|
||
const auto point = args.GetCurrentPoint(*this);
|
||
const auto cursorPosition = point.Position();
|
||
const auto pixelPosition = _toTerminalOrigin(cursorPosition);
|
||
const auto type = ptr.PointerDeviceType();
|
||
|
||
_ReleasePointerCapture(sender, args);
|
||
|
||
if (type == Windows::Devices::Input::PointerDeviceType::Mouse ||
|
||
type == Windows::Devices::Input::PointerDeviceType::Pen)
|
||
{
|
||
_interactivity.PointerReleased(TermControl::GetPressedMouseButtons(point),
|
||
TermControl::GetPointerUpdateKind(point),
|
||
ControlKeyStates(args.KeyModifiers()),
|
||
pixelPosition.to_core_point());
|
||
}
|
||
else if (type == Windows::Devices::Input::PointerDeviceType::Touch)
|
||
{
|
||
_interactivity.TouchReleased();
|
||
}
|
||
|
||
_TryStopAutoScroll(ptr.PointerId());
|
||
|
||
args.Handled(true);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Event handler for the PointerWheelChanged event. This is raised in
|
||
// response to mouse wheel changes. Depending upon what modifier keys are
|
||
// pressed, different actions will take place.
|
||
// - Primarily just takes the data from the PointerRoutedEventArgs and uses
|
||
// it to call _DoMouseWheel, see _DoMouseWheel for more details.
|
||
// Arguments:
|
||
// - args: the event args containing information about t`he mouse wheel event.
|
||
void TermControl::_MouseWheelHandler(const Windows::Foundation::IInspectable& /*sender*/,
|
||
const Input::PointerRoutedEventArgs& args)
|
||
{
|
||
if (_IsClosing())
|
||
{
|
||
return;
|
||
}
|
||
|
||
RestorePointerCursor.raise(*this, nullptr);
|
||
|
||
const auto point = args.GetCurrentPoint(*this);
|
||
// GH#10329 - we don't need to handle horizontal scrolls. Only vertical ones.
|
||
// So filter out the horizontal ones.
|
||
if (point.Properties().IsHorizontalMouseWheel())
|
||
{
|
||
return;
|
||
}
|
||
|
||
auto result = _interactivity.MouseWheel(ControlKeyStates{ args.KeyModifiers() },
|
||
point.Properties().MouseWheelDelta(),
|
||
_toTerminalOrigin(point.Position()).to_core_point(),
|
||
TermControl::GetPressedMouseButtons(point));
|
||
if (result)
|
||
{
|
||
args.Handled(true);
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - This is part of the solution to GH#979
|
||
// - Manually handle a scrolling event. This is used to help support
|
||
// scrolling on devices where the touchpad doesn't correctly handle
|
||
// scrolling inactive windows.
|
||
// Arguments:
|
||
// - location: the location of the mouse during this event. This location is
|
||
// relative to the origin of the control
|
||
// - delta: the mouse wheel delta that triggered this event.
|
||
// - state: the state for each of the mouse buttons individually (pressed/unpressed)
|
||
bool TermControl::OnMouseWheel(const Windows::Foundation::Point location,
|
||
const int32_t delta,
|
||
const bool leftButtonDown,
|
||
const bool midButtonDown,
|
||
const bool rightButtonDown)
|
||
{
|
||
const auto modifiers = _GetPressedModifierKeys();
|
||
|
||
Control::MouseButtonState state{};
|
||
WI_SetFlagIf(state, Control::MouseButtonState::IsLeftButtonDown, leftButtonDown);
|
||
WI_SetFlagIf(state, Control::MouseButtonState::IsMiddleButtonDown, midButtonDown);
|
||
WI_SetFlagIf(state, Control::MouseButtonState::IsRightButtonDown, rightButtonDown);
|
||
|
||
return _interactivity.MouseWheel(modifiers, delta, _toTerminalOrigin(location).to_core_point(), state);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Called in response to the core's TransparencyChanged event. We'll use
|
||
// this to update our background brush.
|
||
// - The Core should have already updated the TintOpacity and UseAcrylic
|
||
// properties in the _settings->
|
||
// Arguments:
|
||
// - <unused>
|
||
// Return Value:
|
||
// - <none>
|
||
winrt::fire_and_forget TermControl::_coreTransparencyChanged(IInspectable /*sender*/,
|
||
Control::TransparencyChangedEventArgs /*args*/)
|
||
{
|
||
co_await wil::resume_foreground(Dispatcher());
|
||
try
|
||
{
|
||
_changeBackgroundOpacity();
|
||
}
|
||
CATCH_LOG();
|
||
}
|
||
|
||
// Method Description:
|
||
// - Reset the font size of the terminal to its default size.
|
||
// Arguments:
|
||
// - none
|
||
void TermControl::ResetFontSize()
|
||
{
|
||
_core.ResetFontSize();
|
||
}
|
||
|
||
// Method Description:
|
||
// - Adjust the font size of the terminal control.
|
||
// Arguments:
|
||
// - fontSizeDelta: The amount to increase or decrease the font size by.
|
||
void TermControl::AdjustFontSize(float fontSizeDelta)
|
||
{
|
||
_core.AdjustFontSize(fontSizeDelta);
|
||
}
|
||
|
||
void TermControl::_ScrollbarChangeHandler(const Windows::Foundation::IInspectable& /*sender*/,
|
||
const Controls::Primitives::RangeBaseValueChangedEventArgs& args)
|
||
{
|
||
if (_isInternalScrollBarUpdate || _IsClosing())
|
||
{
|
||
// The update comes from ourselves, more specifically from the
|
||
// terminal. So we don't have to update the terminal because it
|
||
// already knows.
|
||
return;
|
||
}
|
||
|
||
const auto newValue = args.NewValue();
|
||
_interactivity.UpdateScrollbar(static_cast<float>(newValue));
|
||
|
||
// User input takes priority over terminal events so cancel
|
||
// any pending scroll bar update if the user scrolls.
|
||
_updateScrollBar->ModifyPending([](auto& update) {
|
||
update.newValue.reset();
|
||
});
|
||
}
|
||
|
||
// Method Description:
|
||
// - captures the pointer so that none of the other XAML elements respond to pointer events
|
||
// Arguments:
|
||
// - sender: XAML element that is interacting with pointer
|
||
// - args: pointer data (i.e.: mouse, touch)
|
||
// Return Value:
|
||
// - true if we successfully capture the pointer, false otherwise.
|
||
bool TermControl::_CapturePointer(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& args)
|
||
{
|
||
IUIElement uielem;
|
||
if (sender.try_as(uielem))
|
||
{
|
||
uielem.CapturePointer(args.Pointer());
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Method Description:
|
||
// - releases the captured pointer because we're done responding to XAML pointer events
|
||
// Arguments:
|
||
// - sender: XAML element that is interacting with pointer
|
||
// - args: pointer data (i.e.: mouse, touch)
|
||
// Return Value:
|
||
// - true if we release capture of the pointer, false otherwise.
|
||
bool TermControl::_ReleasePointerCapture(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& args)
|
||
{
|
||
IUIElement uielem;
|
||
if (sender.try_as(uielem))
|
||
{
|
||
uielem.ReleasePointerCapture(args.Pointer());
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Starts new pointer related auto scroll behavior, or continues existing one.
|
||
// Does nothing when there is already auto scroll associated with another pointer.
|
||
// Arguments:
|
||
// - pointerPoint: info about pointer that causes auto scroll. Pointer's position
|
||
// is later used to update selection.
|
||
// - scrollVelocity: target velocity of scrolling in characters / sec
|
||
void TermControl::_TryStartAutoScroll(const Windows::UI::Input::PointerPoint& pointerPoint, const double scrollVelocity)
|
||
{
|
||
// Allow only one pointer at the time
|
||
if (!_autoScrollingPointerPoint ||
|
||
_autoScrollingPointerPoint->PointerId() == pointerPoint.PointerId())
|
||
{
|
||
_autoScrollingPointerPoint = pointerPoint;
|
||
_autoScrollVelocity = scrollVelocity;
|
||
|
||
// If this is first time the auto scroll update is about to be called,
|
||
// kick-start it by initializing its time delta as if it started now
|
||
if (!_lastAutoScrollUpdateTime)
|
||
{
|
||
_lastAutoScrollUpdateTime = std::chrono::high_resolution_clock::now();
|
||
}
|
||
|
||
// Apparently this check is not necessary but greatly improves performance
|
||
if (!_autoScrollTimer.IsEnabled())
|
||
{
|
||
_autoScrollTimer.Start();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Stops auto scroll if it's active and is associated with supplied pointer id.
|
||
// Arguments:
|
||
// - pointerId: id of pointer for which to stop auto scroll
|
||
void TermControl::_TryStopAutoScroll(const uint32_t pointerId)
|
||
{
|
||
if (_autoScrollingPointerPoint &&
|
||
pointerId == _autoScrollingPointerPoint->PointerId())
|
||
{
|
||
_autoScrollingPointerPoint = std::nullopt;
|
||
_autoScrollVelocity = 0;
|
||
_lastAutoScrollUpdateTime = std::nullopt;
|
||
|
||
// Apparently this check is not necessary but greatly improves performance
|
||
if (_autoScrollTimer.IsEnabled())
|
||
{
|
||
_autoScrollTimer.Stop();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Called continuously to gradually scroll viewport when user is mouse
|
||
// selecting outside it (to 'follow' the cursor).
|
||
// Arguments:
|
||
// - none
|
||
void TermControl::_UpdateAutoScroll(const Windows::Foundation::IInspectable& /* sender */,
|
||
const Windows::Foundation::IInspectable& /* e */)
|
||
{
|
||
if (_autoScrollVelocity != 0)
|
||
{
|
||
const auto timeNow = std::chrono::high_resolution_clock::now();
|
||
|
||
if (_lastAutoScrollUpdateTime)
|
||
{
|
||
static constexpr auto microSecPerSec = 1000000.0;
|
||
const auto deltaTime = std::chrono::duration_cast<std::chrono::microseconds>(timeNow - *_lastAutoScrollUpdateTime).count() / microSecPerSec;
|
||
ScrollBar().Value(ScrollBar().Value() + _autoScrollVelocity * deltaTime);
|
||
|
||
if (_autoScrollingPointerPoint)
|
||
{
|
||
_SetEndSelectionPointAtCursor(_autoScrollingPointerPoint->Position());
|
||
}
|
||
}
|
||
|
||
_lastAutoScrollUpdateTime = timeNow;
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Event handler for the GotFocus event. This is used to...
|
||
// - enable accessibility notifications for this TermControl
|
||
// - start blinking the cursor when the window is focused
|
||
// - update the number of lines to scroll to the value set in the system
|
||
void TermControl::_GotFocusHandler(const Windows::Foundation::IInspectable& /* sender */,
|
||
const RoutedEventArgs& /* args */)
|
||
{
|
||
if (_IsClosing())
|
||
{
|
||
return;
|
||
}
|
||
|
||
_focused = true;
|
||
|
||
InputPane::GetForCurrentView().TryShow();
|
||
|
||
// GH#5421: Enable the UiaEngine before checking for the SearchBox
|
||
// That way, new selections are notified to automation clients.
|
||
// The _uiaEngine lives in _interactivity, so call into there to enable it.
|
||
|
||
if (_interactivity)
|
||
{
|
||
_interactivity.GotFocus();
|
||
}
|
||
|
||
// If the searchbox is focused, we don't want TSFInputControl to think
|
||
// it has focus so it doesn't intercept IME input. We also don't want the
|
||
// terminal's cursor to start blinking. So, we'll just return quickly here.
|
||
if (_searchBox && _searchBox->ContainsFocus())
|
||
{
|
||
return;
|
||
}
|
||
if (_cursorTimer)
|
||
{
|
||
// When the terminal focuses, show the cursor immediately
|
||
_core.CursorOn(_core.SelectionMode() != SelectionInteractionMode::Mark);
|
||
_cursorTimer.Start();
|
||
}
|
||
|
||
if (_blinkTimer)
|
||
{
|
||
_blinkTimer.Start();
|
||
}
|
||
|
||
// Only update the appearance here if an unfocused config exists - if an
|
||
// unfocused config does not exist then we never would have switched
|
||
// appearances anyway so there's no need to switch back upon gaining
|
||
// focus
|
||
if (_core.HasUnfocusedAppearance())
|
||
{
|
||
UpdateAppearance(_core.FocusedAppearance());
|
||
}
|
||
|
||
GetTSFHandle().Focus(&_tsfDataProvider);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Event handler for the LostFocus event. This is used to...
|
||
// - disable accessibility notifications for this TermControl
|
||
// - hide and stop blinking the cursor when the window loses focus.
|
||
void TermControl::_LostFocusHandler(const Windows::Foundation::IInspectable& /* sender */,
|
||
const RoutedEventArgs& /* args */)
|
||
{
|
||
if (_IsClosing())
|
||
{
|
||
return;
|
||
}
|
||
|
||
RestorePointerCursor.raise(*this, nullptr);
|
||
|
||
_focused = false;
|
||
|
||
// This will disable the accessibility notifications, because the
|
||
// UiaEngine lives in ControlInteractivity
|
||
if (_interactivity)
|
||
{
|
||
_interactivity.LostFocus();
|
||
}
|
||
|
||
if (_cursorTimer && !_displayCursorWhileBlurred())
|
||
{
|
||
_cursorTimer.Stop();
|
||
_core.CursorOn(false);
|
||
}
|
||
|
||
if (_blinkTimer)
|
||
{
|
||
_blinkTimer.Stop();
|
||
}
|
||
|
||
// Check if there is an unfocused config we should set the appearance to
|
||
// upon losing focus
|
||
if (_core.HasUnfocusedAppearance())
|
||
{
|
||
UpdateAppearance(_core.UnfocusedAppearance());
|
||
}
|
||
|
||
GetTSFHandle().Unfocus(&_tsfDataProvider);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Triggered when the swapchain changes size. We use this to resize the
|
||
// terminal buffers to match the new visible size.
|
||
// Arguments:
|
||
// - e: a SizeChangedEventArgs with the new dimensions of the SwapChainPanel
|
||
void TermControl::_SwapChainSizeChanged(const winrt::Windows::Foundation::IInspectable& /*sender*/,
|
||
const SizeChangedEventArgs& e)
|
||
{
|
||
if (!_initializedTerminal || _IsClosing())
|
||
{
|
||
return;
|
||
}
|
||
|
||
const auto newSize = e.NewSize();
|
||
_core.SizeChanged(newSize.Width, newSize.Height);
|
||
|
||
if (_automationPeer)
|
||
{
|
||
_automationPeer.UpdateControlBounds();
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Triggered when the swapchain changes DPI. When this happens, we're
|
||
// going to receive 3 events:
|
||
// - 1. First, a CompositionScaleChanged _for the original scale_. I don't
|
||
// know why this event happens first. **It also doesn't always happen.**
|
||
// However, when it does happen, it doesn't give us any useful
|
||
// information.
|
||
// - 2. Then, a SizeChanged. During that SizeChanged, either:
|
||
// - the CompositionScale will still be the original DPI. This happens
|
||
// when the control is visible as the DPI changes.
|
||
// - The CompositionScale will be the new DPI. This happens when the
|
||
// control wasn't focused as the window's DPI changed, so it only got
|
||
// these messages after XAML updated its scaling.
|
||
// - 3. Finally, a CompositionScaleChanged with the _new_ DPI.
|
||
// - 4. We'll usually get another SizeChanged some time after this last
|
||
// ScaleChanged. This usually seems to happen after something triggers
|
||
// the UI to re-layout, like hovering over the scrollbar. This event
|
||
// doesn't reliably happen immediately after a scale change, so we can't
|
||
// depend on it (despite the fact that both the scale and size state is
|
||
// definitely correct in it)
|
||
// - In the 3rd event, we're going to update our font size for the new DPI.
|
||
// At that point, we know how big the font should be for the new DPI, and
|
||
// how big the SwapChainPanel will be. If these sizes are different, we'll
|
||
// need to resize the buffer to fit in the new window.
|
||
// Arguments:
|
||
// - sender: The SwapChainPanel who's DPI changed. This is our _swapchainPanel.
|
||
// - args: This param is unused in the CompositionScaleChanged event.
|
||
void TermControl::_SwapChainScaleChanged(const Windows::UI::Xaml::Controls::SwapChainPanel& sender,
|
||
const Windows::Foundation::IInspectable& /*args*/)
|
||
{
|
||
const auto scaleX = sender.CompositionScaleX();
|
||
|
||
_core.ScaleChanged(scaleX);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Toggle the cursor on and off when called by the cursor blink timer.
|
||
// Arguments:
|
||
// - sender: not used
|
||
// - e: not used
|
||
void TermControl::_CursorTimerTick(const Windows::Foundation::IInspectable& /* sender */,
|
||
const Windows::Foundation::IInspectable& /* e */)
|
||
{
|
||
if (!_IsClosing())
|
||
{
|
||
_core.BlinkCursor();
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Toggle the blinking rendition state when called by the blink timer.
|
||
// Arguments:
|
||
// - sender: not used
|
||
// - e: not used
|
||
void TermControl::_BlinkTimerTick(const Windows::Foundation::IInspectable& /* sender */,
|
||
const Windows::Foundation::IInspectable& /* e */)
|
||
{
|
||
if (!_IsClosing())
|
||
{
|
||
_core.BlinkAttributeTick();
|
||
}
|
||
}
|
||
|
||
// 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 TermControl::_SetEndSelectionPointAtCursor(const Windows::Foundation::Point& cursorPosition)
|
||
{
|
||
_interactivity.SetEndSelectionPoint(_toTerminalOrigin(cursorPosition).to_core_point());
|
||
}
|
||
|
||
// 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 TermControl::_ScrollPositionChanged(const IInspectable& /*sender*/,
|
||
const Control::ScrollPositionChangedArgs& args)
|
||
{
|
||
ScrollBarUpdate update;
|
||
const auto hiddenContent = args.BufferSize() - args.ViewHeight();
|
||
update.newMaximum = hiddenContent;
|
||
update.newMinimum = 0;
|
||
update.newViewportSize = args.ViewHeight();
|
||
update.newValue = args.ViewTop();
|
||
|
||
_updateScrollBar->Run(update);
|
||
|
||
// if a selection marker is already visible,
|
||
// update the position of those markers
|
||
if (SelectionStartMarker().Visibility() == Visibility::Visible || SelectionEndMarker().Visibility() == Visibility::Visible)
|
||
{
|
||
_updateSelectionMarkers(nullptr, winrt::make<UpdateSelectionMarkersEventArgs>(false));
|
||
}
|
||
}
|
||
|
||
hstring TermControl::Title()
|
||
{
|
||
return _core.Title();
|
||
}
|
||
|
||
hstring TermControl::GetProfileName() const
|
||
{
|
||
return _core.Settings().ProfileName();
|
||
}
|
||
|
||
hstring TermControl::WorkingDirectory() const
|
||
{
|
||
return _core.WorkingDirectory();
|
||
}
|
||
|
||
bool TermControl::BracketedPasteEnabled() const noexcept
|
||
{
|
||
return _core.BracketedPasteEnabled();
|
||
}
|
||
|
||
// Method Description:
|
||
// - Given a copy-able selection, get the selected text from the buffer and send it to the
|
||
// Windows Clipboard (CascadiaWin32:main.cpp).
|
||
// - CopyOnSelect does NOT clear the selection
|
||
// Arguments:
|
||
// - dismissSelection: dismiss the text selection after copy
|
||
// - 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 TermControl::CopySelectionToClipboard(bool dismissSelection, bool singleLine, const Windows::Foundation::IReference<CopyFormat>& formats)
|
||
{
|
||
if (_IsClosing())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const auto successfulCopy = _interactivity.CopySelectionToClipboard(singleLine, formats);
|
||
|
||
if (dismissSelection)
|
||
{
|
||
_core.ClearSelection();
|
||
}
|
||
|
||
return successfulCopy;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Initiate a paste operation.
|
||
void TermControl::PasteTextFromClipboard()
|
||
{
|
||
_interactivity.RequestPasteTextFromClipboard();
|
||
}
|
||
|
||
void TermControl::SelectAll()
|
||
{
|
||
_core.SelectAll();
|
||
}
|
||
|
||
bool TermControl::ToggleBlockSelection()
|
||
{
|
||
return _core.ToggleBlockSelection();
|
||
}
|
||
|
||
void TermControl::ToggleMarkMode()
|
||
{
|
||
_core.ToggleMarkMode();
|
||
}
|
||
|
||
bool TermControl::SwitchSelectionEndpoint()
|
||
{
|
||
return _core.SwitchSelectionEndpoint();
|
||
}
|
||
|
||
bool TermControl::ExpandSelectionToWord()
|
||
{
|
||
return _core.ExpandSelectionToWord();
|
||
}
|
||
|
||
void TermControl::RestoreFromPath(winrt::hstring path)
|
||
{
|
||
_restorePath = std::move(path);
|
||
}
|
||
|
||
void TermControl::PersistToPath(const winrt::hstring& path) const
|
||
{
|
||
// Don't persist us if we weren't ever initialized. In that case, we
|
||
// never got an initial size, never instantiated a buffer, and didn't
|
||
// start the connection yet, so there's nothing for us to add here.
|
||
//
|
||
// If we were supposed to be restored from a path, then we don't need to
|
||
// do anything special here. We'll leave the original file untouched,
|
||
// and the next time we actually are initialized, we'll just use that
|
||
// file then.
|
||
if (_initializedTerminal)
|
||
{
|
||
winrt::get_self<ControlCore>(_core)->PersistToPath(path.c_str());
|
||
}
|
||
}
|
||
|
||
void TermControl::Close()
|
||
{
|
||
if (!_IsClosing())
|
||
{
|
||
_closing = true;
|
||
if (_automationPeer)
|
||
{
|
||
auto autoPeerImpl{ winrt::get_self<implementation::TermControlAutomationPeer>(_automationPeer) };
|
||
autoPeerImpl->Close();
|
||
}
|
||
|
||
RestorePointerCursor.raise(*this, nullptr);
|
||
|
||
_revokers = {};
|
||
|
||
// At the time of writing, closing the last tab of a window inexplicably
|
||
// does not lead to the destruction of the remaining TermControl instance(s).
|
||
// On Win10 we don't destroy window threads due to bugs in DesktopWindowXamlSource.
|
||
// In turn, we leak TermControl instances. This results in constant HWND messages
|
||
// while the thread is supposed to be idle. Stop these timers avoids this.
|
||
_autoScrollTimer.Stop();
|
||
_bellLightTimer.Stop();
|
||
_cursorTimer.Stop();
|
||
_blinkTimer.Stop();
|
||
|
||
// This is absolutely crucial, as the TSF code tries to hold a strong reference to _tsfDataProvider,
|
||
// but right now _tsfDataProvider implements IUnknown as a no-op. This ensures that TSF stops referencing us.
|
||
// ~TermControl() calls Close() so this should be safe.
|
||
GetTSFHandle().Unfocus(&_tsfDataProvider);
|
||
|
||
if (!_detached)
|
||
{
|
||
_interactivity.Close();
|
||
}
|
||
}
|
||
}
|
||
|
||
void TermControl::Detach()
|
||
{
|
||
_revokers = {};
|
||
|
||
Control::ControlInteractivity old{ nullptr };
|
||
std::swap(old, _interactivity);
|
||
old.Detach();
|
||
|
||
_detached = true;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Scrolls the viewport of the terminal and updates the scroll bar accordingly
|
||
// Arguments:
|
||
// - viewTop: the viewTop to scroll to
|
||
void TermControl::ScrollViewport(int viewTop)
|
||
{
|
||
ScrollBar().Value(viewTop);
|
||
}
|
||
|
||
int TermControl::ScrollOffset() const
|
||
{
|
||
return _core.ScrollOffset();
|
||
}
|
||
|
||
// Function Description:
|
||
// - Gets the height of the terminal in lines of text
|
||
// Return Value:
|
||
// - The height of the terminal in lines of text
|
||
int TermControl::ViewHeight() const
|
||
{
|
||
return _core.ViewHeight();
|
||
}
|
||
|
||
int TermControl::BufferHeight() const
|
||
{
|
||
return _core.BufferHeight();
|
||
}
|
||
|
||
// Function Description:
|
||
// - Determines how much space (in pixels) an app would need to reserve to
|
||
// create a control with the settings stored in the settings param. This
|
||
// accounts for things like the font size and face, the initialRows and
|
||
// initialCols, and scrollbar visibility. The returned sized is based upon
|
||
// the provided DPI value
|
||
// Arguments:
|
||
// - settings: A IControlSettings with the settings to get the pixel size of.
|
||
// - dpi: The DPI we should create the terminal at. This affects things such
|
||
// as font size, scrollbar and other control scaling, etc. Make sure the
|
||
// caller knows what monitor the control is about to appear on.
|
||
// - commandlineCols: Number of cols specified on the commandline
|
||
// - commandlineRows: Number of rows specified on the commandline
|
||
// Return Value:
|
||
// - a size containing the requested dimensions in pixels.
|
||
winrt::Windows::Foundation::Size TermControl::GetProposedDimensions(const IControlSettings& settings,
|
||
const uint32_t dpi,
|
||
int32_t commandlineCols,
|
||
int32_t commandlineRows)
|
||
{
|
||
// If the settings have negative or zero row or column counts, ignore those counts.
|
||
// (The lower TerminalCore layer also has upper bounds as well, but at this layer
|
||
// we may eventually impose different ones depending on how many pixels we can address.)
|
||
const auto cols = static_cast<float>(std::max(commandlineCols > 0 ?
|
||
commandlineCols :
|
||
settings.InitialCols(),
|
||
1));
|
||
const auto rows = static_cast<float>(std::max(commandlineRows > 0 ?
|
||
commandlineRows :
|
||
settings.InitialRows(),
|
||
1));
|
||
|
||
const winrt::Windows::Foundation::Size initialSize{ cols, rows };
|
||
|
||
return GetProposedDimensions(settings, dpi, initialSize);
|
||
}
|
||
|
||
// Function Description:
|
||
// - Determines how much space (in pixels) an app would need to reserve to
|
||
// create a control with the settings stored in the settings param. This
|
||
// accounts for things like the font size and face, the initialRows and
|
||
// initialCols, and scrollbar visibility. The returned sized is based upon
|
||
// the provided DPI value
|
||
// Arguments:
|
||
// - initialSizeInChars: The size to get the proposed dimensions for.
|
||
// - fontHeight: The font height to use to calculate the proposed size for.
|
||
// - fontWeight: The font weight to use to calculate the proposed size for.
|
||
// - fontFace: The font name to use to calculate the proposed size for.
|
||
// - scrollState: The ScrollbarState to use to calculate the proposed size for.
|
||
// - padding: The padding to use to calculate the proposed size for.
|
||
// - dpi: The DPI we should create the terminal at. This affects things such
|
||
// as font size, scrollbar and other control scaling, etc. Make sure the
|
||
// caller knows what monitor the control is about to appear on.
|
||
// Return Value:
|
||
// - a size containing the requested dimensions in pixels.
|
||
winrt::Windows::Foundation::Size TermControl::GetProposedDimensions(const IControlSettings& settings, const uint32_t dpi, const winrt::Windows::Foundation::Size& initialSizeInChars)
|
||
{
|
||
const auto cols = ::base::saturated_cast<int>(initialSizeInChars.Width);
|
||
const auto rows = ::base::saturated_cast<int>(initialSizeInChars.Height);
|
||
const auto fontSize = settings.FontSize();
|
||
const auto fontWeight = settings.FontWeight();
|
||
const auto fontFace = settings.FontFace();
|
||
const auto scrollState = settings.ScrollState();
|
||
const auto padding = settings.Padding();
|
||
|
||
// Initialize our font information.
|
||
// The font width doesn't terribly matter, we'll only be using the
|
||
// height to look it up
|
||
// The other params here also largely don't matter.
|
||
// The family is only used to determine if the font is truetype or
|
||
// not, but DX doesn't use that info at all.
|
||
// The Codepage is additionally not actually used by the DX engine at all.
|
||
FontInfoDesired desiredFont{ fontFace, 0, fontWeight.Weight, fontSize, CP_UTF8 };
|
||
FontInfo actualFont{ fontFace, 0, fontWeight.Weight, desiredFont.GetEngineSize(), CP_UTF8, false };
|
||
|
||
// Create a DX engine and initialize it with our font and DPI. We'll
|
||
// then use it to measure how much space the requested rows and columns
|
||
// will take up.
|
||
// TODO: MSFT:21254947 - use a static function to do this instead of
|
||
// instantiating a AtlasEngine.
|
||
// GH#10211 - UNDER NO CIRCUMSTANCE should this fail. If it does, the
|
||
// whole app will crash instantaneously on launch, which is no good.
|
||
const auto engine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
|
||
LOG_IF_FAILED(engine->UpdateDpi(dpi));
|
||
LOG_IF_FAILED(engine->UpdateFont(desiredFont, actualFont));
|
||
|
||
const auto scale = dpi / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
|
||
const auto actualFontSize = actualFont.GetSize();
|
||
|
||
// UWP XAML scrollbars aren't guaranteed to be the same size as the
|
||
// ComCtl scrollbars, but it's certainly close enough.
|
||
const auto scrollbarSize = GetSystemMetricsForDpi(SM_CXVSCROLL, dpi);
|
||
|
||
float width = cols * static_cast<float>(actualFontSize.width);
|
||
|
||
// Reserve additional space if scrollbar is intended to be visible
|
||
if (scrollState != ScrollbarState::Hidden)
|
||
{
|
||
width += scrollbarSize;
|
||
}
|
||
|
||
float height = rows * static_cast<float>(actualFontSize.height);
|
||
const auto thickness = ParseThicknessFromPadding(padding);
|
||
// GH#2061 - make sure to account for the size the padding _will be_ scaled to
|
||
width += scale * static_cast<float>(thickness.Left + thickness.Right);
|
||
height += scale * static_cast<float>(thickness.Top + thickness.Bottom);
|
||
|
||
return { width, height };
|
||
}
|
||
|
||
// Method Description:
|
||
// - Get the size of a single character of this control. The size is in
|
||
// _pixels_. If you want it in DIPs, you'll need to DIVIDE by the
|
||
// current display scaling.
|
||
// Arguments:
|
||
// - <none>
|
||
// Return Value:
|
||
// - The dimensions of a single character of this control, in DIPs
|
||
winrt::Windows::Foundation::Size TermControl::CharacterDimensions() const
|
||
{
|
||
return _core.FontSize();
|
||
}
|
||
|
||
// Method Description:
|
||
// - Get the absolute minimum size that this control can be resized to and
|
||
// still have 1x1 character visible. This includes the space needed for
|
||
// the scrollbar and the padding.
|
||
// Arguments:
|
||
// - <none>
|
||
// Return Value:
|
||
// - The minimum size that this terminal control can be resized to and still
|
||
// have a visible character.
|
||
winrt::Windows::Foundation::Size TermControl::MinimumSize()
|
||
{
|
||
if (_initializedTerminal)
|
||
{
|
||
const auto fontSize = _core.FontSize();
|
||
auto width = fontSize.Width;
|
||
auto height = fontSize.Height;
|
||
// Reserve additional space if scrollbar is intended to be visible
|
||
if (_core.Settings().ScrollState() != ScrollbarState::Hidden)
|
||
{
|
||
width += static_cast<float>(ScrollBar().ActualWidth());
|
||
}
|
||
|
||
// Account for the size of any padding
|
||
const auto padding = GetPadding();
|
||
width += static_cast<float>(padding.Left + padding.Right);
|
||
height += static_cast<float>(padding.Top + padding.Bottom);
|
||
|
||
return { width, height };
|
||
}
|
||
else
|
||
{
|
||
// If the terminal hasn't been initialized yet, then the font size will
|
||
// have dimensions {1, fontSize.height}, which can mess with consumers of
|
||
// this method. In that case, we'll need to pre-calculate the font
|
||
// width, before we actually have a renderer or swapchain.
|
||
const winrt::Windows::Foundation::Size minSize{ 1, 1 };
|
||
const auto scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel();
|
||
const auto dpi = ::base::saturated_cast<uint32_t>(USER_DEFAULT_SCREEN_DPI * scaleFactor);
|
||
return GetProposedDimensions(_core.Settings(), dpi, minSize);
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Adjusts given dimension (width or height) so that it aligns to the character grid.
|
||
// The snap is always downward.
|
||
// Arguments:
|
||
// - widthOrHeight: if true operates on width, otherwise on height
|
||
// - dimension: a dimension (width or height) to be snapped
|
||
// Return Value:
|
||
// - A dimension that would be aligned to the character grid.
|
||
float TermControl::SnapDimensionToGrid(const bool widthOrHeight, const float dimension)
|
||
{
|
||
const auto fontSize = _core.FontSize();
|
||
const auto fontDimension = widthOrHeight ? fontSize.Width : fontSize.Height;
|
||
|
||
const auto padding = GetPadding();
|
||
auto nonTerminalArea = gsl::narrow_cast<float>(widthOrHeight ?
|
||
padding.Left + padding.Right :
|
||
padding.Top + padding.Bottom);
|
||
|
||
if (widthOrHeight && _core.Settings().ScrollState() != ScrollbarState::Hidden)
|
||
{
|
||
nonTerminalArea += gsl::narrow_cast<float>(ScrollBar().ActualWidth());
|
||
}
|
||
|
||
const auto gridSize = dimension - nonTerminalArea;
|
||
const auto cells = floor(gridSize / fontDimension);
|
||
return cells * fontDimension + nonTerminalArea;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Forwards window visibility changing event down into the control core
|
||
// to eventually let the hosting PTY know whether the window is visible or
|
||
// not (which can be relevant to `::GetConsoleWindow()` calls.)
|
||
// Arguments:
|
||
// - showOrHide: Show is true; hide is false.
|
||
// Return Value:
|
||
// - <none>
|
||
void TermControl::WindowVisibilityChanged(const bool showOrHide)
|
||
{
|
||
_core.WindowVisibilityChanged(showOrHide);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Create XAML Thickness object based on padding props provided.
|
||
// Used for controlling the TermControl XAML Grid container's Padding prop.
|
||
// Arguments:
|
||
// - padding: 2D padding values
|
||
// Single Double value provides uniform padding
|
||
// Two Double values provide isometric horizontal & vertical padding
|
||
// Four Double values provide independent padding for 4 sides of the bounding rectangle
|
||
// Return Value:
|
||
// - Windows::UI::Xaml::Thickness object
|
||
Windows::UI::Xaml::Thickness TermControl::ParseThicknessFromPadding(const hstring padding)
|
||
{
|
||
const auto singleCharDelim = L',';
|
||
std::wstringstream tokenStream(padding.c_str());
|
||
std::wstring token;
|
||
uint8_t paddingPropIndex = 0;
|
||
std::array<double, 4> thicknessArr = {};
|
||
size_t* idx = nullptr;
|
||
|
||
// Get padding values till we run out of delimiter separated values in the stream
|
||
// or we hit max number of allowable values (= 4) for the bounding rectangle
|
||
// Non-numeral values detected will default to 0
|
||
// std::getline will not throw exception unless flags are set on the wstringstream
|
||
// std::stod will throw invalid_argument exception if the input is an invalid double value
|
||
// std::stod will throw out_of_range exception if the input value is more than DBL_MAX
|
||
try
|
||
{
|
||
for (; std::getline(tokenStream, token, singleCharDelim) && (paddingPropIndex < thicknessArr.size()); paddingPropIndex++)
|
||
{
|
||
// std::stod internally calls wcstod which handles whitespace prefix (which is ignored)
|
||
// & stops the scan when first char outside the range of radix is encountered
|
||
// We'll be permissive till the extent that stod function allows us to be by default
|
||
// Ex. a value like 100.3#535w2 will be read as 100.3, but ;df25 will fail
|
||
thicknessArr[paddingPropIndex] = std::stod(token, idx);
|
||
}
|
||
}
|
||
catch (...)
|
||
{
|
||
// If something goes wrong, even if due to a single bad padding value, we'll reset the index & return default 0 padding
|
||
paddingPropIndex = 0;
|
||
LOG_CAUGHT_EXCEPTION();
|
||
}
|
||
|
||
switch (paddingPropIndex)
|
||
{
|
||
case 1:
|
||
return ThicknessHelper::FromUniformLength(thicknessArr[0]);
|
||
case 2:
|
||
return ThicknessHelper::FromLengths(thicknessArr[0], thicknessArr[1], thicknessArr[0], thicknessArr[1]);
|
||
// No case for paddingPropIndex = 3, since it's not a norm to provide just Left, Top & Right padding values leaving out Bottom
|
||
case 4:
|
||
return ThicknessHelper::FromLengths(thicknessArr[0], thicknessArr[1], thicknessArr[2], thicknessArr[3]);
|
||
default:
|
||
return Thickness();
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Get the modifier keys that are currently pressed. This can be used to
|
||
// find out which modifiers (ctrl, alt, shift) are pressed in events that
|
||
// don't necessarily include that state.
|
||
// Return Value:
|
||
// - The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states.
|
||
ControlKeyStates TermControl::_GetPressedModifierKeys() noexcept
|
||
{
|
||
const auto window = CoreWindow::GetForCurrentThread();
|
||
// DONT USE
|
||
// != CoreVirtualKeyStates::None
|
||
// OR
|
||
// == CoreVirtualKeyStates::Down
|
||
// Sometimes with the key down, the state is Down | Locked.
|
||
// Sometimes with the key up, the state is Locked.
|
||
// IsFlagSet(Down) is the only correct solution.
|
||
|
||
struct KeyModifier
|
||
{
|
||
VirtualKey vkey;
|
||
ControlKeyStates flags;
|
||
};
|
||
|
||
constexpr std::array<KeyModifier, 7> modifiers{ {
|
||
{ VirtualKey::RightMenu, ControlKeyStates::RightAltPressed },
|
||
{ VirtualKey::LeftMenu, ControlKeyStates::LeftAltPressed },
|
||
{ VirtualKey::RightControl, ControlKeyStates::RightCtrlPressed },
|
||
{ VirtualKey::LeftControl, ControlKeyStates::LeftCtrlPressed },
|
||
{ VirtualKey::Shift, ControlKeyStates::ShiftPressed },
|
||
{ VirtualKey::RightWindows, ControlKeyStates::RightWinPressed },
|
||
{ VirtualKey::LeftWindows, ControlKeyStates::LeftWinPressed },
|
||
} };
|
||
|
||
ControlKeyStates flags;
|
||
|
||
for (const auto& mod : modifiers)
|
||
{
|
||
const auto state = window.GetKeyState(mod.vkey);
|
||
const auto isDown = WI_IsFlagSet(state, CoreVirtualKeyStates::Down);
|
||
|
||
if (isDown)
|
||
{
|
||
flags |= mod.flags;
|
||
}
|
||
}
|
||
|
||
constexpr std::array<KeyModifier, 3> modalities{ {
|
||
{ VirtualKey::CapitalLock, ControlKeyStates::CapslockOn },
|
||
{ VirtualKey::NumberKeyLock, ControlKeyStates::NumlockOn },
|
||
{ VirtualKey::Scroll, ControlKeyStates::ScrolllockOn },
|
||
} };
|
||
|
||
for (const auto& mod : modalities)
|
||
{
|
||
const auto state = window.GetKeyState(mod.vkey);
|
||
const auto isLocked = WI_IsFlagSet(state, CoreVirtualKeyStates::Locked);
|
||
|
||
if (isLocked)
|
||
{
|
||
flags |= mod.flags;
|
||
}
|
||
}
|
||
|
||
return flags;
|
||
}
|
||
|
||
til::point TermControl::_toControlOrigin(const til::point terminalPos)
|
||
{
|
||
const auto fontSize{ CharacterDimensions() };
|
||
|
||
// Convert text buffer cursor position to client coordinate position
|
||
// within the window. This point is in _pixels_
|
||
const auto clientCursorPosX = terminalPos.x * fontSize.Width;
|
||
const auto clientCursorPosY = terminalPos.y * fontSize.Height;
|
||
|
||
// Get scale factor for view
|
||
const auto scaleFactor = SwapChainPanel().CompositionScaleX();
|
||
|
||
const auto clientCursorInDipsX = clientCursorPosX / scaleFactor;
|
||
const auto clientCursorInDipsY = clientCursorPosY / scaleFactor;
|
||
|
||
auto padding{ GetPadding() };
|
||
til::point relativeToOrigin{ til::math::rounding,
|
||
clientCursorInDipsX + padding.Left,
|
||
clientCursorInDipsY + padding.Top };
|
||
return relativeToOrigin;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Gets the corresponding viewport pixel position for the cursor
|
||
// by excluding the padding.
|
||
// Arguments:
|
||
// - cursorPosition: the (x,y) position of a given cursor (i.e.: mouse cursor).
|
||
// NOTE: origin (0,0) is top-left.
|
||
// Return Value:
|
||
// - the corresponding viewport terminal position (in pixels) for the given Point parameter
|
||
const til::point TermControl::_toTerminalOrigin(winrt::Windows::Foundation::Point cursorPosition)
|
||
{
|
||
// cursorPosition is DIPs, relative to SwapChainPanel origin
|
||
const til::point cursorPosInDIPs{ til::math::rounding, cursorPosition };
|
||
const til::size marginsInDips{ til::math::rounding, GetPadding().Left, GetPadding().Top };
|
||
|
||
// This point is the location of the cursor within the actual grid of characters, in DIPs
|
||
const auto relativeToMarginInDIPs = cursorPosInDIPs - marginsInDips;
|
||
|
||
// Convert it to pixels
|
||
const auto scale = SwapChainPanel().CompositionScaleX();
|
||
const til::point relativeToMarginInPixels{
|
||
til::math::flooring,
|
||
relativeToMarginInDIPs.x * scale,
|
||
relativeToMarginInDIPs.y * scale,
|
||
};
|
||
|
||
return relativeToMarginInPixels;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Calculates speed of single axis of auto scrolling. It has to allow for both
|
||
// fast and precise selection.
|
||
// Arguments:
|
||
// - cursorDistanceFromBorder: distance from viewport border to cursor, in pixels. Must be non-negative.
|
||
// Return Value:
|
||
// - positive speed in characters / sec
|
||
double TermControl::_GetAutoScrollSpeed(double cursorDistanceFromBorder) const
|
||
{
|
||
// The numbers below just feel well, feel free to change.
|
||
// TODO: Maybe account for space beyond border that user has available
|
||
return std::pow(cursorDistanceFromBorder, 2.0) / 25.0 + 2.0;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Async handler for the "Drop" event. If a file was dropped onto our
|
||
// root, we'll try to get the path of the file dropped onto us, and write
|
||
// the full path of the file to our terminal connection. Like conhost, if
|
||
// the path contains a space, we'll wrap the path in quotes.
|
||
// - Unlike conhost, if multiple files are dropped onto the terminal, we'll
|
||
// write all the paths to the terminal, separated by spaces.
|
||
// Arguments:
|
||
// - e: The DragEventArgs from the Drop event
|
||
// Return Value:
|
||
// - <none>
|
||
winrt::fire_and_forget TermControl::_DragDropHandler(Windows::Foundation::IInspectable /*sender*/,
|
||
DragEventArgs e)
|
||
{
|
||
if (_IsClosing())
|
||
{
|
||
co_return;
|
||
}
|
||
|
||
if (e.DataView().Contains(StandardDataFormats::ApplicationLink()))
|
||
{
|
||
try
|
||
{
|
||
auto link{ co_await e.DataView().GetApplicationLinkAsync() };
|
||
_pasteTextWithBroadcast(link.AbsoluteUri());
|
||
}
|
||
CATCH_LOG();
|
||
}
|
||
else if (e.DataView().Contains(StandardDataFormats::WebLink()))
|
||
{
|
||
try
|
||
{
|
||
auto link{ co_await e.DataView().GetWebLinkAsync() };
|
||
_pasteTextWithBroadcast(link.AbsoluteUri());
|
||
}
|
||
CATCH_LOG();
|
||
}
|
||
else if (e.DataView().Contains(StandardDataFormats::Text()))
|
||
{
|
||
try
|
||
{
|
||
auto text{ co_await e.DataView().GetTextAsync() };
|
||
_pasteTextWithBroadcast(text);
|
||
}
|
||
CATCH_LOG();
|
||
}
|
||
// StorageItem must be last. Some applications put hybrid data format items
|
||
// in a drop message and we'll eat a crash when we request them.
|
||
// Those applications usually include Text as well, so having storage items
|
||
// last makes sure we'll hit text before getting to them.
|
||
else if (e.DataView().Contains(StandardDataFormats::StorageItems()))
|
||
{
|
||
Windows::Foundation::Collections::IVectorView<Windows::Storage::IStorageItem> items;
|
||
try
|
||
{
|
||
items = co_await e.DataView().GetStorageItemsAsync();
|
||
}
|
||
CATCH_LOG();
|
||
|
||
if (items.Size() > 0)
|
||
{
|
||
std::vector<std::wstring> fullPaths;
|
||
|
||
// GH#14628: Workaround for GetStorageItemsAsync() only returning 16 items
|
||
// at most when dragging and dropping from archives (zip, 7z, rar, etc.)
|
||
if (items.Size() == 16 && e.DataView().Contains(winrt::hstring{ L"FileDrop" }))
|
||
{
|
||
auto fileDropData = co_await e.DataView().GetDataAsync(winrt::hstring{ L"FileDrop" });
|
||
if (fileDropData != nullptr)
|
||
{
|
||
auto stream = fileDropData.as<IRandomAccessStream>();
|
||
stream.Seek(0);
|
||
|
||
const uint32_t streamSize = gsl::narrow_cast<uint32_t>(stream.Size());
|
||
const Buffer buf(streamSize);
|
||
const auto buffer = co_await stream.ReadAsync(buf, streamSize, InputStreamOptions::None);
|
||
|
||
const HGLOBAL hGlobal = buffer.data();
|
||
const auto count = DragQueryFileW(static_cast<HDROP>(hGlobal), 0xFFFFFFFF, nullptr, 0);
|
||
fullPaths.reserve(count);
|
||
|
||
for (unsigned int i = 0; i < count; i++)
|
||
{
|
||
std::wstring path;
|
||
path.resize(wil::max_path_length);
|
||
const auto charsCopied = DragQueryFileW(static_cast<HDROP>(hGlobal), i, path.data(), wil::max_path_length);
|
||
|
||
if (charsCopied > 0)
|
||
{
|
||
path.resize(charsCopied);
|
||
fullPaths.emplace_back(std::move(path));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
fullPaths.reserve(items.Size());
|
||
for (const auto& item : items)
|
||
{
|
||
fullPaths.emplace_back(item.Path());
|
||
}
|
||
}
|
||
|
||
std::wstring allPathsString;
|
||
for (auto& fullPath : fullPaths)
|
||
{
|
||
// Join the paths with spaces
|
||
if (!allPathsString.empty())
|
||
{
|
||
allPathsString += L" ";
|
||
}
|
||
|
||
// Fix path for WSL
|
||
// In the fullness of time, we should likely plumb this up
|
||
// to the TerminalApp layer, and have it make the decision
|
||
// if this control should have its path mangled (and do the
|
||
// mangling), rather than exposing the source concept to the
|
||
// Control layer.
|
||
//
|
||
// However, it's likely that the control layer may need to
|
||
// know about the source anyways in the future, to support
|
||
// GH#3158
|
||
const auto isWSL = _interactivity.ManglePathsForWsl();
|
||
|
||
if (isWSL)
|
||
{
|
||
std::replace(fullPath.begin(), fullPath.end(), L'\\', L'/');
|
||
|
||
if (fullPath.size() >= 2 && fullPath.at(1) == L':')
|
||
{
|
||
// C:/foo/bar -> Cc/foo/bar
|
||
fullPath.at(1) = til::tolower_ascii(fullPath.at(0));
|
||
// Cc/foo/bar -> /mnt/c/foo/bar
|
||
fullPath.replace(0, 1, L"/mnt/");
|
||
}
|
||
else
|
||
{
|
||
static constexpr std::wstring_view wslPathPrefixes[] = { L"//wsl.localhost/", L"//wsl$/" };
|
||
for (auto prefix : wslPathPrefixes)
|
||
{
|
||
if (til::starts_with(fullPath, prefix))
|
||
{
|
||
if (const auto idx = fullPath.find(L'/', prefix.size()); idx != std::wstring::npos)
|
||
{
|
||
// //wsl.localhost/Ubuntu-18.04/foo/bar -> /foo/bar
|
||
fullPath.erase(0, idx);
|
||
}
|
||
else
|
||
{
|
||
// //wsl.localhost/Ubuntu-18.04 -> /
|
||
fullPath = L"/";
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const auto quotesNeeded = isWSL || fullPath.find(L' ') != std::wstring::npos;
|
||
const auto quotesChar = isWSL ? L'\'' : L'"';
|
||
|
||
// Append fullPath and also wrap it in quotes if needed
|
||
if (quotesNeeded)
|
||
{
|
||
allPathsString.push_back(quotesChar);
|
||
}
|
||
allPathsString.append(fullPath);
|
||
if (quotesNeeded)
|
||
{
|
||
allPathsString.push_back(quotesChar);
|
||
}
|
||
}
|
||
|
||
_pasteTextWithBroadcast(winrt::hstring{ allPathsString });
|
||
}
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Paste this text, and raise a StringSent, to potentially broadcast this
|
||
// text to other controls in the app. For certain interactions, like
|
||
// drag/dropping a file, we want to act like we "pasted" the text (even if
|
||
// the text didn't come from the clipboard). This lets those interactions
|
||
// broadcast as well.
|
||
void TermControl::_pasteTextWithBroadcast(const winrt::hstring& text)
|
||
{
|
||
// only broadcast if there's an actual listener. Saves the overhead of some object creation.
|
||
if (StringSent)
|
||
{
|
||
StringSent.raise(*this, winrt::make<StringSentEventArgs>(text));
|
||
}
|
||
_core.PasteText(text);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Handle the DragOver event. We'll signal that the drag operation we
|
||
// support is the "copy" operation, and we'll also customize the
|
||
// appearance of the drag-drop UI, by removing the preview and setting a
|
||
// custom caption. For more information, see
|
||
// https://docs.microsoft.com/en-us/windows/uwp/design/input/drag-and-drop#customize-the-ui
|
||
// Arguments:
|
||
// - e: The DragEventArgs from the DragOver event
|
||
// Return Value:
|
||
// - <none>
|
||
void TermControl::_DragOverHandler(const Windows::Foundation::IInspectable& /*sender*/,
|
||
const DragEventArgs& e)
|
||
{
|
||
if (_IsClosing())
|
||
{
|
||
return;
|
||
}
|
||
|
||
// We can only handle drag/dropping StorageItems (files) and plain Text
|
||
// currently. If the format on the clipboard is anything else, returning
|
||
// early here will prevent the drag/drop from doing anything.
|
||
if (!(e.DataView().Contains(StandardDataFormats::StorageItems()) ||
|
||
e.DataView().Contains(StandardDataFormats::Text())))
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Make sure to set the AcceptedOperation, so that we can later receive the path in the Drop event
|
||
e.AcceptedOperation(DataPackageOperation::Copy);
|
||
|
||
// Sets custom UI text
|
||
if (e.DataView().Contains(StandardDataFormats::StorageItems()))
|
||
{
|
||
e.DragUIOverride().Caption(RS_(L"DragFileCaption"));
|
||
}
|
||
else if (e.DataView().Contains(StandardDataFormats::Text()))
|
||
{
|
||
e.DragUIOverride().Caption(RS_(L"DragTextCaption"));
|
||
}
|
||
|
||
// Sets if the caption is visible
|
||
e.DragUIOverride().IsCaptionVisible(true);
|
||
// Sets if the dragged content is visible
|
||
e.DragUIOverride().IsContentVisible(false);
|
||
// Sets if the glyph is visible
|
||
e.DragUIOverride().IsGlyphVisible(false);
|
||
}
|
||
|
||
// Method description:
|
||
// - Checks if the uri is valid and sends an event if so
|
||
// Arguments:
|
||
// - The uri
|
||
winrt::fire_and_forget TermControl::_HyperlinkHandler(IInspectable /*sender*/,
|
||
Control::OpenHyperlinkEventArgs args)
|
||
{
|
||
// Save things we need to resume later.
|
||
auto strongThis{ get_strong() };
|
||
|
||
// Pop the rest of this function to the tail of the UI thread
|
||
// Just in case someone was holding a lock when they called us and
|
||
// the handlers decide to do something that take another lock
|
||
// (like ShellExecute pumping our messaging thread...GH#7994)
|
||
co_await winrt::resume_foreground(Dispatcher());
|
||
|
||
OpenHyperlink.raise(*strongThis, args);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Produces the error dialog that notifies the user that rendering cannot proceed.
|
||
winrt::fire_and_forget TermControl::_RendererEnteredErrorState(IInspectable /*sender*/,
|
||
IInspectable /*args*/)
|
||
{
|
||
auto strongThis{ get_strong() };
|
||
co_await winrt::resume_foreground(Dispatcher()); // pop up onto the UI thread
|
||
|
||
if (auto loadedUiElement{ FindName(L"RendererFailedNotice") })
|
||
{
|
||
if (auto uiElement{ loadedUiElement.try_as<::winrt::Windows::UI::Xaml::UIElement>() })
|
||
{
|
||
uiElement.Visibility(Visibility::Visible);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Responds to the Click event on the button that will re-enable the renderer.
|
||
void TermControl::_RenderRetryButton_Click(const IInspectable& /*sender*/, const IInspectable& /*args*/)
|
||
{
|
||
// It's already loaded if we get here, so just hide it.
|
||
RendererFailedNotice().Visibility(Visibility::Collapsed);
|
||
_core.ResumeRendering();
|
||
}
|
||
|
||
IControlSettings TermControl::Settings() const
|
||
{
|
||
return _core.Settings();
|
||
}
|
||
|
||
Windows::Foundation::IReference<winrt::Windows::UI::Color> TermControl::TabColor() noexcept
|
||
{
|
||
// NOTE TO FUTURE READERS: TabColor is down in the Core for the
|
||
// hypothetical future where we allow an application to set the tab
|
||
// color with VT sequences like they're currently allowed to with the
|
||
// title.
|
||
return _core.TabColor();
|
||
}
|
||
|
||
// Method Description:
|
||
// - Gets the internal taskbar state value
|
||
// Return Value:
|
||
// - The taskbar state of this control
|
||
const uint64_t TermControl::TaskbarState() const noexcept
|
||
{
|
||
return _core.TaskbarState();
|
||
}
|
||
|
||
// Method Description:
|
||
// - Gets the internal taskbar progress value
|
||
// Return Value:
|
||
// - The taskbar progress of this control
|
||
const uint64_t TermControl::TaskbarProgress() const noexcept
|
||
{
|
||
return _core.TaskbarProgress();
|
||
}
|
||
|
||
void TermControl::BellLightOn()
|
||
{
|
||
// Initialize the animation if it does not exist
|
||
// We only initialize here instead of in the ctor because depending on the bell style setting,
|
||
// we may never need this animation
|
||
if (!_bellLightAnimation && !_isBackgroundLight)
|
||
{
|
||
_bellLightAnimation = Window::Current().Compositor().CreateScalarKeyFrameAnimation();
|
||
// Add key frames and a duration to our bell light animation
|
||
_bellLightAnimation.InsertKeyFrame(0.0f, 4.0f);
|
||
_bellLightAnimation.InsertKeyFrame(1.0f, 1.9f);
|
||
_bellLightAnimation.Duration(winrt::Windows::Foundation::TimeSpan(std::chrono::milliseconds(TerminalWarningBellInterval)));
|
||
}
|
||
|
||
// Likewise, initialize the dark version of the animation only if required
|
||
if (!_bellDarkAnimation && _isBackgroundLight)
|
||
{
|
||
_bellDarkAnimation = Window::Current().Compositor().CreateScalarKeyFrameAnimation();
|
||
// reversing the order of the intensity values produces a similar effect as the light version
|
||
_bellDarkAnimation.InsertKeyFrame(0.0f, 1.0f);
|
||
_bellDarkAnimation.InsertKeyFrame(1.0f, 2.0f);
|
||
_bellDarkAnimation.Duration(winrt::Windows::Foundation::TimeSpan(std::chrono::milliseconds(TerminalWarningBellInterval)));
|
||
}
|
||
|
||
Windows::Foundation::Numerics::float2 zeroSize{ 0, 0 };
|
||
// If the grid has 0 size or if the bell timer is
|
||
// already active, do nothing
|
||
if (RootGrid().ActualSize() != zeroSize && !_bellLightTimer.IsEnabled())
|
||
{
|
||
_bellLightTimer.Interval(std::chrono::milliseconds(TerminalWarningBellInterval));
|
||
_bellLightTimer.Tick({ get_weak(), &TermControl::_BellLightOff });
|
||
_bellLightTimer.Start();
|
||
|
||
// Switch on the light and animate the intensity to fade out
|
||
VisualBellLight::SetIsTarget(RootGrid(), true);
|
||
|
||
if (_isBackgroundLight)
|
||
{
|
||
BellLight().CompositionLight().StartAnimation(L"Intensity", _bellDarkAnimation);
|
||
}
|
||
else
|
||
{
|
||
BellLight().CompositionLight().StartAnimation(L"Intensity", _bellLightAnimation);
|
||
}
|
||
}
|
||
}
|
||
|
||
void TermControl::_BellLightOff(const Windows::Foundation::IInspectable& /* sender */,
|
||
const Windows::Foundation::IInspectable& /* e */)
|
||
{
|
||
// Stop the timer and switch off the light
|
||
_bellLightTimer.Stop();
|
||
|
||
if (!_IsClosing())
|
||
{
|
||
VisualBellLight::SetIsTarget(RootGrid(), false);
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Checks whether the control is in a read-only mode (in this mode node input is sent to connection).
|
||
// Return Value:
|
||
// - True if the mode is read-only
|
||
bool TermControl::ReadOnly() const noexcept
|
||
{
|
||
return _core.IsInReadOnlyMode();
|
||
}
|
||
|
||
// Method Description:
|
||
// - Toggles the read-only flag, raises event describing the value change
|
||
void TermControl::ToggleReadOnly()
|
||
{
|
||
_core.ToggleReadOnlyMode();
|
||
ReadOnlyChanged.raise(*this, winrt::box_value(_core.IsInReadOnlyMode()));
|
||
}
|
||
|
||
// Method Description:
|
||
// - Sets the read-only flag, raises event describing the value change
|
||
void TermControl::SetReadOnly(const bool readOnlyState)
|
||
{
|
||
_core.SetReadOnlyMode(readOnlyState);
|
||
ReadOnlyChanged.raise(*this, winrt::box_value(_core.IsInReadOnlyMode()));
|
||
}
|
||
|
||
// Method Description:
|
||
// - Handle a mouse exited event, specifically clearing last hovered cell
|
||
// and removing selection from hyper link if exists
|
||
// Arguments:
|
||
// - sender: not used
|
||
// - args: event data
|
||
void TermControl::_PointerExitedHandler(const Windows::Foundation::IInspectable& /*sender*/,
|
||
const Windows::UI::Xaml::Input::PointerRoutedEventArgs& /*e*/)
|
||
{
|
||
_core.ClearHoveredCell();
|
||
}
|
||
|
||
void TermControl::_hoveredHyperlinkChanged(const IInspectable& /*sender*/, const IInspectable& /*args*/)
|
||
{
|
||
const auto lastHoveredCell = _core.HoveredCell();
|
||
if (!lastHoveredCell)
|
||
{
|
||
return;
|
||
}
|
||
|
||
auto uriText = _core.HoveredUriText();
|
||
if (uriText.empty())
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Attackers abuse Unicode characters that happen to look similar to ASCII characters. Cyrillic for instance has
|
||
// its own glyphs for а, с, е, о, р, х, and у that look practically identical to their ASCII counterparts.
|
||
// This is called an "IDN homoglyph attack".
|
||
//
|
||
// But outright showing Punycode URIs only is similarly flawed as they can end up looking similar to valid ASCII URIs.
|
||
// xn--cnn.com for instance looks confusingly similar to cnn.com, but actually represents U+407E.
|
||
//
|
||
// An optimal solution would detect any URI that contains homoglyphs and show them in their Punycode form.
|
||
// Such a detector however is not quite trivial and requires constant maintenance, which this project's
|
||
// maintainers aren't currently well equipped to handle. As such we do the next best thing and show the
|
||
// Punycode encoding side-by-side with the Unicode string for any IDN.
|
||
try
|
||
{
|
||
// DisplayUri/Iri drop authentication credentials, which is probably great, but AbsoluteCanonicalUri()
|
||
// is the only getter that returns a punycode encoding of the URL. AbsoluteUri() is the only possible
|
||
// counterpart, but as the name indicates, we'll end up hitting the != below for any non-canonical URL.
|
||
//
|
||
// This issue can be fixed by using the IUrl API from urlmon.h directly, which the WinRT API simply wraps.
|
||
// IUrl is a very complex system with a ton of useful functionality, but we don't rely on it (neither WinRT),
|
||
// so we could alternatively use its underlying API in wininet.h (InternetCrackUrlW, etc.).
|
||
// That API however is rather difficult to use for such seldom executed code.
|
||
const Windows::Foundation::Uri uri{ uriText };
|
||
const auto unicode = uri.AbsoluteUri();
|
||
const auto punycode = uri.AbsoluteCanonicalUri();
|
||
|
||
if (punycode != unicode)
|
||
{
|
||
const auto text = fmt::format(FMT_COMPILE(L"{}\n({})"), punycode, unicode);
|
||
uriText = winrt::hstring{ text };
|
||
}
|
||
}
|
||
catch (...)
|
||
{
|
||
uriText = RS_(L"InvalidUri");
|
||
}
|
||
|
||
const auto panel = SwapChainPanel();
|
||
const auto scale = panel.CompositionScaleX();
|
||
const auto offset = panel.ActualOffset();
|
||
|
||
// Update the tooltip with the URI
|
||
HoveredUri().Text(uriText);
|
||
|
||
// Set the border thickness so it covers the entire cell
|
||
const auto charSizeInPixels = CharacterDimensions();
|
||
const auto htInDips = charSizeInPixels.Height / scale;
|
||
const auto wtInDips = charSizeInPixels.Width / scale;
|
||
const Thickness newThickness{ wtInDips, htInDips, 0, 0 };
|
||
HyperlinkTooltipBorder().BorderThickness(newThickness);
|
||
|
||
// Compute the location of the top left corner of the cell in DIPS
|
||
const til::point locationInDIPs{ _toPosInDips(lastHoveredCell.Value()) };
|
||
|
||
// Move the border to the top left corner of the cell
|
||
OverlayCanvas().SetLeft(HyperlinkTooltipBorder(), locationInDIPs.x - offset.x);
|
||
OverlayCanvas().SetTop(HyperlinkTooltipBorder(), locationInDIPs.y - offset.y);
|
||
}
|
||
|
||
winrt::fire_and_forget TermControl::_updateSelectionMarkers(IInspectable /*sender*/, Control::UpdateSelectionMarkersEventArgs args)
|
||
{
|
||
auto weakThis{ get_weak() };
|
||
co_await resume_foreground(Dispatcher());
|
||
if (weakThis.get() && args)
|
||
{
|
||
if (_core.HasSelection() && !args.ClearMarkers())
|
||
{
|
||
// retrieve all of the necessary selection marker data
|
||
// from the TerminalCore layer under one lock to improve performance
|
||
const auto markerData{ _core.SelectionInfo() };
|
||
|
||
// lambda helper function that can be used to display a selection marker
|
||
// - targetEnd: if true, target the "end" selection marker. Otherwise, target "start".
|
||
auto displayMarker = [&](bool targetEnd) {
|
||
const auto flipMarker{ targetEnd ? markerData.EndAtRightBoundary : markerData.StartAtLeftBoundary };
|
||
const auto& marker{ targetEnd ? SelectionEndMarker() : SelectionStartMarker() };
|
||
|
||
// Ensure the marker is oriented properly
|
||
// (i.e. if start is at the beginning of the buffer, it should be flipped)
|
||
//
|
||
// Note - This RenderTransform might not be a
|
||
// ScaleTransform, if we haven't had a _coreFontSizeChanged
|
||
// handled yet, because that's the first place we set the
|
||
// RenderTransform
|
||
if (const auto& transform{ marker.RenderTransform().try_as<Windows::UI::Xaml::Media::ScaleTransform>() })
|
||
{
|
||
transform.ScaleX(std::abs(transform.ScaleX()) * (flipMarker ? -1.0 : 1.0));
|
||
marker.RenderTransform(transform);
|
||
}
|
||
|
||
// Compute the location of the top left corner of the cell in DIPS
|
||
auto terminalPos{ targetEnd ? markerData.EndPos : markerData.StartPos };
|
||
if (flipMarker)
|
||
{
|
||
// When we flip the marker, a negative scaling makes us be one cell-width to the left.
|
||
// Add one to the viewport pos' x-coord to fix that.
|
||
terminalPos.X += 1;
|
||
}
|
||
const til::point locationInDIPs{ _toPosInDips(terminalPos) };
|
||
|
||
// Move the marker to the top left corner of the cell
|
||
SelectionCanvas().SetLeft(marker,
|
||
(locationInDIPs.x - SwapChainPanel().ActualOffset().x));
|
||
SelectionCanvas().SetTop(marker,
|
||
(locationInDIPs.y - SwapChainPanel().ActualOffset().y));
|
||
marker.Visibility(Visibility::Visible);
|
||
};
|
||
|
||
// show/update selection markers
|
||
// figure out which endpoint to move, get it and the relevant icon (hide the other icon)
|
||
const auto movingEnd{ WI_IsFlagSet(markerData.Endpoint, SelectionEndpointTarget::End) };
|
||
const auto selectionAnchor{ movingEnd ? markerData.EndPos : markerData.StartPos };
|
||
const auto& marker{ movingEnd ? SelectionEndMarker() : SelectionStartMarker() };
|
||
const auto& otherMarker{ movingEnd ? SelectionStartMarker() : SelectionEndMarker() };
|
||
if (selectionAnchor.Y < 0 || selectionAnchor.Y >= _core.ViewHeight())
|
||
{
|
||
// if the endpoint is outside of the viewport,
|
||
// just hide the markers
|
||
marker.Visibility(Visibility::Collapsed);
|
||
otherMarker.Visibility(Visibility::Collapsed);
|
||
co_return;
|
||
}
|
||
else if (WI_AreAllFlagsSet(markerData.Endpoint, SelectionEndpointTarget::Start | SelectionEndpointTarget::End))
|
||
{
|
||
// display both markers
|
||
displayMarker(true);
|
||
displayMarker(false);
|
||
}
|
||
else
|
||
{
|
||
// display one marker,
|
||
// but hide the other
|
||
displayMarker(movingEnd);
|
||
otherMarker.Visibility(Visibility::Collapsed);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// hide selection markers
|
||
SelectionStartMarker().Visibility(Visibility::Collapsed);
|
||
SelectionEndMarker().Visibility(Visibility::Collapsed);
|
||
}
|
||
}
|
||
}
|
||
|
||
til::point TermControl::_toPosInDips(const Core::Point terminalCellPos)
|
||
{
|
||
const til::point terminalPos{ terminalCellPos };
|
||
const til::size marginsInDips{ til::math::rounding, GetPadding().Left, GetPadding().Top };
|
||
const til::size fontSize{ til::math::rounding, _core.FontSize() };
|
||
const til::point posInPixels{ terminalPos * fontSize };
|
||
const auto scale{ SwapChainPanel().CompositionScaleX() };
|
||
const til::point posInDIPs{ til::math::flooring, posInPixels.x / scale, posInPixels.y / scale };
|
||
return posInDIPs + marginsInDips;
|
||
}
|
||
|
||
void TermControl::_coreFontSizeChanged(const IInspectable& /*sender*/,
|
||
const Control::FontSizeChangedArgs& args)
|
||
{
|
||
// scale the selection markers to be the size of a cell
|
||
auto scaleMarker = [args, dpiScale{ SwapChainPanel().CompositionScaleX() }](const Windows::UI::Xaml::Shapes::Path& shape) {
|
||
// The selection markers were designed to be 5x14 in size,
|
||
// so use those dimensions below for the scaling
|
||
const auto scaleX = args.Width() / 5.0 / dpiScale;
|
||
const auto scaleY = args.Height() / 14.0 / dpiScale;
|
||
|
||
Windows::UI::Xaml::Media::ScaleTransform transform;
|
||
transform.ScaleX(scaleX);
|
||
transform.ScaleY(scaleY);
|
||
shape.RenderTransform(transform);
|
||
|
||
// now hide the shape
|
||
shape.Visibility(Visibility::Collapsed);
|
||
};
|
||
scaleMarker(SelectionStartMarker());
|
||
scaleMarker(SelectionEndMarker());
|
||
}
|
||
|
||
void TermControl::_coreRaisedNotice(const IInspectable& /*sender*/,
|
||
const Control::NoticeEventArgs& eventArgs)
|
||
{
|
||
// Don't try to inspect the core here. The Core might be raising this
|
||
// while it's holding its write lock. If the handlers calls back to some
|
||
// method on the TermControl on the same thread, and _that_ method calls
|
||
// to ControlCore, we might be in danger of deadlocking.
|
||
RaiseNotice.raise(*this, eventArgs);
|
||
}
|
||
|
||
Control::MouseButtonState TermControl::GetPressedMouseButtons(const winrt::Windows::UI::Input::PointerPoint point)
|
||
{
|
||
Control::MouseButtonState state{};
|
||
WI_SetFlagIf(state, Control::MouseButtonState::IsLeftButtonDown, point.Properties().IsLeftButtonPressed());
|
||
WI_SetFlagIf(state, Control::MouseButtonState::IsMiddleButtonDown, point.Properties().IsMiddleButtonPressed());
|
||
WI_SetFlagIf(state, Control::MouseButtonState::IsRightButtonDown, point.Properties().IsRightButtonPressed());
|
||
return state;
|
||
}
|
||
|
||
unsigned int TermControl::GetPointerUpdateKind(const winrt::Windows::UI::Input::PointerPoint point)
|
||
{
|
||
const auto props = point.Properties();
|
||
|
||
// Which mouse button changed state (and how)
|
||
unsigned int uiButton{};
|
||
switch (props.PointerUpdateKind())
|
||
{
|
||
case winrt::Windows::UI::Input::PointerUpdateKind::LeftButtonPressed:
|
||
uiButton = WM_LBUTTONDOWN;
|
||
break;
|
||
case winrt::Windows::UI::Input::PointerUpdateKind::LeftButtonReleased:
|
||
uiButton = WM_LBUTTONUP;
|
||
break;
|
||
case winrt::Windows::UI::Input::PointerUpdateKind::MiddleButtonPressed:
|
||
uiButton = WM_MBUTTONDOWN;
|
||
break;
|
||
case winrt::Windows::UI::Input::PointerUpdateKind::MiddleButtonReleased:
|
||
uiButton = WM_MBUTTONUP;
|
||
break;
|
||
case winrt::Windows::UI::Input::PointerUpdateKind::RightButtonPressed:
|
||
uiButton = WM_RBUTTONDOWN;
|
||
break;
|
||
case winrt::Windows::UI::Input::PointerUpdateKind::RightButtonReleased:
|
||
uiButton = WM_RBUTTONUP;
|
||
break;
|
||
default:
|
||
uiButton = WM_MOUSEMOVE;
|
||
}
|
||
|
||
return uiButton;
|
||
}
|
||
|
||
void TermControl::_coreWarningBell(const IInspectable& /*sender*/, const IInspectable& /*args*/)
|
||
{
|
||
_playWarningBell->Run();
|
||
}
|
||
|
||
hstring TermControl::ReadEntireBuffer() const
|
||
{
|
||
return _core.ReadEntireBuffer();
|
||
}
|
||
Control::CommandHistoryContext TermControl::CommandHistory() const
|
||
{
|
||
return _core.CommandHistory();
|
||
}
|
||
|
||
Core::Scheme TermControl::ColorScheme() const noexcept
|
||
{
|
||
return _core.ColorScheme();
|
||
}
|
||
|
||
void TermControl::ColorScheme(const Core::Scheme& scheme) const noexcept
|
||
{
|
||
_core.ColorScheme(scheme);
|
||
}
|
||
|
||
void TermControl::AdjustOpacity(const float opacity, const bool relative)
|
||
{
|
||
_core.AdjustOpacity(opacity, relative);
|
||
}
|
||
|
||
// - You'd think this should just be "Opacity", but UIElement already
|
||
// defines an "Opacity", which we're actually not setting at all. We're
|
||
// not overriding or changing _that_ value. Callers that want the opacity
|
||
// set by the settings should call this instead.
|
||
float TermControl::BackgroundOpacity() const
|
||
{
|
||
return _core.Opacity();
|
||
}
|
||
|
||
bool TermControl::HasSelection() const
|
||
{
|
||
return _core.HasSelection();
|
||
}
|
||
bool TermControl::HasMultiLineSelection() const
|
||
{
|
||
return _core.HasMultiLineSelection();
|
||
}
|
||
winrt::hstring TermControl::SelectedText(bool trimTrailingWhitespace) const
|
||
{
|
||
return _core.SelectedText(trimTrailingWhitespace);
|
||
}
|
||
|
||
void TermControl::_refreshSearch()
|
||
{
|
||
if (!_searchBox || !_searchBox->IsOpen())
|
||
{
|
||
return;
|
||
}
|
||
|
||
const auto text = _searchBox->Text();
|
||
if (text.empty())
|
||
{
|
||
return;
|
||
}
|
||
|
||
const auto goForward = _searchBox->GoForward();
|
||
const auto caseSensitive = _searchBox->CaseSensitive();
|
||
_handleSearchResults(_core.Search(text, goForward, caseSensitive, true));
|
||
}
|
||
|
||
void TermControl::_handleSearchResults(SearchResults results)
|
||
{
|
||
if (!_searchBox)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Only show status when we have a search term
|
||
if (_searchBox->Text().empty())
|
||
{
|
||
_searchBox->ClearStatus();
|
||
}
|
||
else
|
||
{
|
||
_searchBox->SetStatus(results.TotalMatches, results.CurrentMatch);
|
||
}
|
||
|
||
if (results.SearchInvalidated)
|
||
{
|
||
if (_showMarksInScrollbar)
|
||
{
|
||
const auto scrollBar = ScrollBar();
|
||
ScrollBarUpdate update{
|
||
.newValue = scrollBar.Value(),
|
||
.newMaximum = scrollBar.Maximum(),
|
||
.newMinimum = scrollBar.Minimum(),
|
||
.newViewportSize = scrollBar.ViewportSize(),
|
||
};
|
||
_updateScrollBar->Run(update);
|
||
}
|
||
|
||
if (auto automationPeer{ FrameworkElementAutomationPeer::FromElement(*this) })
|
||
{
|
||
automationPeer.RaiseNotificationEvent(
|
||
AutomationNotificationKind::ActionCompleted,
|
||
AutomationNotificationProcessing::ImportantMostRecent,
|
||
results.TotalMatches > 0 ? RS_(L"SearchBox_MatchesAvailable") : RS_(L"SearchBox_NoMatches"), // what to announce if results were found
|
||
L"SearchBoxResultAnnouncement" /* unique name for this group of notifications */);
|
||
}
|
||
}
|
||
}
|
||
|
||
void TermControl::_coreOutputIdle(const IInspectable& /*sender*/, const IInspectable& /*args*/)
|
||
{
|
||
_refreshSearch();
|
||
}
|
||
|
||
void TermControl::OwningHwnd(uint64_t owner)
|
||
{
|
||
_core.OwningHwnd(owner);
|
||
}
|
||
|
||
uint64_t TermControl::OwningHwnd()
|
||
{
|
||
return _core.OwningHwnd();
|
||
}
|
||
|
||
void TermControl::AddMark(const Control::ScrollMark& mark)
|
||
{
|
||
_core.AddMark(mark);
|
||
}
|
||
void TermControl::ClearMark() { _core.ClearMark(); }
|
||
void TermControl::ClearAllMarks() { _core.ClearAllMarks(); }
|
||
void TermControl::ScrollToMark(const Control::ScrollToMarkDirection& direction) { _core.ScrollToMark(direction); }
|
||
|
||
Windows::Foundation::Collections::IVector<Control::ScrollMark> TermControl::ScrollMarks() const
|
||
{
|
||
return _core.ScrollMarks();
|
||
}
|
||
|
||
void TermControl::SelectCommand(const bool goUp)
|
||
{
|
||
_core.SelectCommand(goUp);
|
||
}
|
||
|
||
void TermControl::SelectOutput(const bool goUp)
|
||
{
|
||
_core.SelectOutput(goUp);
|
||
}
|
||
|
||
void TermControl::ColorSelection(Control::SelectionColor fg, Control::SelectionColor bg, Core::MatchMode matchMode)
|
||
{
|
||
_core.ColorSelection(fg, bg, matchMode);
|
||
}
|
||
|
||
// Returns the text cursor's position relative to our origin, in DIPs.
|
||
Windows::Foundation::Point TermControl::CursorPositionInDips()
|
||
{
|
||
const til::point cursorPos{ _core.CursorPosition() };
|
||
|
||
// CharacterDimensions returns a font size in pixels.
|
||
const auto fontSize{ CharacterDimensions() };
|
||
|
||
// Convert text buffer cursor position to client coordinate position
|
||
// within the window. This point is in _pixels_
|
||
const Windows::Foundation::Point clientCursorPos{ cursorPos.x * fontSize.Width,
|
||
cursorPos.y * fontSize.Height };
|
||
|
||
// Get scale factor for view
|
||
const auto scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel();
|
||
|
||
// Adjust to DIPs
|
||
const til::point clientCursorInDips{ til::math::rounding, clientCursorPos.X / scaleFactor, clientCursorPos.Y / scaleFactor };
|
||
|
||
// Account for the margins, which are in DIPs
|
||
auto padding{ GetPadding() };
|
||
til::point relativeToOrigin{ til::math::flooring,
|
||
clientCursorInDips.x + padding.Left,
|
||
clientCursorInDips.y + padding.Top };
|
||
|
||
return relativeToOrigin.to_winrt_point();
|
||
}
|
||
|
||
void TermControl::_contextMenuHandler(IInspectable /*sender*/,
|
||
Control::ContextMenuRequestedEventArgs args)
|
||
{
|
||
// Position the menu where the pointer is. This was the best way I found how.
|
||
const til::point absolutePointerPos{ til::math::rounding, CoreWindow::GetForCurrentThread().PointerPosition() };
|
||
const til::point absoluteWindowOrigin{ til::math::rounding,
|
||
CoreWindow::GetForCurrentThread().Bounds().X,
|
||
CoreWindow::GetForCurrentThread().Bounds().Y };
|
||
// Get the offset (margin + tabs, etc..) of the control within the window
|
||
const til::point controlOrigin{ til::math::flooring,
|
||
this->TransformToVisual(nullptr).TransformPoint(Windows::Foundation::Point(0, 0)) };
|
||
|
||
const auto pos = (absolutePointerPos - absoluteWindowOrigin - controlOrigin);
|
||
_showContextMenuAt(pos);
|
||
}
|
||
|
||
void TermControl::_showContextMenuAt(const til::point& controlRelativePos)
|
||
{
|
||
Controls::Primitives::FlyoutShowOptions myOption{};
|
||
myOption.ShowMode(Controls::Primitives::FlyoutShowMode::Standard);
|
||
myOption.Placement(Controls::Primitives::FlyoutPlacementMode::TopEdgeAlignedLeft);
|
||
myOption.Position(controlRelativePos.to_winrt_point());
|
||
|
||
// The "Select command" and "Select output" buttons should only be
|
||
// visible if shell integration is actually turned on.
|
||
const auto shouldShowSelectCommand{ _core.ShouldShowSelectCommand() };
|
||
const auto shouldShowSelectOutput{ _core.ShouldShowSelectOutput() };
|
||
SelectCommandButton().Visibility(shouldShowSelectCommand ? Visibility::Visible : Visibility::Collapsed);
|
||
SelectOutputButton().Visibility(shouldShowSelectOutput ? Visibility::Visible : Visibility::Collapsed);
|
||
SelectCommandWithSelectionButton().Visibility(shouldShowSelectCommand ? Visibility::Visible : Visibility::Collapsed);
|
||
SelectOutputWithSelectionButton().Visibility(shouldShowSelectOutput ? Visibility::Visible : Visibility::Collapsed);
|
||
|
||
(_core.HasSelection() ? SelectionContextMenu() :
|
||
ContextMenu())
|
||
.ShowAt(*this, myOption);
|
||
}
|
||
|
||
void TermControl::ShowContextMenu()
|
||
{
|
||
const bool hasSelection = _core.HasSelection();
|
||
til::point cursorPos{
|
||
hasSelection ? _core.SelectionInfo().EndPos :
|
||
_core.CursorPosition()
|
||
};
|
||
// Offset this position a bit:
|
||
// * {+0,+1} if there's a selection. The selection endpoint is already
|
||
// exclusive, so add one row to align to the bottom of the selection
|
||
// * {+1,+1} if there's no selection, to be on the bottom-right corner of
|
||
// the cursor position
|
||
cursorPos += til::point{ hasSelection ? 0 : 1, 1 };
|
||
_showContextMenuAt(_toControlOrigin(cursorPos));
|
||
}
|
||
|
||
void TermControl::_PasteCommandHandler(const IInspectable& /*sender*/,
|
||
const IInspectable& /*args*/)
|
||
{
|
||
_interactivity.RequestPasteTextFromClipboard();
|
||
ContextMenu().Hide();
|
||
SelectionContextMenu().Hide();
|
||
}
|
||
void TermControl::_CopyCommandHandler(const IInspectable& /*sender*/,
|
||
const IInspectable& /*args*/)
|
||
{
|
||
// formats = nullptr -> copy all formats
|
||
_interactivity.CopySelectionToClipboard(false, nullptr);
|
||
ContextMenu().Hide();
|
||
SelectionContextMenu().Hide();
|
||
}
|
||
void TermControl::_SearchCommandHandler(const IInspectable& /*sender*/,
|
||
const IInspectable& /*args*/)
|
||
{
|
||
ContextMenu().Hide();
|
||
SelectionContextMenu().Hide();
|
||
|
||
// CreateSearchBoxControl will actually create the search box and
|
||
// pre-populate the box with the currently selected text.
|
||
CreateSearchBoxControl();
|
||
}
|
||
|
||
void TermControl::_SelectCommandHandler(const IInspectable& /*sender*/,
|
||
const IInspectable& /*args*/)
|
||
{
|
||
ContextMenu().Hide();
|
||
SelectionContextMenu().Hide();
|
||
_core.ContextMenuSelectCommand();
|
||
}
|
||
|
||
void TermControl::_SelectOutputHandler(const IInspectable& /*sender*/,
|
||
const IInspectable& /*args*/)
|
||
{
|
||
ContextMenu().Hide();
|
||
SelectionContextMenu().Hide();
|
||
_core.ContextMenuSelectOutput();
|
||
}
|
||
|
||
// Should the text cursor be displayed, even when the control isn't focused?
|
||
// n.b. "blur" is the opposite of "focus".
|
||
bool TermControl::_displayCursorWhileBlurred() const noexcept
|
||
{
|
||
return CursorVisibility() == Control::CursorDisplayState::Shown;
|
||
}
|
||
Control::CursorDisplayState TermControl::CursorVisibility() const noexcept
|
||
{
|
||
return _cursorVisibility;
|
||
}
|
||
void TermControl::CursorVisibility(Control::CursorDisplayState cursorVisibility)
|
||
{
|
||
_cursorVisibility = cursorVisibility;
|
||
if (!_initializedTerminal)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (_displayCursorWhileBlurred())
|
||
{
|
||
// If we should be ALWAYS displaying the cursor, turn it on and start blinking.
|
||
_core.CursorOn(true);
|
||
if (_cursorTimer)
|
||
{
|
||
_cursorTimer.Start();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Otherwise, if we're unfocused, then turn the cursor off and stop
|
||
// blinking. (if we're focused, then we're already doing the right
|
||
// thing)
|
||
const auto focused = FocusState() != FocusState::Unfocused;
|
||
if (!focused && _cursorTimer)
|
||
{
|
||
_cursorTimer.Stop();
|
||
}
|
||
_core.CursorOn(focused);
|
||
}
|
||
}
|
||
}
|