terminal/src/cascadia/TerminalControl/TermControl.cpp
msftbot[bot] c6879d75af
Make Korean IME input more consistent (#4796)
## Summary of the Pull Request
Korean IME was not working correctly due to way we were clearing the input buffer inside of `TSFInputControl`. We wanted to clear our input buffer and tell TSF to clear its input buffer as well when we receive a `CompositionCompleted` event. This works fine in some IME languages such as Chinese and Japanese. However, Korean IME composes characters differently in such a way where we can't tell TSF to clear their buffer during a `CompositionCompleted` event because it would clear the character that triggered the `CompositionCompleted` event in the first place.

The solution in this PR is to keep our `_inputBuffer` intact until the user presses <kbd>Enter</kbd> or <kbd>Esc</kbd>, in which case we clear our buffer and the TSF buffer. I've chosen these two keys because it seems to make sense to clear the buffer after text is sent to the terminal with <kbd>Enter</kbd>, and <kbd>Esc</kbd> usually means to cancel a current composition anyway.

This means we need to keep track of our last known "Composition Start Point", which is represented by `_activeTextStart`. Whenever we complete a composition, we'll send the portion of the input buffer between `_activeTextStart` and the end of the input buffer to the terminal. Then, we'll update `_activeTextStart` to be the end of the input buffer so that the next time we send text to the terminal, we'll only send the portion of our buffer that's "active".

## PR Checklist
* [x] Closes #4226
* [x] CLA signed
* [x] Tests added/passed

## Validation Steps Performed
Manual testing with Chinese, Japanese, and Korean IME.
2020-03-04 20:01:01 +00:00

2205 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 <LibraryResources.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::Input;
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;
using namespace winrt::Windows::ApplicationModel::DataTransfer;
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 },
_settings{ settings },
_closing{ false },
_isTerminalInitiatedScroll{ false },
_autoScrollVelocity{ 0 },
_autoScrollingPointerPoint{ std::nullopt },
_autoScrollTimer{},
_lastAutoScrollUpdateTime{ std::nullopt },
_desiredFont{ DEFAULT_FONT_FACE, 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8 },
_actualFont{ DEFAULT_FONT_FACE, 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false },
_touchAnchor{ std::nullopt },
_cursorTimer{},
_lastMouseClick{},
_lastMouseClickPos{},
_searchBox{ nullptr },
_unfocusedClickPos{ std::nullopt },
_isClickDragSelection{ false }
{
_EnsureStaticInitialization();
InitializeComponent();
// 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();
}
});
// 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);
});
_ApplyUISettings();
}
// 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::TerminalControl::SearchBoxControl>() })
{
// get at its private implementation
_searchBox.copy_from(winrt::get_self<implementation::SearchBoxControl>(searchBox));
_searchBox->Visibility(Visibility::Visible);
_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->SetBlockSelection(false);
search.Select();
_renderer->TriggerSelection();
}
}
// 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*/, RoutedEventArgs const& /*args*/)
{
_searchBox->Visibility(Visibility::Collapsed);
// 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(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);
}
}
}
// 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());
SwapChainPanel().Margin(newMargin);
// Initialize our font information.
const auto fontFace = _settings.FontFace();
const short fontHeight = gsl::narrow_cast<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);
// Apply settings for scrollbar
if (_settings.ScrollState() == ScrollbarState::Hidden)
{
// 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().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);
}
// 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 = RootGrid().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 (RootGrid().Background() != acrylic)
{
RootGrid().Background(acrylic);
}
}
else
{
Media::SolidColorBrush solidColor{};
RootGrid().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 = BackgroundImage().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);
BackgroundImage().Source(image);
}
// Apply stretch, opacity and alignment settings
BackgroundImage().Stretch(_settings.BackgroundImageStretchMode());
BackgroundImage().Opacity(_settings.BackgroundImageOpacity());
BackgroundImage().HorizontalAlignment(_settings.BackgroundImageHorizontalAlignment());
BackgroundImage().VerticalAlignment(_settings.BackgroundImageVerticalAlignment());
}
else
{
BackgroundImage().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(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 = RootGrid().Background().try_as<Media::AcrylicBrush>())
{
acrylic.FallbackColor(bgColor);
acrylic.TintColor(bgColor);
}
else if (auto solidColor = RootGrid().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()
{
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(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(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 Terminal is initialized.
dxEngine->SetRetroTerminalEffects(_settings.RetroTerminalEffect());
// TODO:GH#3927 - hot-reload this one too
// Update DxEngine's AntialiasingMode
switch (_settings.AntialiasingMode())
{
case TextAntialiasingMode::Cleartype:
dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE);
break;
case TextAntialiasingMode::Aliased:
dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_ALIASED);
break;
case TextAntialiasingMode::Grayscale:
default:
dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
break;
}
THROW_IF_FAILED(dxEngine->Enable());
_renderEngine = std::move(dxEngine);
// This event is explicitly revoked in the destructor: does not need weak_ref
auto onReceiveOutputFn = [this](const hstring str) {
_terminal->Write(str);
};
_connectionOutputEventToken = _connection.TerminalOutput(onReceiveOutputFn);
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;
ScrollBar().Maximum(bufferHeight - bufferHeight);
ScrollBar().Minimum(0);
ScrollBar().Value(0);
ScrollBar().ViewportSize(bufferHeight);
localPointerToThread->EnablePainting();
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;
// Focus the control here. If we do it during control initialization, then
// 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;
_InitializedHandlers(*this, nullptr);
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)
{
// When there is a selection active, escape should clear it and NOT flow through
// to the terminal. With any other keypress, it should clear the selection AND
// flow through to the terminal.
if (_terminal->IsSelectionActive())
{
_terminal->ClearSelection();
_renderer->TriggerSelection();
if (vkey == VK_ESCAPE)
{
return true;
}
}
if (vkey == VK_ESCAPE ||
vkey == VK_RETURN)
{
TSFInputControl().ClearBuffer();
}
// 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 ? _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 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(Windows::Foundation::IInspectable const& sender,
Input::PointerRoutedEventArgs const& args)
{
_CapturePointer(sender, args);
const auto ptr = args.Pointer();
const auto point = args.GetCurrentPoint(*this);
if (!_focused)
{
Focus(FocusState::Pointer);
// Save the click position here when the terminal does not have focus
// because they might be performing a click-drag selection. Since we
// only want to start the selection when the user moves the pointer with
// the left mouse button held down, the PointerMovedHandler will use
// this saved position to set the SelectionAnchor.
_unfocusedClickPos = point.Position();
}
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 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())
{
// _unfocusedClickPos having a value signifies to us that
// the user clicked on an unfocused terminal. We don't want
// a single left click from out of focus to start a selection,
// so we return fast here.
if (_unfocusedClickPos)
{
args.Handled(true);
return;
}
const auto cursorPosition = point.Position();
const auto terminalPosition = _GetTerminalPosition(cursorPosition);
// handle ALT key
_terminal->SetBlockSelection(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->MultiClickSelection(terminalPosition, ::Terminal::SelectionExpansionMode::Line);
}
else if (multiClickMapper == 2)
{
_terminal->MultiClickSelection(terminalPosition, ::Terminal::SelectionExpansionMode::Word);
}
else
{
if (shiftEnabled && _terminal->IsSelectionActive())
{
_terminal->SetSelectionEnd(terminalPosition, ::Terminal::SelectionExpansionMode::Cell);
}
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(*this);
if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen)
{
if (point.Properties().IsLeftButtonPressed())
{
_isClickDragSelection = true;
// If this does not have a value, it means that PointerPressedHandler already
// set the SelectionAnchor. If it does have a value, that means the user is
// performing a click-drag selection on an unfocused terminal, so
// a SelectionAnchor isn't set yet. We'll have to set it here.
if (_unfocusedClickPos)
{
_terminal->SetSelectionAnchor(_GetTerminalPosition(*_unfocusedClickPos));
}
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;
// Start viewport scroll after we've moved more than a half row of text
if (std::abs(dy) > (fontHeight / 2.0f))
{
// Multiply by -1, because moving the touch point down will
// create a positive delta, but we want the viewport to move up,
// so we'll need a negative scroll amount (and the inverse for
// panning down)
const float numRows = -1.0f * (dy / fontHeight);
const auto currentOffset = ::base::ClampedNumeric<double>(ScrollBar().Value());
const auto newValue = numRows + currentOffset;
ScrollBar().Value(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())
{
// If the terminal was in focus, copy to clipboard.
// If the terminal was unfocused AND a click-drag selection happened, copy to clipboard.
if (!_unfocusedClickPos || (_unfocusedClickPos && _isClickDragSelection))
{
CopySelectionToClipboard(!shiftEnabled);
}
}
}
else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch)
{
_touchAnchor = std::nullopt;
}
_unfocusedClickPos = std::nullopt;
_isClickDragSelection = false;
_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(*this);
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 = RootGrid().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 = ScrollBar().Value();
// negative = down, positive = up
// However, for us, the signs are flipped.
const auto rowDelta = mouseDelta / (-1.0 * WHEEL_DELTA);
// 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);
// The scroll bar's ValueChanged handler will actually move the viewport
// for us.
ScrollBar().Value(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)
{
if (_isTerminalInitiatedScroll)
{
return;
}
const auto newValue = static_cast<int>(args.NewValue());
// This is a scroll event that wasn't initiated by the terminal
// itself - it was initiated by the mouse wheel, or the scrollbar.
_terminal->UserScrollViewport(newValue);
// We've just told the terminal to update its viewport to reflect the
// new scroll value so the scroll bar matches the viewport now.
_willUpdateScrollBarToMatchViewport.store(false);
}
// 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 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 (_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);
}
}
// 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::_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 conversion 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<short>(gsl::narrow_cast<short>(fontSize), 1);
const auto fontFace = _settings.FontFace();
_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*/)
{
if (_renderEngine)
{
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->SetSelectionEnd(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;
}
_terminal->ClearSelection();
// 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 position 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)
{
// The terminal is already in the scroll position it wants, so no need
// to tell it to scroll.
_isTerminalInitiatedScroll = true;
const auto hiddenContent = bufferSize - viewHeight;
scrollBar.Maximum(hiddenContent);
scrollBar.Minimum(0);
scrollBar.ViewportSize(viewHeight);
scrollBar.Value(viewTop);
_isTerminalInitiatedScroll = false;
}
// 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
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;
}
_scrollPositionChangedHandlers(viewTop, viewHeight, bufferSize);
auto weakThis{ get_weak() };
co_await winrt::resume_foreground(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;
}
hstring TermControl::GetProfileName() const
{
return _settings.ProfileName();
}
// 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();
_renderer->TriggerSelection();
}
// send data up for clipboard
auto copyArgs = winrt::make_self<CopyToClipboardEventArgs>(winrt::hstring(textData),
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();
TSFInputControl().Close(); // Disconnect the TSF input control so it doesn't receive EditContext events.
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.
}
}
}
// 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::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();
const short fontHeight = gsl::narrow_cast<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_cast<float>(fontSize.X * scale);
const float fFontHeight = gsl::narrow_cast<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 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 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 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() 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_cast<float>(cursorPos.X), gsl::narrow_cast<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;
}
// 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::_DoDragDrop(DragEventArgs const e)
{
if (e.DataView().Contains(StandardDataFormats::StorageItems()))
{
auto items = co_await e.DataView().GetStorageItemsAsync();
if (items.Size() > 0)
{
std::wstring allPaths;
for (auto item : items)
{
// Join the paths with spaces
if (!allPaths.empty())
{
allPaths += L" ";
}
std::wstring fullPath{ item.Path() };
const auto containsSpaces = std::find(fullPath.begin(),
fullPath.end(),
L' ') != fullPath.end();
auto lock = _terminal->LockForWriting();
if (containsSpaces)
{
fullPath.insert(0, L"\"");
fullPath += L"\"";
}
allPaths += fullPath;
}
_SendInputToConnection(allPaths);
}
}
}
// Method Description:
// - Synchronous handler for the "Drop" event. We'll dispatch the async
// _DoDragDrop method to handle this, because getting information about
// the file that was potentially dropped onto us must be done off the UI
// thread.
// Arguments:
// - e: The DragEventArgs from the Drop event
// Return Value:
// - <none>
void TermControl::_DragDropHandler(Windows::Foundation::IInspectable const& /*sender*/,
DragEventArgs const& e)
{
// Dispatch an async method to handle the drop event.
_DoDragDrop(e);
}
// 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(Windows::Foundation::IInspectable const& /*sender*/,
DragEventArgs const& e)
{
if (!e.DataView().Contains(StandardDataFormats::StorageItems()))
{
// We can't do anything for non-storageitems right now.
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
e.DragUIOverride().Caption(RS_(L"DragFileCaption"));
// 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 visibile
e.DragUIOverride().IsGlyphVisible(false);
}
// -------------------------------- 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
}