2161 lines
90 KiB
C++
2161 lines
90 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.h"
|
|
#include "TermControl.h"
|
|
#include <argb.h>
|
|
#include <DefaultSettings.h>
|
|
#include <unicode.hpp>
|
|
#include <Utf16Parser.hpp>
|
|
#include <Utils.h>
|
|
#include <WinUser.h>
|
|
#include "..\..\types\inc\GlyphWidth.hpp"
|
|
|
|
#include "TermControl.g.cpp"
|
|
#include "TermControlAutomationPeer.h"
|
|
|
|
using namespace ::Microsoft::Console::Types;
|
|
using namespace ::Microsoft::Terminal::Core;
|
|
using namespace winrt::Windows::UI::Xaml;
|
|
using namespace winrt::Windows::UI::Xaml::Automation::Peers;
|
|
using namespace winrt::Windows::UI::Core;
|
|
using namespace winrt::Windows::System;
|
|
using namespace winrt::Microsoft::Terminal::Settings;
|
|
|
|
namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|
{
|
|
// Helper static function to ensure that all ambiguous-width glyphs are reported as narrow.
|
|
// See microsoft/terminal#2066 for more info.
|
|
static bool _IsGlyphWideForceNarrowFallback(const std::wstring_view /* glyph */)
|
|
{
|
|
return false; // glyph is not wide.
|
|
}
|
|
|
|
static bool _EnsureStaticInitialization()
|
|
{
|
|
// use C++11 magic statics to make sure we only do this once.
|
|
static bool initialized = []() {
|
|
// *** THIS IS A SINGLETON ***
|
|
SetGlyphWidthFallback(_IsGlyphWideForceNarrowFallback);
|
|
|
|
return true;
|
|
}();
|
|
return initialized;
|
|
}
|
|
|
|
TermControl::TermControl() :
|
|
TermControl(Settings::TerminalSettings{}, TerminalConnection::ITerminalConnection{ nullptr })
|
|
{
|
|
}
|
|
|
|
TermControl::TermControl(Settings::IControlSettings settings, TerminalConnection::ITerminalConnection connection) :
|
|
_connection{ connection },
|
|
_initializedTerminal{ false },
|
|
_root{ nullptr },
|
|
_swapChainPanel{ nullptr },
|
|
_settings{ settings },
|
|
_closing{ false },
|
|
_lastScrollOffset{ std::nullopt },
|
|
_autoScrollVelocity{ 0 },
|
|
_autoScrollingPointerPoint{ std::nullopt },
|
|
_autoScrollTimer{},
|
|
_lastAutoScrollUpdateTime{ std::nullopt },
|
|
_desiredFont{ DEFAULT_FONT_FACE.c_str(), 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8 },
|
|
_actualFont{ DEFAULT_FONT_FACE.c_str(), 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false },
|
|
_touchAnchor{ std::nullopt },
|
|
_cursorTimer{},
|
|
_lastMouseClick{},
|
|
_lastMouseClickPos{},
|
|
_searchBox{ nullptr },
|
|
_tsfInputControl{ nullptr }
|
|
{
|
|
_EnsureStaticInitialization();
|
|
_Create();
|
|
}
|
|
|
|
void TermControl::_Create()
|
|
{
|
|
Controls::Grid container;
|
|
|
|
Controls::ColumnDefinition contentColumn{};
|
|
Controls::ColumnDefinition scrollbarColumn{};
|
|
contentColumn.Width(GridLength{ 1.0, GridUnitType::Star });
|
|
scrollbarColumn.Width(GridLength{ 1.0, GridUnitType::Auto });
|
|
|
|
container.ColumnDefinitions().Append(contentColumn);
|
|
container.ColumnDefinitions().Append(scrollbarColumn);
|
|
|
|
_scrollBar = Controls::Primitives::ScrollBar{};
|
|
_scrollBar.Orientation(Controls::Orientation::Vertical);
|
|
_scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator);
|
|
_scrollBar.HorizontalAlignment(HorizontalAlignment::Right);
|
|
_scrollBar.VerticalAlignment(VerticalAlignment::Stretch);
|
|
|
|
// Initialize the scrollbar with some placeholder values.
|
|
// The scrollbar will be updated with real values on _Initialize
|
|
_scrollBar.Maximum(1);
|
|
_scrollBar.ViewportSize(10);
|
|
_scrollBar.IsTabStop(false);
|
|
_scrollBar.SmallChange(1);
|
|
_scrollBar.LargeChange(4);
|
|
_scrollBar.Visibility(Visibility::Visible);
|
|
|
|
_tsfInputControl = TSFInputControl();
|
|
_tsfInputControl.CompositionCompleted({ this, &TermControl::_CompositionCompleted });
|
|
_tsfInputControl.CurrentCursorPosition({ this, &TermControl::_CurrentCursorPositionHandler });
|
|
_tsfInputControl.CurrentFontInfo({ this, &TermControl::_FontInfoHandler });
|
|
container.Children().Append(_tsfInputControl);
|
|
|
|
// Create the SwapChainPanel that will display our content
|
|
Controls::SwapChainPanel swapChainPanel;
|
|
|
|
_sizeChangedRevoker = swapChainPanel.SizeChanged(winrt::auto_revoke, { this, &TermControl::_SwapChainSizeChanged });
|
|
_compositionScaleChangedRevoker = swapChainPanel.CompositionScaleChanged(winrt::auto_revoke, { this, &TermControl::_SwapChainScaleChanged });
|
|
|
|
// 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())
|
|
{
|
|
// Only let this succeed once.
|
|
_layoutUpdatedRevoker.revoke();
|
|
}
|
|
});
|
|
|
|
container.Children().Append(swapChainPanel);
|
|
container.Children().Append(_scrollBar);
|
|
Controls::Grid::SetColumn(swapChainPanel, 0);
|
|
Controls::Grid::SetColumn(_scrollBar, 1);
|
|
|
|
Controls::Grid root{};
|
|
Controls::Image bgImageLayer{};
|
|
root.Children().Append(bgImageLayer);
|
|
root.Children().Append(container);
|
|
|
|
_root = root;
|
|
_bgImageLayer = bgImageLayer;
|
|
|
|
_swapChainPanel = swapChainPanel;
|
|
this->Content(_root);
|
|
|
|
_ApplyUISettings();
|
|
|
|
// These are important:
|
|
// 1. When we get tapped, focus us
|
|
_tappedRevoker = this->Tapped(winrt::auto_revoke, [this](auto&, auto& e) {
|
|
Focus(FocusState::Pointer);
|
|
e.Handled(true);
|
|
});
|
|
// 2. Make sure we can be focused (why this isn't `Focusable` I'll never know)
|
|
this->IsTabStop(true);
|
|
// 3. Actually not sure about this one. Maybe it isn't necessary either.
|
|
this->AllowFocusOnInteraction(true);
|
|
|
|
// DON'T CALL _InitializeTerminal here - wait until the swap chain is loaded to do that.
|
|
|
|
// Subscribe to the connection's disconnected event and call our connection closed handlers.
|
|
_connectionStateChangedRevoker = _connection.StateChanged(winrt::auto_revoke, [this](auto&& /*s*/, auto&& /*v*/) {
|
|
_ConnectionStateChangedHandlers(*this, nullptr);
|
|
});
|
|
}
|
|
|
|
// Method Description:
|
|
// - Create the SearchBoxControl object, and attach it
|
|
// to the Terminal Control root
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void TermControl::CreateSearchBoxControl()
|
|
{
|
|
if (!_searchBox)
|
|
{
|
|
_searchBox = winrt::make_self<SearchBoxControl>();
|
|
_searchBox->HorizontalAlignment(HorizontalAlignment::Right);
|
|
_searchBox->VerticalAlignment(VerticalAlignment::Top);
|
|
// We need to make sure the searchbox does not overlap
|
|
// with the scroll bar
|
|
Thickness searchBoxPadding = { 0, 0, _scrollBar.ActualWidth(), 0 };
|
|
_searchBox->Margin(searchBoxPadding);
|
|
|
|
_root.Children().Append(*_searchBox);
|
|
|
|
// Event handlers
|
|
_searchBox->Search({ get_weak(), &TermControl::_Search });
|
|
_searchBox->Closed({ get_weak(), &TermControl::_CloseSearchBoxControl });
|
|
}
|
|
|
|
_searchBox->SetFocusOnTextbox();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Search text in text buffer. This is triggered if the user click
|
|
// search button or press enter.
|
|
// Arguments:
|
|
// - text: the text to search
|
|
// - goForward: boolean that represents if the current search direction is forward
|
|
// - caseSensitive: boolean that represents if the current search is case sensitive
|
|
// Return Value:
|
|
// - <none>
|
|
void TermControl::_Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive)
|
|
{
|
|
if (text.size() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const Search::Direction direction = goForward ?
|
|
Search::Direction::Forward :
|
|
Search::Direction::Backward;
|
|
|
|
const Search::Sensitivity sensitivity = caseSensitive ?
|
|
Search::Sensitivity::CaseSensitive :
|
|
Search::Sensitivity::CaseInsensitive;
|
|
|
|
Search search(*GetUiaData(), text.c_str(), direction, sensitivity);
|
|
auto lock = _terminal->LockForWriting();
|
|
if (search.FindNext())
|
|
{
|
|
_terminal->SetBoxSelection(false);
|
|
search.Select();
|
|
_renderer->TriggerSelection();
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - The handler for the close button or pressing "Esc" when focusing on the
|
|
// search dialog.
|
|
// This removes the SearchBoxControl object from the XAML tree,
|
|
// reset smart pointer and set focus back to Terminal
|
|
// Arguments:
|
|
// - IInspectable: not used
|
|
// - RoutedEventArgs: not used
|
|
// Return Value:
|
|
// - <none>
|
|
void TermControl::_CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& /*sender*/, RoutedEventArgs const& /*args*/)
|
|
{
|
|
unsigned int idx;
|
|
_root.Children().IndexOf(*_searchBox, idx);
|
|
_root.Children().RemoveAt(idx);
|
|
|
|
_searchBox = nullptr;
|
|
|
|
// Set focus back to terminal control
|
|
this->Focus(FocusState::Programmatic);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Given new settings for this profile, applies the settings to the current terminal.
|
|
// Arguments:
|
|
// - newSettings: New settings values for the profile in this terminal.
|
|
// Return Value:
|
|
// - <none>
|
|
winrt::fire_and_forget TermControl::UpdateSettings(Settings::IControlSettings newSettings)
|
|
{
|
|
_settings = newSettings;
|
|
auto weakThis{ get_weak() };
|
|
|
|
// Dispatch a call to the UI thread to apply the new settings to the
|
|
// terminal.
|
|
co_await winrt::resume_foreground(_root.Dispatcher());
|
|
|
|
// If 'weakThis' is locked, then we can safely work with 'this'
|
|
if (auto control{ weakThis.get() })
|
|
{
|
|
// Update our control settings
|
|
_ApplyUISettings();
|
|
|
|
// Update DxEngine's SelectionBackground
|
|
_renderEngine->SetSelectionBackground(_settings.SelectionBackground());
|
|
|
|
// Update the terminal core with its new Core settings
|
|
_terminal->UpdateSettings(_settings);
|
|
|
|
// Refresh our font with the renderer
|
|
_UpdateFont();
|
|
|
|
const auto width = _swapChainPanel.ActualWidth();
|
|
const auto height = _swapChainPanel.ActualHeight();
|
|
if (width != 0 && height != 0)
|
|
{
|
|
// If the font size changed, or the _swapchainPanel's size changed
|
|
// for any reason, we'll need to make sure to also resize the
|
|
// buffer. _DoResize will invalidate everything for us.
|
|
auto lock = _terminal->LockForWriting();
|
|
_DoResize(width, height);
|
|
}
|
|
|
|
// set TSF Foreground
|
|
Media::SolidColorBrush foregroundBrush{};
|
|
foregroundBrush.Color(ColorRefToColor(_settings.DefaultForeground()));
|
|
_tsfInputControl.Foreground(foregroundBrush);
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
|
|
uint32_t bg = _settings.DefaultBackground();
|
|
_BackgroundColorChanged(bg);
|
|
|
|
// Apply padding as swapChainPanel's margin
|
|
auto newMargin = _ParseThicknessFromPadding(_settings.Padding());
|
|
auto existingMargin = _swapChainPanel.Margin();
|
|
_swapChainPanel.Margin(newMargin);
|
|
|
|
if (newMargin != existingMargin && newMargin != Thickness{ 0 })
|
|
{
|
|
TraceLoggingWrite(g_hTerminalControlProvider,
|
|
"NonzeroPaddingApplied",
|
|
TraceLoggingDescription("An event emitted when a control has padding applied to it"),
|
|
TraceLoggingStruct(4, "Padding"),
|
|
TraceLoggingFloat64(newMargin.Left, "Left"),
|
|
TraceLoggingFloat64(newMargin.Top, "Top"),
|
|
TraceLoggingFloat64(newMargin.Right, "Right"),
|
|
TraceLoggingFloat64(newMargin.Bottom, "Bottom"),
|
|
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
|
|
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
|
|
}
|
|
|
|
// Initialize our font information.
|
|
const auto* fontFace = _settings.FontFace().c_str();
|
|
const short fontHeight = gsl::narrow<short>(_settings.FontSize());
|
|
// 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.
|
|
_actualFont = { fontFace, 0, 10, { 0, fontHeight }, CP_UTF8, false };
|
|
_desiredFont = { _actualFont };
|
|
|
|
// set TSF Foreground
|
|
Media::SolidColorBrush foregroundBrush{};
|
|
foregroundBrush.Color(ColorRefToColor(_settings.DefaultForeground()));
|
|
_tsfInputControl.Foreground(foregroundBrush);
|
|
_tsfInputControl.Margin(newMargin);
|
|
|
|
// set number of rows to scroll at a time
|
|
_rowsToScroll = _settings.RowsToScroll();
|
|
}
|
|
|
|
// 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.
|
|
// - Does not apply background color outside of acrylic mode;
|
|
// _BackgroundColorChanged must be called to do so.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void TermControl::_InitializeBackgroundBrush()
|
|
{
|
|
if (_settings.UseAcrylic())
|
|
{
|
|
// See if we've already got an acrylic background brush
|
|
// to avoid the flicker when setting up a new one
|
|
auto acrylic = _root.Background().try_as<Media::AcrylicBrush>();
|
|
|
|
// Instantiate a brush if there's not already one there
|
|
if (acrylic == nullptr)
|
|
{
|
|
acrylic = Media::AcrylicBrush{};
|
|
acrylic.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop);
|
|
}
|
|
|
|
// see GH#1082: Initialize background color so we don't get a
|
|
// fade/flash when _BackgroundColorChanged is called
|
|
uint32_t color = _settings.DefaultBackground();
|
|
winrt::Windows::UI::Color bgColor{};
|
|
bgColor.R = GetRValue(color);
|
|
bgColor.G = GetGValue(color);
|
|
bgColor.B = GetBValue(color);
|
|
bgColor.A = 255;
|
|
|
|
acrylic.FallbackColor(bgColor);
|
|
acrylic.TintColor(bgColor);
|
|
|
|
// Apply brush settings
|
|
acrylic.TintOpacity(_settings.TintOpacity());
|
|
|
|
// Apply brush to control if it's not already there
|
|
if (_root.Background() != acrylic)
|
|
{
|
|
_root.Background(acrylic);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Media::SolidColorBrush solidColor{};
|
|
_root.Background(solidColor);
|
|
}
|
|
|
|
if (!_settings.BackgroundImage().empty())
|
|
{
|
|
Windows::Foundation::Uri imageUri{ _settings.BackgroundImage() };
|
|
|
|
// 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 = _bgImageLayer.Source().try_as<Media::Imaging::BitmapImage>();
|
|
|
|
if (imageSource == nullptr ||
|
|
imageSource.UriSource() == nullptr ||
|
|
imageSource.UriSource().RawUri() != imageUri.RawUri())
|
|
{
|
|
// 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);
|
|
_bgImageLayer.Source(image);
|
|
}
|
|
|
|
// Apply stretch, opacity and alignment settings
|
|
_bgImageLayer.Stretch(_settings.BackgroundImageStretchMode());
|
|
_bgImageLayer.Opacity(_settings.BackgroundImageOpacity());
|
|
_bgImageLayer.HorizontalAlignment(_settings.BackgroundImageHorizontalAlignment());
|
|
_bgImageLayer.VerticalAlignment(_settings.BackgroundImageVerticalAlignment());
|
|
}
|
|
else
|
|
{
|
|
_bgImageLayer.Source(nullptr);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Style the background of the control with the provided background color
|
|
// Arguments:
|
|
// - color: The background color to use as a uint32 (aka DWORD COLORREF)
|
|
// Return Value:
|
|
// - <none>
|
|
winrt::fire_and_forget TermControl::_BackgroundColorChanged(const uint32_t color)
|
|
{
|
|
auto weakThis{ get_weak() };
|
|
|
|
co_await winrt::resume_foreground(_root.Dispatcher());
|
|
|
|
if (auto control{ weakThis.get() })
|
|
{
|
|
const auto R = GetRValue(color);
|
|
const auto G = GetGValue(color);
|
|
const auto B = GetBValue(color);
|
|
|
|
winrt::Windows::UI::Color bgColor{};
|
|
bgColor.R = R;
|
|
bgColor.G = G;
|
|
bgColor.B = B;
|
|
bgColor.A = 255;
|
|
|
|
if (auto acrylic = _root.Background().try_as<Media::AcrylicBrush>())
|
|
{
|
|
acrylic.FallbackColor(bgColor);
|
|
acrylic.TintColor(bgColor);
|
|
}
|
|
else if (auto solidColor = _root.Background().try_as<Media::SolidColorBrush>())
|
|
{
|
|
solidColor.Color(bgColor);
|
|
}
|
|
|
|
// Set the default background as transparent to prevent the
|
|
// DX layer from overwriting the background image or acrylic effect
|
|
_settings.DefaultBackground(ARGB(0, R, G, B));
|
|
}
|
|
}
|
|
|
|
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()
|
|
try
|
|
{
|
|
if (GetUiaData())
|
|
{
|
|
// create a custom automation peer with this code pattern:
|
|
// (https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/custom-automation-peers)
|
|
auto autoPeer = winrt::make_self<winrt::Microsoft::Terminal::TerminalControl::implementation::TermControlAutomationPeer>(this);
|
|
|
|
_uiaEngine = std::make_unique<::Microsoft::Console::Render::UiaEngine>(autoPeer.get());
|
|
_renderer->AddRenderEngine(_uiaEngine.get());
|
|
return *autoPeer;
|
|
}
|
|
return nullptr;
|
|
}
|
|
catch (...)
|
|
{
|
|
LOG_CAUGHT_EXCEPTION();
|
|
return nullptr;
|
|
}
|
|
|
|
::Microsoft::Console::Types::IUiaData* TermControl::GetUiaData() const
|
|
{
|
|
return _terminal.get();
|
|
}
|
|
|
|
const FontInfo TermControl::GetActualFont() const
|
|
{
|
|
return _actualFont;
|
|
}
|
|
|
|
const Windows::UI::Xaml::Thickness TermControl::GetPadding() const
|
|
{
|
|
return _swapChainPanel.Margin();
|
|
}
|
|
|
|
TerminalConnection::ConnectionState TermControl::ConnectionState() const
|
|
{
|
|
return _connection.State();
|
|
}
|
|
|
|
winrt::fire_and_forget TermControl::SwapChainChanged()
|
|
{
|
|
if (!_initializedTerminal)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto chain = _renderEngine->GetSwapChain();
|
|
auto weakThis{ get_weak() };
|
|
|
|
co_await winrt::resume_foreground(_swapChainPanel.Dispatcher());
|
|
|
|
// If 'weakThis' is locked, then we can safely work with 'this'
|
|
if (auto control{ weakThis.get() })
|
|
{
|
|
auto lock = _terminal->LockForWriting();
|
|
auto nativePanel = _swapChainPanel.as<ISwapChainPanelNative>();
|
|
nativePanel->SetSwapChain(chain.Get());
|
|
}
|
|
}
|
|
|
|
winrt::fire_and_forget TermControl::_SwapChainRoutine()
|
|
{
|
|
auto chain = _renderEngine->GetSwapChain();
|
|
auto weakThis{ get_weak() };
|
|
|
|
co_await winrt::resume_foreground(_swapChainPanel.Dispatcher());
|
|
|
|
if (auto control{ weakThis.get() })
|
|
{
|
|
_terminal->LockConsole();
|
|
auto nativePanel = _swapChainPanel.as<ISwapChainPanelNative>();
|
|
nativePanel->SetSwapChain(chain.Get());
|
|
_terminal->UnlockConsole();
|
|
}
|
|
}
|
|
|
|
bool TermControl::_InitializeTerminal()
|
|
{
|
|
if (_initializedTerminal)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const auto windowWidth = _swapChainPanel.ActualWidth(); // Width() and Height() are NaN?
|
|
const auto windowHeight = _swapChainPanel.ActualHeight();
|
|
|
|
if (windowWidth == 0 || windowHeight == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_terminal = std::make_unique<::Microsoft::Terminal::Core::Terminal>();
|
|
|
|
// First create the render thread.
|
|
// Then stash a local pointer to the render thread so we can initialize it and enable it
|
|
// to paint itself *after* we hand off its ownership to the renderer.
|
|
// We split up construction and initialization of the render thread object this way
|
|
// because the renderer and render thread have circular references to each other.
|
|
auto renderThread = std::make_unique<::Microsoft::Console::Render::RenderThread>();
|
|
auto* const localPointerToThread = renderThread.get();
|
|
|
|
// Now create the renderer and initialize the render thread.
|
|
_renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(_terminal.get(), nullptr, 0, std::move(renderThread));
|
|
::Microsoft::Console::Render::IRenderTarget& renderTarget = *_renderer;
|
|
|
|
THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get()));
|
|
|
|
// Set up the DX Engine
|
|
auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
|
|
_renderer->AddRenderEngine(dxEngine.get());
|
|
|
|
// Initialize our font with the renderer
|
|
// We don't have to care about DPI. We'll get a change message immediately if it's not 96
|
|
// and react accordingly.
|
|
_UpdateFont(true);
|
|
|
|
const COORD windowSize{ static_cast<short>(windowWidth), static_cast<short>(windowHeight) };
|
|
|
|
// Fist set up the dx engine with the window size in pixels.
|
|
// Then, using the font, get the number of characters that can fit.
|
|
// Resize our terminal connection to match that size, and initialize the terminal with that size.
|
|
const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize);
|
|
THROW_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() }));
|
|
|
|
// Update DxEngine's SelectionBackground
|
|
dxEngine->SetSelectionBackground(_settings.SelectionBackground());
|
|
|
|
const auto vp = dxEngine->GetViewportInCharacters(viewInPixels);
|
|
const auto width = vp.Width();
|
|
const auto height = vp.Height();
|
|
_connection.Resize(height, width);
|
|
|
|
// Override the default width and height to match the size of the swapChainPanel
|
|
_settings.InitialCols(width);
|
|
_settings.InitialRows(height);
|
|
|
|
_terminal->CreateFromSettings(_settings, renderTarget);
|
|
|
|
// Tell the DX Engine to notify us when the swap chain changes.
|
|
dxEngine->SetCallback(std::bind(&TermControl::SwapChainChanged, this));
|
|
|
|
// TODO:GH#3927 - Make it possible to hot-reload this setting. Right
|
|
// here, the setting will only be used when the Temrinal is initialized.
|
|
dxEngine->SetRetroTerminalEffects(_settings.RetroTerminalEffect());
|
|
|
|
THROW_IF_FAILED(dxEngine->Enable());
|
|
_renderEngine = std::move(dxEngine);
|
|
|
|
// This event is explicitly revoked in the destructor: does not need weak_ref
|
|
auto onRecieveOutputFn = [this](const hstring str) {
|
|
_terminal->Write(str.c_str());
|
|
};
|
|
_connectionOutputEventToken = _connection.TerminalOutput(onRecieveOutputFn);
|
|
|
|
auto inputFn = std::bind(&TermControl::_SendInputToConnection, this, std::placeholders::_1);
|
|
_terminal->SetWriteInputCallback(inputFn);
|
|
|
|
_SwapChainRoutine();
|
|
|
|
// Set up the height of the ScrollViewer and the grid we're using to fake our scrolling height
|
|
auto bottom = _terminal->GetViewport().BottomExclusive();
|
|
auto bufferHeight = bottom;
|
|
|
|
const auto originalMaximum = _scrollBar.Maximum();
|
|
const auto originalMinimum = _scrollBar.Minimum();
|
|
const auto originalValue = _scrollBar.Value();
|
|
const auto originalViewportSize = _scrollBar.ViewportSize();
|
|
|
|
_scrollBar.Maximum(bufferHeight - bufferHeight);
|
|
_scrollBar.Minimum(0);
|
|
_scrollBar.Value(0);
|
|
_scrollBar.ViewportSize(bufferHeight);
|
|
_scrollBar.ValueChanged({ this, &TermControl::_ScrollbarChangeHandler });
|
|
_scrollBar.PointerPressed({ this, &TermControl::_CapturePointer });
|
|
_scrollBar.PointerReleased({ this, &TermControl::_ReleasePointerCapture });
|
|
|
|
// Apply settings for scrollbar
|
|
if (_settings.ScrollState() == ScrollbarState::Visible)
|
|
{
|
|
_scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator);
|
|
}
|
|
else if (_settings.ScrollState() == ScrollbarState::Hidden)
|
|
{
|
|
_scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::None);
|
|
|
|
// In the scenario where the user has turned off the OS setting to automatically hide scollbars, the
|
|
// Terminal scrollbar would still be visible; so, we need to set the control's visibility accordingly to
|
|
// achieve the intended effect.
|
|
_scrollBar.Visibility(Visibility::Collapsed);
|
|
}
|
|
else
|
|
{
|
|
// Default behavior
|
|
_scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator);
|
|
}
|
|
|
|
_root.PointerWheelChanged({ this, &TermControl::_MouseWheelHandler });
|
|
|
|
// These need to be hooked up to the SwapChainPanel because we don't want the scrollbar to respond to pointer events (GitHub #950)
|
|
_swapChainPanel.PointerPressed({ this, &TermControl::_PointerPressedHandler });
|
|
_swapChainPanel.PointerMoved({ this, &TermControl::_PointerMovedHandler });
|
|
_swapChainPanel.PointerReleased({ this, &TermControl::_PointerReleasedHandler });
|
|
|
|
localPointerToThread->EnablePainting();
|
|
|
|
// No matter what order these guys are in, The KeyDown's will fire
|
|
// before the CharacterRecieved, so we can't easily get characters
|
|
// first, then fallback to getting keys from vkeys.
|
|
// TODO: This apparently handles keys and characters correctly, though
|
|
// I'd keep an eye on it, and test more.
|
|
// I presume that the characters that aren't translated by terminalInput
|
|
// just end up getting ignored, and the rest of the input comes
|
|
// through CharacterRecieved.
|
|
// I don't believe there's a difference between KeyDown and
|
|
// PreviewKeyDown for our purposes
|
|
// These two handlers _must_ be on this, not _root.
|
|
this->PreviewKeyDown({ this, &TermControl::_KeyDownHandler });
|
|
this->CharacterReceived({ this, &TermControl::_CharacterHandler });
|
|
|
|
auto pfnTitleChanged = std::bind(&TermControl::_TerminalTitleChanged, this, std::placeholders::_1);
|
|
_terminal->SetTitleChangedCallback(pfnTitleChanged);
|
|
|
|
auto pfnBackgroundColorChanged = std::bind(&TermControl::_BackgroundColorChanged, this, std::placeholders::_1);
|
|
_terminal->SetBackgroundCallback(pfnBackgroundColorChanged);
|
|
|
|
auto pfnScrollPositionChanged = std::bind(&TermControl::_TerminalScrollPositionChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
|
|
_terminal->SetScrollPositionChangedCallback(pfnScrollPositionChanged);
|
|
|
|
static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast<int>(1.0 / 30.0 * 1000000));
|
|
_autoScrollTimer.Interval(AutoScrollUpdateInterval);
|
|
_autoScrollTimer.Tick({ get_weak(), &TermControl::_UpdateAutoScroll });
|
|
|
|
// Set up blinking cursor
|
|
int blinkTime = GetCaretBlinkTime();
|
|
if (blinkTime != INFINITE)
|
|
{
|
|
// Create a timer
|
|
_cursorTimer = std::make_optional(DispatcherTimer());
|
|
_cursorTimer.value().Interval(std::chrono::milliseconds(blinkTime));
|
|
_cursorTimer.value().Tick({ get_weak(), &TermControl::_BlinkCursor });
|
|
_cursorTimer.value().Start();
|
|
}
|
|
else
|
|
{
|
|
// The user has disabled cursor blinking
|
|
_cursorTimer = std::nullopt;
|
|
}
|
|
|
|
// import value from WinUser (convert from milli-seconds to micro-seconds)
|
|
_multiClickTimer = GetDoubleClickTime() * 1000;
|
|
|
|
_gotFocusRevoker = this->GotFocus(winrt::auto_revoke, { this, &TermControl::_GotFocusHandler });
|
|
_lostFocusRevoker = this->LostFocus(winrt::auto_revoke, { this, &TermControl::_LostFocusHandler });
|
|
|
|
// Focus the control here. If we do it up above (in _Create_), then the
|
|
// focus won't actually get passed to us. I believe this is because
|
|
// we're not technically a part of the UI tree yet, so focusing us
|
|
// becomes a no-op.
|
|
this->Focus(FocusState::Programmatic);
|
|
|
|
_connection.Start();
|
|
_initializedTerminal = true;
|
|
return true;
|
|
}
|
|
|
|
void TermControl::_CharacterHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/,
|
|
Input::CharacterReceivedRoutedEventArgs const& e)
|
|
{
|
|
if (_closing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const auto ch = e.Character();
|
|
|
|
const bool handled = _terminal->SendCharEvent(ch);
|
|
e.Handled(handled);
|
|
}
|
|
|
|
void TermControl::_KeyDownHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/,
|
|
Input::KeyRoutedEventArgs const& e)
|
|
{
|
|
// 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;
|
|
}
|
|
|
|
// mark event as handled and do nothing if...
|
|
// - closing
|
|
// - key modifier is pressed
|
|
// NOTE: for key combos like CTRL + C, two events are fired (one for CTRL, one for 'C'). We care about the 'C' event and then check for key modifiers below.
|
|
if (_closing ||
|
|
e.OriginalKey() == VirtualKey::Control ||
|
|
e.OriginalKey() == VirtualKey::Shift ||
|
|
e.OriginalKey() == VirtualKey::Menu ||
|
|
e.OriginalKey() == VirtualKey::LeftWindows ||
|
|
e.OriginalKey() == VirtualKey::RightWindows)
|
|
|
|
{
|
|
e.Handled(true);
|
|
return;
|
|
}
|
|
|
|
const auto modifiers = _GetPressedModifierKeys();
|
|
const auto vkey = gsl::narrow_cast<WORD>(e.OriginalKey());
|
|
const auto scanCode = gsl::narrow_cast<WORD>(e.KeyStatus().ScanCode);
|
|
bool handled = false;
|
|
|
|
// 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.
|
|
if (!modifiers.IsAltGrPressed())
|
|
{
|
|
auto bindings = _settings.KeyBindings();
|
|
if (bindings)
|
|
{
|
|
handled = bindings.TryKeyChord({
|
|
modifiers.IsCtrlPressed(),
|
|
modifiers.IsAltPressed(),
|
|
modifiers.IsShiftPressed(),
|
|
vkey,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!handled)
|
|
{
|
|
handled = _TrySendKeyEvent(vkey, scanCode, modifiers);
|
|
}
|
|
|
|
// 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.
|
|
if (e.OriginalKey() == VirtualKey::Tab)
|
|
{
|
|
handled = true;
|
|
}
|
|
|
|
e.Handled(handled);
|
|
}
|
|
|
|
// 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.
|
|
// - states: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states.
|
|
bool TermControl::_TrySendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates modifiers)
|
|
{
|
|
_terminal->ClearSelection();
|
|
|
|
// 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 CharacterRecieved event.
|
|
const auto handled = vkey ? _terminal->SendKeyEvent(vkey, scanCode, modifiers) : true;
|
|
|
|
if (_cursorTimer.has_value())
|
|
{
|
|
// Manually show the cursor when a key is pressed. Restarting
|
|
// the timer prevents flickering.
|
|
_terminal->SetCursorVisible(true);
|
|
_cursorTimer.value().Start();
|
|
}
|
|
|
|
return handled;
|
|
}
|
|
|
|
// 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(Windows::Foundation::IInspectable const& sender,
|
|
Input::PointerRoutedEventArgs const& args)
|
|
{
|
|
_CapturePointer(sender, args);
|
|
|
|
const auto ptr = args.Pointer();
|
|
const auto point = args.GetCurrentPoint(_root);
|
|
|
|
if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen)
|
|
{
|
|
// Ignore mouse events while the terminal does not have focus.
|
|
// This prevents the user from selecting and copying text if they
|
|
// click inside the current tab to refocus the terminal window.
|
|
if (!_focused)
|
|
{
|
|
args.Handled(true);
|
|
return;
|
|
}
|
|
|
|
const auto modifiers = static_cast<uint32_t>(args.KeyModifiers());
|
|
// static_cast to a uint32_t because we can't use the WI_IsFlagSet
|
|
// macro directly with a VirtualKeyModifiers
|
|
const auto altEnabled = WI_IsFlagSet(modifiers, static_cast<uint32_t>(VirtualKeyModifiers::Menu));
|
|
const auto shiftEnabled = WI_IsFlagSet(modifiers, static_cast<uint32_t>(VirtualKeyModifiers::Shift));
|
|
|
|
if (point.Properties().IsLeftButtonPressed())
|
|
{
|
|
const auto cursorPosition = point.Position();
|
|
const auto terminalPosition = _GetTerminalPosition(cursorPosition);
|
|
|
|
// handle ALT key
|
|
_terminal->SetBoxSelection(altEnabled);
|
|
|
|
auto clickCount = _NumberOfClicks(cursorPosition, point.Timestamp());
|
|
|
|
// This formula enables the number of clicks to cycle properly between single-, double-, and triple-click.
|
|
// To increase the number of acceptable click states, simply increment MAX_CLICK_COUNT and add another if-statement
|
|
const unsigned int MAX_CLICK_COUNT = 3;
|
|
const auto multiClickMapper = clickCount > MAX_CLICK_COUNT ? ((clickCount + MAX_CLICK_COUNT - 1) % MAX_CLICK_COUNT) + 1 : clickCount;
|
|
|
|
if (multiClickMapper == 3)
|
|
{
|
|
_terminal->TripleClickSelection(terminalPosition);
|
|
}
|
|
else if (multiClickMapper == 2)
|
|
{
|
|
_terminal->DoubleClickSelection(terminalPosition);
|
|
}
|
|
else
|
|
{
|
|
// save location before rendering
|
|
_terminal->SetSelectionAnchor(terminalPosition);
|
|
|
|
_lastMouseClick = point.Timestamp();
|
|
_lastMouseClickPos = cursorPosition;
|
|
}
|
|
_renderer->TriggerSelection();
|
|
}
|
|
else if (point.Properties().IsRightButtonPressed())
|
|
{
|
|
// copyOnSelect causes right-click to always paste
|
|
if (_terminal->IsCopyOnSelectActive() || !_terminal->IsSelectionActive())
|
|
{
|
|
PasteTextFromClipboard();
|
|
}
|
|
else
|
|
{
|
|
CopySelectionToClipboard(!shiftEnabled);
|
|
}
|
|
}
|
|
}
|
|
else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch)
|
|
{
|
|
const auto contactRect = point.Properties().ContactRect();
|
|
// Set our touch rect, to start a pan.
|
|
_touchAnchor = winrt::Windows::Foundation::Point{ contactRect.X, contactRect.Y };
|
|
}
|
|
|
|
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(Windows::Foundation::IInspectable const& /*sender*/,
|
|
Input::PointerRoutedEventArgs const& args)
|
|
{
|
|
const auto ptr = args.Pointer();
|
|
const auto point = args.GetCurrentPoint(_root);
|
|
|
|
if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen)
|
|
{
|
|
if (point.Properties().IsLeftButtonPressed())
|
|
{
|
|
const auto cursorPosition = point.Position();
|
|
_SetEndSelectionPointAtCursor(cursorPosition);
|
|
|
|
const double cursorBelowBottomDist = cursorPosition.Y - _swapChainPanel.Margin().Top - _swapChainPanel.ActualHeight();
|
|
const double cursorAboveTopDist = -1 * cursorPosition.Y + _swapChainPanel.Margin().Top;
|
|
|
|
constexpr double MinAutoScrollDist = 2.0; // Arbitrary value
|
|
double 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 (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch && _touchAnchor)
|
|
{
|
|
const auto contactRect = point.Properties().ContactRect();
|
|
winrt::Windows::Foundation::Point newTouchPoint{ contactRect.X, contactRect.Y };
|
|
const auto anchor = _touchAnchor.value();
|
|
|
|
// Get the difference between the point we've dragged to and the start of the touch.
|
|
const float fontHeight = float(_actualFont.GetSize().Y);
|
|
|
|
const float dy = newTouchPoint.Y - anchor.Y;
|
|
|
|
// If we've moved more than one row of text, we'll want to scroll the viewport
|
|
if (std::abs(dy) > fontHeight)
|
|
{
|
|
// Multiply by -1, because moving the touch point down will
|
|
// create a positive delta, but we want the viewport to move up,
|
|
// so we'll need a negative scroll amount (and the inverse for
|
|
// panning down)
|
|
const float numRows = -1.0f * (dy / fontHeight);
|
|
|
|
const auto currentOffset = this->GetScrollOffset();
|
|
const double newValue = (numRows) + (currentOffset);
|
|
|
|
// Clear our expected scroll offset. The viewport will now move
|
|
// in response to our user input.
|
|
_lastScrollOffset = std::nullopt;
|
|
_scrollBar.Value(static_cast<int>(newValue));
|
|
|
|
// Use this point as our new scroll anchor.
|
|
_touchAnchor = newTouchPoint;
|
|
}
|
|
}
|
|
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(Windows::Foundation::IInspectable const& sender,
|
|
Input::PointerRoutedEventArgs const& args)
|
|
{
|
|
_ReleasePointerCapture(sender, args);
|
|
|
|
const auto ptr = args.Pointer();
|
|
|
|
if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen)
|
|
{
|
|
const auto modifiers = static_cast<uint32_t>(args.KeyModifiers());
|
|
// static_cast to a uint32_t because we can't use the WI_IsFlagSet
|
|
// macro directly with a VirtualKeyModifiers
|
|
const auto shiftEnabled = WI_IsFlagSet(modifiers, static_cast<uint32_t>(VirtualKeyModifiers::Shift));
|
|
|
|
if (_terminal->IsCopyOnSelectActive())
|
|
{
|
|
CopySelectionToClipboard(!shiftEnabled);
|
|
}
|
|
}
|
|
else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch)
|
|
{
|
|
_touchAnchor = std::nullopt;
|
|
}
|
|
|
|
_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.
|
|
// Arguments:
|
|
// - args: the event args containing information about t`he mouse wheel event.
|
|
void TermControl::_MouseWheelHandler(Windows::Foundation::IInspectable const& /*sender*/,
|
|
Input::PointerRoutedEventArgs const& args)
|
|
{
|
|
const auto point = args.GetCurrentPoint(_root);
|
|
const auto delta = point.Properties().MouseWheelDelta();
|
|
|
|
// Get the state of the Ctrl & Shift keys
|
|
// static_cast to a uint32_t because we can't use the WI_IsFlagSet macro
|
|
// directly with a VirtualKeyModifiers
|
|
const auto modifiers = static_cast<uint32_t>(args.KeyModifiers());
|
|
const auto ctrlPressed = WI_IsFlagSet(modifiers, static_cast<uint32_t>(VirtualKeyModifiers::Control));
|
|
const auto shiftPressed = WI_IsFlagSet(modifiers, static_cast<uint32_t>(VirtualKeyModifiers::Shift));
|
|
|
|
if (ctrlPressed && shiftPressed)
|
|
{
|
|
_MouseTransparencyHandler(delta);
|
|
}
|
|
else if (ctrlPressed)
|
|
{
|
|
_MouseZoomHandler(delta);
|
|
}
|
|
else
|
|
{
|
|
_MouseScrollHandler(delta, point);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Adjust the opacity of the acrylic background in response to a mouse
|
|
// scrolling event.
|
|
// Arguments:
|
|
// - mouseDelta: the mouse wheel delta that triggered this event.
|
|
void TermControl::_MouseTransparencyHandler(const double mouseDelta)
|
|
{
|
|
// Transparency is on a scale of [0.0,1.0], so only increment by .01.
|
|
const auto effectiveDelta = mouseDelta < 0 ? -.01 : .01;
|
|
|
|
if (_settings.UseAcrylic())
|
|
{
|
|
try
|
|
{
|
|
auto acrylicBrush = _root.Background().as<Media::AcrylicBrush>();
|
|
acrylicBrush.TintOpacity(acrylicBrush.TintOpacity() + effectiveDelta);
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Adjust the font size of the terminal in response to a mouse scrolling
|
|
// event.
|
|
// Arguments:
|
|
// - mouseDelta: the mouse wheel delta that triggered this event.
|
|
void TermControl::_MouseZoomHandler(const double mouseDelta)
|
|
{
|
|
const auto fontDelta = mouseDelta < 0 ? -1 : 1;
|
|
AdjustFontSize(fontDelta);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Reset the font size of the terminal to its default size.
|
|
// Arguments:
|
|
// - none
|
|
void TermControl::ResetFontSize()
|
|
{
|
|
_SetFontSize(_settings.FontSize());
|
|
}
|
|
|
|
// 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(int fontSizeDelta)
|
|
{
|
|
const auto newSize = _desiredFont.GetEngineSize().Y + fontSizeDelta;
|
|
_SetFontSize(newSize);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Scroll the visible viewport in response to a mouse wheel event.
|
|
// Arguments:
|
|
// - mouseDelta: the mouse wheel delta that triggered this event.
|
|
void TermControl::_MouseScrollHandler(const double mouseDelta, Windows::UI::Input::PointerPoint const& pointerPoint)
|
|
{
|
|
const auto currentOffset = this->GetScrollOffset();
|
|
|
|
// negative = down, positive = up
|
|
// However, for us, the signs are flipped.
|
|
const auto rowDelta = mouseDelta < 0 ? 1.0 : -1.0;
|
|
|
|
// With one of the precision mouses, one click is always a multiple of 120,
|
|
// but the "smooth scrolling" mode results in non-int values
|
|
|
|
double newValue = (_rowsToScroll * rowDelta) + (currentOffset);
|
|
|
|
// Clear our expected scroll offset. The viewport will now move in
|
|
// response to our user input.
|
|
_lastScrollOffset = std::nullopt;
|
|
// The scroll bar's ValueChanged handler will actually move the viewport
|
|
// for us.
|
|
_scrollBar.Value(static_cast<int>(newValue));
|
|
|
|
if (_terminal->IsSelectionActive() && pointerPoint.Properties().IsLeftButtonPressed())
|
|
{
|
|
// If user is mouse selecting and scrolls, they then point at new character.
|
|
// Make sure selection reflects that immediately.
|
|
_SetEndSelectionPointAtCursor(pointerPoint.Position());
|
|
}
|
|
}
|
|
|
|
void TermControl::_ScrollbarChangeHandler(Windows::Foundation::IInspectable const& /*sender*/,
|
|
Controls::Primitives::RangeBaseValueChangedEventArgs const& args)
|
|
{
|
|
const auto newValue = args.NewValue();
|
|
|
|
// If we've stored a lastScrollOffset, that means the terminal has
|
|
// initiated some scrolling operation. We're responding to that event here.
|
|
if (_lastScrollOffset.has_value())
|
|
{
|
|
// If this event's offset is the same as the last offset message
|
|
// we've sent, then clear out the expected offset. We do this
|
|
// because in that case, the message we're replying to was the
|
|
// last scroll event we raised.
|
|
// Regardless, we're going to ignore this message, because the
|
|
// terminal is already in the scroll position it wants.
|
|
const auto ourLastOffset = _lastScrollOffset.value();
|
|
if (newValue == ourLastOffset)
|
|
{
|
|
_lastScrollOffset = std::nullopt;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// This is a scroll event that wasn't initiated by the termnial
|
|
// itself - it was initiated by the mouse wheel, or the scrollbar.
|
|
this->ScrollViewport(static_cast<int>(newValue));
|
|
}
|
|
}
|
|
|
|
// 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(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& 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(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& 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(Windows::UI::Input::PointerPoint const& pointerPoint, const double scrollVelocity)
|
|
{
|
|
// Allow only one pointer at the time
|
|
if (!_autoScrollingPointerPoint.has_value() || _autoScrollingPointerPoint.value().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.has_value())
|
|
{
|
|
_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.has_value() && pointerId == _autoScrollingPointerPoint.value().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(Windows::Foundation::IInspectable const& /* sender */,
|
|
Windows::Foundation::IInspectable const& /* e */)
|
|
{
|
|
if (_autoScrollVelocity != 0)
|
|
{
|
|
const auto timeNow = std::chrono::high_resolution_clock::now();
|
|
|
|
if (_lastAutoScrollUpdateTime.has_value())
|
|
{
|
|
static constexpr double microSecPerSec = 1000000.0;
|
|
const double deltaTime = std::chrono::duration_cast<std::chrono::microseconds>(timeNow - _lastAutoScrollUpdateTime.value()).count() / microSecPerSec;
|
|
_scrollBar.Value(_scrollBar.Value() + _autoScrollVelocity * deltaTime);
|
|
|
|
if (_autoScrollingPointerPoint.has_value())
|
|
{
|
|
_SetEndSelectionPointAtCursor(_autoScrollingPointerPoint.value().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(Windows::Foundation::IInspectable const& /* sender */,
|
|
RoutedEventArgs const& /* args */)
|
|
{
|
|
if (_closing)
|
|
{
|
|
return;
|
|
}
|
|
_focused = true;
|
|
|
|
if (_uiaEngine.get())
|
|
{
|
|
THROW_IF_FAILED(_uiaEngine->Enable());
|
|
}
|
|
|
|
if (_tsfInputControl != nullptr)
|
|
{
|
|
_tsfInputControl.NotifyFocusEnter();
|
|
}
|
|
|
|
if (_cursorTimer.has_value())
|
|
{
|
|
// When the terminal focuses, show the cursor immediately
|
|
_terminal->SetCursorVisible(true);
|
|
_cursorTimer.value().Start();
|
|
}
|
|
_rowsToScroll = _settings.RowsToScroll();
|
|
}
|
|
|
|
// 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(Windows::Foundation::IInspectable const& /* sender */,
|
|
RoutedEventArgs const& /* args */)
|
|
{
|
|
if (_closing)
|
|
{
|
|
return;
|
|
}
|
|
_focused = false;
|
|
|
|
if (_uiaEngine.get())
|
|
{
|
|
THROW_IF_FAILED(_uiaEngine->Disable());
|
|
}
|
|
|
|
if (_tsfInputControl != nullptr)
|
|
{
|
|
_tsfInputControl.NotifyFocusLeave();
|
|
}
|
|
|
|
if (_cursorTimer.has_value())
|
|
{
|
|
_cursorTimer.value().Stop();
|
|
_terminal->SetCursorVisible(false);
|
|
}
|
|
}
|
|
|
|
void TermControl::_SendInputToConnection(const std::wstring& wstr)
|
|
{
|
|
_connection.WriteInput(wstr);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Pre-process text pasted (presumably from the clipboard)
|
|
// before sending it over the terminal's connection, converting
|
|
// Windows-space \r\n line-endings to \r line-endings
|
|
void TermControl::_SendPastedTextToConnection(const std::wstring& wstr)
|
|
{
|
|
// Some notes on this implementation:
|
|
//
|
|
// - std::regex can do this in a single line, but is somewhat
|
|
// overkill for a simple search/replace operation (and its
|
|
// performance guarantees aren't exactly stellar)
|
|
// - The STL doesn't have a simple string search/replace method.
|
|
// This fact is lamentable.
|
|
// - This line-ending converstion is intentionally fairly
|
|
// conservative, to avoid stripping out lone \n characters
|
|
// where they could conceivably be intentional.
|
|
|
|
std::wstring stripped{ wstr };
|
|
|
|
std::wstring::size_type pos = 0;
|
|
|
|
while ((pos = stripped.find(L"\r\n", pos)) != std::wstring::npos)
|
|
{
|
|
stripped.replace(pos, 2, L"\r");
|
|
}
|
|
|
|
_connection.WriteInput(stripped);
|
|
_terminal->TrySnapOnInput();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update the font with the renderer. This will be called either when the
|
|
// font changes or the DPI changes, as DPI changes will necessitate a
|
|
// font change. This method will *not* change the buffer/viewport size
|
|
// to account for the new glyph dimensions. Callers should make sure to
|
|
// appropriately call _DoResize after this method is called.
|
|
// Arguments:
|
|
// - initialUpdate: whether this font update should be considered as being
|
|
// concerned with initialization process. Value forwarded to event handler.
|
|
void TermControl::_UpdateFont(const bool initialUpdate)
|
|
{
|
|
auto lock = _terminal->LockForWriting();
|
|
|
|
const int newDpi = static_cast<int>(static_cast<double>(USER_DEFAULT_SCREEN_DPI) * _swapChainPanel.CompositionScaleX());
|
|
|
|
// TODO: MSFT:20895307 If the font doesn't exist, this doesn't
|
|
// actually fail. We need a way to gracefully fallback.
|
|
_renderer->TriggerFontChange(newDpi, _desiredFont, _actualFont);
|
|
|
|
const auto actualNewSize = _actualFont.GetSize();
|
|
_fontSizeChangedHandlers(actualNewSize.X, actualNewSize.Y, initialUpdate);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Set the font size of the terminal control.
|
|
// Arguments:
|
|
// - fontSize: The size of the font.
|
|
void TermControl::_SetFontSize(int fontSize)
|
|
{
|
|
try
|
|
{
|
|
// Make sure we have a non-zero font size
|
|
const auto newSize = std::max(gsl::narrow<short>(fontSize), static_cast<short>(1));
|
|
const auto* fontFace = _settings.FontFace().c_str();
|
|
_actualFont = { fontFace, 0, 10, { 0, newSize }, CP_UTF8, false };
|
|
_desiredFont = { _actualFont };
|
|
|
|
// Refresh our font with the renderer
|
|
_UpdateFont();
|
|
// Resize the terminal's BUFFER to match the new font size. This does
|
|
// NOT change the size of the window, because that can lead to more
|
|
// problems (like what happens when you change the font size while the
|
|
// window is maximized?)
|
|
auto lock = _terminal->LockForWriting();
|
|
_DoResize(_swapChainPanel.ActualWidth(), _swapChainPanel.ActualHeight());
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
|
|
// 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(winrt::Windows::Foundation::IInspectable const& /*sender*/,
|
|
SizeChangedEventArgs const& e)
|
|
{
|
|
if (!_initializedTerminal)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto lock = _terminal->LockForWriting();
|
|
|
|
const auto foundationSize = e.NewSize();
|
|
|
|
_DoResize(foundationSize.Width, foundationSize.Height);
|
|
}
|
|
|
|
void TermControl::_SwapChainScaleChanged(Windows::UI::Xaml::Controls::SwapChainPanel const& sender,
|
|
Windows::Foundation::IInspectable const& /*args*/)
|
|
{
|
|
const auto scale = sender.CompositionScaleX();
|
|
const auto dpi = (int)(scale * USER_DEFAULT_SCREEN_DPI);
|
|
|
|
// TODO: MSFT: 21169071 - Shouldn't this all happen through _renderer and trigger the invalidate automatically on DPI change?
|
|
THROW_IF_FAILED(_renderEngine->UpdateDpi(dpi));
|
|
_renderer->TriggerRedrawAll();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Toggle the cursor on and off when called by the cursor blink timer.
|
|
// Arguments:
|
|
// - sender: not used
|
|
// - e: not used
|
|
void TermControl::_BlinkCursor(Windows::Foundation::IInspectable const& /* sender */,
|
|
Windows::Foundation::IInspectable const& /* e */)
|
|
{
|
|
if ((_closing) || (!_terminal->IsCursorBlinkingAllowed() && _terminal->IsCursorVisible()))
|
|
{
|
|
return;
|
|
}
|
|
_terminal->SetCursorVisible(!_terminal->IsCursorVisible());
|
|
}
|
|
|
|
// 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(Windows::Foundation::Point const& cursorPosition)
|
|
{
|
|
auto terminalPosition = _GetTerminalPosition(cursorPosition);
|
|
|
|
const short lastVisibleRow = std::max<short>(_terminal->GetViewport().Height() - 1, 0);
|
|
const short lastVisibleCol = std::max<short>(_terminal->GetViewport().Width() - 1, 0);
|
|
|
|
terminalPosition.Y = std::clamp<short>(terminalPosition.Y, 0, lastVisibleRow);
|
|
terminalPosition.X = std::clamp<short>(terminalPosition.X, 0, lastVisibleCol);
|
|
|
|
// save location (for rendering) + render
|
|
_terminal->SetEndSelectionPosition(terminalPosition);
|
|
_renderer->TriggerSelection();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Process a resize event that was initiated by the user. This can either
|
|
// be due to the user resizing the window (causing the swapchain to
|
|
// resize) or due to the DPI changing (causing us to need to resize the
|
|
// buffer to match)
|
|
// Arguments:
|
|
// - newWidth: the new width of the swapchain, in pixels.
|
|
// - newHeight: the new height of the swapchain, in pixels.
|
|
void TermControl::_DoResize(const double newWidth, const double newHeight)
|
|
{
|
|
SIZE size;
|
|
size.cx = static_cast<long>(newWidth);
|
|
size.cy = static_cast<long>(newHeight);
|
|
|
|
// Don't actually resize so small that a single character wouldn't fit
|
|
// in either dimension. The buffer really doesn't like being size 0.
|
|
if (size.cx < _actualFont.GetSize().X || size.cy < _actualFont.GetSize().Y)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Tell the dx engine that our window is now the new size.
|
|
THROW_IF_FAILED(_renderEngine->SetWindowSize(size));
|
|
|
|
// Invalidate everything
|
|
_renderer->TriggerRedrawAll();
|
|
|
|
// Convert our new dimensions to characters
|
|
const auto viewInPixels = Viewport::FromDimensions({ 0, 0 },
|
|
{ static_cast<short>(size.cx), static_cast<short>(size.cy) });
|
|
const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels);
|
|
|
|
// If this function succeeds with S_FALSE, then the terminal didn't
|
|
// actually change size. No need to notify the connection of this
|
|
// no-op.
|
|
// TODO: MSFT:20642295 Resizing the buffer will corrupt it
|
|
// I believe we'll need support for CSI 2J, and additionally I think
|
|
// we're resetting the viewport to the top
|
|
const HRESULT hr = _terminal->UserResize({ vp.Width(), vp.Height() });
|
|
if (SUCCEEDED(hr) && hr != S_FALSE)
|
|
{
|
|
_connection.Resize(vp.Height(), vp.Width());
|
|
}
|
|
}
|
|
|
|
void TermControl::_TerminalTitleChanged(const std::wstring_view& wstr)
|
|
{
|
|
_titleChangedHandlers(winrt::hstring{ wstr });
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update the postion and size of the scrollbar to match the given
|
|
// viewport top, viewport height, and buffer size.
|
|
// The change will be actually handled in _ScrollbarChangeHandler.
|
|
// This should be done on the UI thread. Make sure the caller is calling
|
|
// us in a RunAsync block.
|
|
// 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::_ScrollbarUpdater(Controls::Primitives::ScrollBar scrollBar,
|
|
const int viewTop,
|
|
const int viewHeight,
|
|
const int bufferSize)
|
|
{
|
|
const auto hiddenContent = bufferSize - viewHeight;
|
|
scrollBar.Maximum(hiddenContent);
|
|
scrollBar.Minimum(0);
|
|
scrollBar.ViewportSize(viewHeight);
|
|
|
|
scrollBar.Value(viewTop);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update the postion 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
|
|
winrt::fire_and_forget TermControl::_TerminalScrollPositionChanged(const int viewTop,
|
|
const int viewHeight,
|
|
const int bufferSize)
|
|
{
|
|
// Since this callback fires from non-UI thread, we might be already
|
|
// closed/closing.
|
|
if (_closing.load())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Set this value as our next expected scroll position.
|
|
_lastScrollOffset = { viewTop };
|
|
_scrollPositionChangedHandlers(viewTop, viewHeight, bufferSize);
|
|
|
|
auto weakThis{ get_weak() };
|
|
|
|
co_await winrt::resume_foreground(_scrollBar.Dispatcher());
|
|
|
|
// Even if we weren't closed/closing few lines above, we might be
|
|
// while waiting for this block of code to be dispatched.
|
|
// If 'weakThis' is locked, then we can safely work with 'this'
|
|
if (auto control{ weakThis.get() })
|
|
{
|
|
if (!_closing.load())
|
|
{
|
|
// Update our scrollbar
|
|
_ScrollbarUpdater(_scrollBar, viewTop, viewHeight, bufferSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
hstring TermControl::Title()
|
|
{
|
|
if (!_initializedTerminal)
|
|
return L"";
|
|
|
|
hstring hstr(_terminal->GetConsoleTitle());
|
|
return hstr;
|
|
}
|
|
|
|
// 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:
|
|
// - trimTrailingWhitespace: enable removing any whitespace from copied selection
|
|
// and get text to appear on separate lines.
|
|
bool TermControl::CopySelectionToClipboard(bool trimTrailingWhitespace)
|
|
{
|
|
// no selection --> nothing to copy
|
|
if (_terminal == nullptr || !_terminal->IsSelectionActive())
|
|
{
|
|
return false;
|
|
}
|
|
// extract text from buffer
|
|
const auto bufferData = _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace);
|
|
|
|
// convert text: vector<string> --> string
|
|
std::wstring textData;
|
|
for (const auto& text : bufferData.text)
|
|
{
|
|
textData += text;
|
|
}
|
|
|
|
// convert text to HTML format
|
|
const auto htmlData = TextBuffer::GenHTML(bufferData,
|
|
_actualFont.GetUnscaledSize().Y,
|
|
_actualFont.GetFaceName(),
|
|
_settings.DefaultBackground(),
|
|
"Windows Terminal");
|
|
|
|
// convert to RTF format
|
|
const auto rtfData = TextBuffer::GenRTF(bufferData,
|
|
_actualFont.GetUnscaledSize().Y,
|
|
_actualFont.GetFaceName(),
|
|
_settings.DefaultBackground());
|
|
|
|
if (!_terminal->IsCopyOnSelectActive())
|
|
{
|
|
_terminal->ClearSelection();
|
|
}
|
|
|
|
// send data up for clipboard
|
|
auto copyArgs = winrt::make_self<CopyToClipboardEventArgs>(winrt::hstring(textData.data(), gsl::narrow<winrt::hstring::size_type>(textData.size())),
|
|
winrt::to_hstring(htmlData),
|
|
winrt::to_hstring(rtfData));
|
|
_clipboardCopyHandlers(*this, *copyArgs);
|
|
return true;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Initiate a paste operation.
|
|
void TermControl::PasteTextFromClipboard()
|
|
{
|
|
// attach TermControl::_SendInputToConnection() as the clipboardDataHandler.
|
|
// This is called when the clipboard data is loaded.
|
|
auto clipboardDataHandler = std::bind(&TermControl::_SendPastedTextToConnection, this, std::placeholders::_1);
|
|
auto pasteArgs = winrt::make_self<PasteFromClipboardEventArgs>(clipboardDataHandler);
|
|
|
|
// send paste event up to TermApp
|
|
_clipboardPasteHandlers(*this, *pasteArgs);
|
|
}
|
|
|
|
void TermControl::Close()
|
|
{
|
|
if (!_closing.exchange(true))
|
|
{
|
|
// Stop accepting new output and state changes before we disconnect everything.
|
|
_connection.TerminalOutput(_connectionOutputEventToken);
|
|
_connectionStateChangedRevoker.revoke();
|
|
|
|
if (auto localConnection{ std::exchange(_connection, nullptr) })
|
|
{
|
|
localConnection.Close();
|
|
// connection is destroyed.
|
|
}
|
|
|
|
if (auto localRenderEngine{ std::exchange(_renderEngine, nullptr) })
|
|
{
|
|
if (auto localRenderer{ std::exchange(_renderer, nullptr) })
|
|
{
|
|
localRenderer->TriggerTeardown();
|
|
// renderer is destroyed
|
|
}
|
|
// renderEngine is destroyed
|
|
}
|
|
|
|
if (auto localTerminal{ std::exchange(_terminal, nullptr) })
|
|
{
|
|
_initializedTerminal = false;
|
|
// terminal is destroyed.
|
|
}
|
|
}
|
|
}
|
|
|
|
void TermControl::ScrollViewport(int viewTop)
|
|
{
|
|
_terminal->UserScrollViewport(viewTop);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Scrolls the viewport of the terminal and updates the scroll bar accordingly
|
|
// Arguments:
|
|
// - viewTop: the viewTop to scroll to
|
|
// The difference between this function and ScrollViewport is that this one also
|
|
// updates the _scrollBar after the viewport scroll. The reason _scrollBar is not updated in
|
|
// ScrollViewport is because ScrollViewport is being called by _ScrollbarChangeHandler
|
|
void TermControl::KeyboardScrollViewport(int viewTop)
|
|
{
|
|
_terminal->UserScrollViewport(viewTop);
|
|
_lastScrollOffset = std::nullopt;
|
|
_scrollBar.Value(static_cast<int>(viewTop));
|
|
}
|
|
|
|
int TermControl::GetScrollOffset()
|
|
{
|
|
return _terminal->GetScrollOffset();
|
|
}
|
|
|
|
// 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::GetViewHeight() const
|
|
{
|
|
const auto viewPort = _terminal->GetViewport();
|
|
return viewPort.Height();
|
|
}
|
|
|
|
// 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.
|
|
// Return Value:
|
|
// - a point containing the requested dimensions in pixels.
|
|
winrt::Windows::Foundation::Point TermControl::GetProposedDimensions(IControlSettings const& settings, const uint32_t dpi)
|
|
{
|
|
// Initialize our font information.
|
|
const auto* fontFace = settings.FontFace().c_str();
|
|
const short fontHeight = gsl::narrow<short>(settings.FontSize());
|
|
// 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.
|
|
FontInfo actualFont = { fontFace, 0, 10, { 0, fontHeight }, CP_UTF8, false };
|
|
FontInfoDesired desiredFont = { actualFont };
|
|
|
|
// 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 = std::max(settings.InitialCols(), 1);
|
|
const auto rows = std::max(settings.InitialRows(), 1);
|
|
|
|
// 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 DxEngine
|
|
auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
|
|
THROW_IF_FAILED(dxEngine->UpdateDpi(dpi));
|
|
THROW_IF_FAILED(dxEngine->UpdateFont(desiredFont, actualFont));
|
|
|
|
const float scale = dxEngine->GetScaling();
|
|
const auto fontSize = actualFont.GetSize();
|
|
|
|
// Manually multiply by the scaling factor. The DX engine doesn't
|
|
// actually store the scaled font size in the fontInfo.GetSize()
|
|
// property when the DX engine is in Composition mode (which it is for
|
|
// the Terminal). At runtime, this is fine, as we'll transform
|
|
// everything by our scaling, so it'll work out. However, right now we
|
|
// need to get the exact pixel count.
|
|
const float fFontWidth = gsl::narrow<float>(fontSize.X * scale);
|
|
const float fFontHeight = gsl::narrow<float>(fontSize.Y * scale);
|
|
|
|
// 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);
|
|
|
|
double width = cols * fFontWidth;
|
|
|
|
// Reserve additional space if scrollbar is intended to be visible
|
|
if (settings.ScrollState() == ScrollbarState::Visible)
|
|
{
|
|
width += scrollbarSize;
|
|
}
|
|
|
|
double height = rows * fFontHeight;
|
|
auto thickness = _ParseThicknessFromPadding(settings.Padding());
|
|
width += thickness.Left + thickness.Right;
|
|
height += thickness.Top + thickness.Bottom;
|
|
|
|
return { gsl::narrow_cast<float>(width), gsl::narrow_cast<float>(height) };
|
|
}
|
|
|
|
// Method Description:
|
|
// - Get the size of a single character of this control. The size is in
|
|
// DIPs. If you need it in _pixels_, you'll need to multiply 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
|
|
{
|
|
const auto fontSize = _actualFont.GetSize();
|
|
return { gsl::narrow_cast<float>(fontSize.X), gsl::narrow_cast<float>(fontSize.Y) };
|
|
}
|
|
|
|
// 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() const
|
|
{
|
|
const auto fontSize = _actualFont.GetSize();
|
|
double width = fontSize.X;
|
|
double height = fontSize.Y;
|
|
// Reserve additional space if scrollbar is intended to be visible
|
|
if (_settings.ScrollState() == ScrollbarState::Visible)
|
|
{
|
|
width += _scrollBar.ActualWidth();
|
|
}
|
|
|
|
// Account for the size of any padding
|
|
const auto padding = _swapChainPanel.Margin();
|
|
width += padding.Left + padding.Right;
|
|
height += padding.Top + padding.Bottom;
|
|
|
|
return { gsl::narrow_cast<float>(width), gsl::narrow_cast<float>(height) };
|
|
}
|
|
|
|
// 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
|
|
{
|
|
const auto fontSize = _actualFont.GetSize();
|
|
const auto fontDimension = widthOrHeight ? fontSize.X : fontSize.Y;
|
|
|
|
const auto padding = _swapChainPanel.Margin();
|
|
auto nonTerminalArea = gsl::narrow_cast<float>(widthOrHeight ?
|
|
padding.Left + padding.Right :
|
|
padding.Top + padding.Bottom);
|
|
|
|
if (widthOrHeight && _settings.ScrollState() == ScrollbarState::Visible)
|
|
{
|
|
nonTerminalArea += gsl::narrow_cast<float>(_scrollBar.ActualWidth());
|
|
}
|
|
|
|
const auto gridSize = dimension - nonTerminalArea;
|
|
const int cells = static_cast<int>(gridSize / fontDimension);
|
|
return cells * fontDimension + nonTerminalArea;
|
|
}
|
|
|
|
// 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 wchar_t 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 expection if the input is an invalid double value
|
|
// std::stod will throw out_of_range expection if the input value is more than DBL_MAX
|
|
try
|
|
{
|
|
for (; std::getline(tokenStream, token, singleCharDelim) && (paddingPropIndex < thicknessArr.size()); paddingPropIndex++)
|
|
{
|
|
// std::stod internall 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() const
|
|
{
|
|
CoreWindow 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, 5> modifiers{ {
|
|
{ VirtualKey::RightMenu, ControlKeyStates::RightAltPressed },
|
|
{ VirtualKey::LeftMenu, ControlKeyStates::LeftAltPressed },
|
|
{ VirtualKey::RightControl, ControlKeyStates::RightCtrlPressed },
|
|
{ VirtualKey::LeftControl, ControlKeyStates::LeftCtrlPressed },
|
|
{ VirtualKey::Shift, ControlKeyStates::ShiftPressed },
|
|
} };
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Gets the corresponding viewport terminal position for the cursor
|
|
// by excluding the padding and normalizing with the font size.
|
|
// This is used for selection.
|
|
// 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 for the given Point parameter
|
|
const COORD TermControl::_GetTerminalPosition(winrt::Windows::Foundation::Point cursorPosition)
|
|
{
|
|
// Exclude padding from cursor position calculation
|
|
COORD terminalPosition = {
|
|
static_cast<SHORT>(cursorPosition.X - _swapChainPanel.Margin().Left),
|
|
static_cast<SHORT>(cursorPosition.Y - _swapChainPanel.Margin().Top)
|
|
};
|
|
|
|
const auto fontSize = _actualFont.GetSize();
|
|
FAIL_FAST_IF(fontSize.X == 0);
|
|
FAIL_FAST_IF(fontSize.Y == 0);
|
|
|
|
// Normalize to terminal coordinates by using font size
|
|
terminalPosition.X /= fontSize.X;
|
|
terminalPosition.Y /= fontSize.Y;
|
|
|
|
return terminalPosition;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Composition Completion handler for the TSFInputControl that
|
|
// handles writing text out to TerminalConnection
|
|
// Arguments:
|
|
// - text: the text to write to TerminalConnection
|
|
// Return Value:
|
|
// - <none>
|
|
void TermControl::_CompositionCompleted(winrt::hstring text)
|
|
{
|
|
_connection.WriteInput(text);
|
|
}
|
|
|
|
// Method Description:
|
|
// - CurrentCursorPosition handler for the TSFInputControl that
|
|
// handles returning current cursor position.
|
|
// Arguments:
|
|
// - eventArgs: event for storing the current cursor position
|
|
// Return Value:
|
|
// - <none>
|
|
void TermControl::_CurrentCursorPositionHandler(const IInspectable& /*sender*/, const CursorPositionEventArgs& eventArgs)
|
|
{
|
|
const COORD cursorPos = _terminal->GetCursorPosition();
|
|
Windows::Foundation::Point p = { gsl::narrow<float>(cursorPos.X), gsl::narrow<float>(cursorPos.Y) };
|
|
eventArgs.CurrentPosition(p);
|
|
}
|
|
|
|
// Method Description:
|
|
// - FontInfo handler for the TSFInputControl that
|
|
// handles returning current font information
|
|
// Arguments:
|
|
// - eventArgs: event for storing the current font information
|
|
// Return Value:
|
|
// - <none>
|
|
void TermControl::_FontInfoHandler(const IInspectable& /*sender*/, const FontInfoEventArgs& eventArgs)
|
|
{
|
|
eventArgs.FontSize(CharacterDimensions());
|
|
eventArgs.FontFace(_actualFont.GetFaceName());
|
|
}
|
|
|
|
// Method Description:
|
|
// - Returns the number of clicks that occurred (double and triple click support)
|
|
// Arguments:
|
|
// - clickPos: the (x,y) position of a given cursor (i.e.: mouse cursor).
|
|
// NOTE: origin (0,0) is top-left.
|
|
// - clickTime: the timestamp that the click occurred
|
|
// Return Value:
|
|
// - if the click is in the same position as the last click and within the timeout, the number of clicks within that time window
|
|
// - otherwise, 1
|
|
const unsigned int TermControl::_NumberOfClicks(winrt::Windows::Foundation::Point clickPos, Timestamp clickTime)
|
|
{
|
|
// if click occurred at a different location or past the multiClickTimer...
|
|
Timestamp delta;
|
|
THROW_IF_FAILED(UInt64Sub(clickTime, _lastMouseClick, &delta));
|
|
if (clickPos != _lastMouseClickPos || delta > _multiClickTimer)
|
|
{
|
|
// exit early. This is a single click.
|
|
_multiClickCounter = 1;
|
|
}
|
|
else
|
|
{
|
|
_multiClickCounter++;
|
|
}
|
|
return _multiClickCounter;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// -------------------------------- WinRT Events ---------------------------------
|
|
// Winrt events need a method for adding a callback to the event and removing the callback.
|
|
// These macros will define them both for you.
|
|
DEFINE_EVENT(TermControl, TitleChanged, _titleChangedHandlers, TerminalControl::TitleChangedEventArgs);
|
|
DEFINE_EVENT(TermControl, FontSizeChanged, _fontSizeChangedHandlers, TerminalControl::FontSizeChangedEventArgs);
|
|
DEFINE_EVENT(TermControl, ScrollPositionChanged, _scrollPositionChangedHandlers, TerminalControl::ScrollPositionChangedEventArgs);
|
|
|
|
DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(TermControl, PasteFromClipboard, _clipboardPasteHandlers, TerminalControl::TermControl, TerminalControl::PasteFromClipboardEventArgs);
|
|
DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(TermControl, CopyToClipboard, _clipboardCopyHandlers, TerminalControl::TermControl, TerminalControl::CopyToClipboardEventArgs);
|
|
// clang-format on
|
|
}
|