terminal/src/cascadia/TerminalControl/TSFInputControl.cpp
Carlos Zamora 1c6aa4d109
Move ICore/ControlSettings to TerminalControl project (#7167)
## Summary of the Pull Request
Move `ICoreSettings` and `IControlSettings` from the TerminalSettings project to the TerminalCore and TerminalControl projects respectively. Also entirely removes the TerminalSettings project.

The purpose of these interfaces is unchanged. `ICoreSettings` is used to instantiate a terminal. `IControlSettings` (which requires an `ICoreSettings`) is used to instantiate a UWP terminal control.

## References
Closes #7140 
Related Epic: #885 
Related Spec: #6904 

## PR Checklist
* [X] Closes #7140 
* [X] CLA signed
* [X] Tests ~added~/passed (no additional tests necessary)
* [X] ~Documentation updated~
* [X] ~Schema updated~

## Detailed Description of the Pull Request / Additional comments
A lot of the work here was having to deal with winmd files across all of these projects. The TerminalCore project now outputs a Microsoft.Terminal.TerminalControl.winmd. Some magic happens in TerminalControl.vcxproj to get this to work properly.

## Validation Steps Performed
Deployed Windows Terminal and opened a few new tabs.
2020-08-07 14:46:52 +00:00

474 lines
20 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "TSFInputControl.h"
#include "TSFInputControl.g.cpp"
#include <Utils.h>
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Graphics::Display;
using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::UI::Text;
using namespace winrt::Windows::UI::Text::Core;
using namespace winrt::Windows::UI::Xaml;
namespace winrt::Microsoft::Terminal::TerminalControl::implementation
{
TSFInputControl::TSFInputControl() :
_editContext{ nullptr },
_inComposition{ false },
_activeTextStart{ 0 },
_focused{ false },
_currentTerminalCursorPos{ 0, 0 },
_currentCanvasWidth{ 0.0 },
_currentTextBlockHeight{ 0.0 },
_currentTextBounds{ 0, 0, 0, 0 },
_currentControlBounds{ 0, 0, 0, 0 },
_currentWindowBounds{ 0, 0, 0, 0 }
{
InitializeComponent();
// Create a CoreTextEditingContext for since we are acting like a custom edit control
auto manager = CoreTextServicesManager::GetForCurrentView();
_editContext = manager.CreateEditContext();
// InputPane is manually shown inside of TermControl.
_editContext.InputPaneDisplayPolicy(CoreTextInputPaneDisplayPolicy::Manual);
// set the input scope to Text because this control is for any text.
_editContext.InputScope(CoreTextInputScope::Text);
_textRequestedRevoker = _editContext.TextRequested(winrt::auto_revoke, { this, &TSFInputControl::_textRequestedHandler });
_selectionRequestedRevoker = _editContext.SelectionRequested(winrt::auto_revoke, { this, &TSFInputControl::_selectionRequestedHandler });
_focusRemovedRevoker = _editContext.FocusRemoved(winrt::auto_revoke, { this, &TSFInputControl::_focusRemovedHandler });
_textUpdatingRevoker = _editContext.TextUpdating(winrt::auto_revoke, { this, &TSFInputControl::_textUpdatingHandler });
_selectionUpdatingRevoker = _editContext.SelectionUpdating(winrt::auto_revoke, { this, &TSFInputControl::_selectionUpdatingHandler });
_formatUpdatingRevoker = _editContext.FormatUpdating(winrt::auto_revoke, { this, &TSFInputControl::_formatUpdatingHandler });
_layoutRequestedRevoker = _editContext.LayoutRequested(winrt::auto_revoke, { this, &TSFInputControl::_layoutRequestedHandler });
_compositionStartedRevoker = _editContext.CompositionStarted(winrt::auto_revoke, { this, &TSFInputControl::_compositionStartedHandler });
_compositionCompletedRevoker = _editContext.CompositionCompleted(winrt::auto_revoke, { this, &TSFInputControl::_compositionCompletedHandler });
}
// Method Description:
// - Prepares this TSFInputControl to be removed from the UI hierarchy.
void TSFInputControl::Close()
{
// Explicitly disconnect the LayoutRequested handler -- it can cause problems during application teardown.
// See GH#4159 for more info.
// Also disconnect compositionCompleted and textUpdating explicitly. It seems to occasionally cause problems if
// a composition is active during application teardown.
_layoutRequestedRevoker.revoke();
_compositionCompletedRevoker.revoke();
_textUpdatingRevoker.revoke();
}
// Method Description:
// - NotifyFocusEnter handler for notifying CoreEditTextContext of focus enter
// when TerminalControl receives focus.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TSFInputControl::NotifyFocusEnter()
{
if (_editContext != nullptr)
{
_editContext.NotifyFocusEnter();
_focused = true;
}
}
// Method Description:
// - NotifyFocusEnter handler for notifying CoreEditTextContext of focus leaving.
// when TerminalControl no longer has focus.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TSFInputControl::NotifyFocusLeave()
{
if (_editContext != nullptr)
{
_editContext.NotifyFocusLeave();
_focused = false;
}
}
// Method Description:
// - Clears the input buffer and tells the text server to clear their buffer as well.
// Also clears the TextBlock and sets the active text starting point to 0.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TSFInputControl::ClearBuffer()
{
if (!_inputBuffer.empty())
{
TextBlock().Text(L"");
const auto bufLen = ::base::ClampedNumeric<int32_t>(_inputBuffer.length());
_inputBuffer.clear();
_editContext.NotifyFocusLeave();
_editContext.NotifyTextChanged({ 0, bufLen }, 0, { 0, 0 });
_editContext.NotifyFocusEnter();
_activeTextStart = 0;
_inComposition = false;
}
}
// Method Description:
// - Redraw the canvas if certain dimensions have changed since the last
// redraw. This includes the Terminal cursor position, the Canvas width, and the TextBlock height.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TSFInputControl::TryRedrawCanvas()
{
if (!_focused)
{
return;
}
// Get the cursor position in text buffer position
auto cursorArgs = winrt::make_self<CursorPositionEventArgs>();
_CurrentCursorPositionHandlers(*this, *cursorArgs);
const til::point cursorPos{ gsl::narrow_cast<ptrdiff_t>(cursorArgs->CurrentPosition().X), gsl::narrow_cast<ptrdiff_t>(cursorArgs->CurrentPosition().Y) };
const double actualCanvasWidth{ Canvas().ActualWidth() };
const double actualTextBlockHeight{ TextBlock().ActualHeight() };
const auto actualWindowBounds{ CoreWindow::GetForCurrentThread().Bounds() };
if (_currentTerminalCursorPos == cursorPos &&
_currentCanvasWidth == actualCanvasWidth &&
_currentTextBlockHeight == actualTextBlockHeight &&
_currentWindowBounds == actualWindowBounds)
{
return;
}
_currentTerminalCursorPos = cursorPos;
_currentCanvasWidth = actualCanvasWidth;
_currentTextBlockHeight = actualTextBlockHeight;
_currentWindowBounds = actualWindowBounds;
_RedrawCanvas();
}
// Method Description:
// - Redraw the Canvas and update the current Text Bounds and Control Bounds for
// the CoreTextEditContext.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TSFInputControl::_RedrawCanvas()
{
// Get Font Info as we use this is the pixel size for characters in the display
auto fontArgs = winrt::make_self<FontInfoEventArgs>();
_CurrentFontInfoHandlers(*this, *fontArgs);
const til::size fontSize{ til::math::flooring, fontArgs->FontSize() };
// Convert text buffer cursor position to client coordinate position
// within the window. This point is in _pixels_
const til::point clientCursorPos{ _currentTerminalCursorPos * fontSize };
// Get scale factor for view
const double scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel();
const til::point clientCursorInDips{ clientCursorPos / scaleFactor };
// Position our TextBlock at the cursor position
Canvas().SetLeft(TextBlock(), clientCursorInDips.x<double>());
Canvas().SetTop(TextBlock(), clientCursorInDips.y<double>());
// calculate FontSize in pixels from Points
const double fontSizePx = (fontSize.height<double>() * 72) / USER_DEFAULT_SCREEN_DPI;
const double unscaledFontSizePx = fontSizePx / scaleFactor;
// Make sure to unscale the font size to correct for DPI! XAML needs
// things in DIPs, and the fontSize is in pixels.
TextBlock().FontSize(unscaledFontSizePx);
TextBlock().FontFamily(Media::FontFamily(fontArgs->FontFace()));
TextBlock().FontWeight(fontArgs->FontWeight());
// TextBlock's actual dimensions right after initialization is 0w x 0h. So,
// if an IME is displayed before TextBlock has text (like showing the emoji picker
// using Win+.), it'll be placed higher than intended.
TextBlock().MinWidth(unscaledFontSizePx);
TextBlock().MinHeight(unscaledFontSizePx);
_currentTextBlockHeight = std::max(unscaledFontSizePx, _currentTextBlockHeight);
const auto widthToTerminalEnd = _currentCanvasWidth - clientCursorInDips.x<double>();
// Make sure that we're setting the MaxWidth to a positive number - a
// negative number here will crash us in mysterious ways with a useless
// stack trace
const auto newMaxWidth = std::max<double>(0.0, widthToTerminalEnd);
TextBlock().MaxWidth(newMaxWidth);
// Get window in screen coordinates, this is the entire window including
// tabs. THIS IS IN DIPs
const til::point windowOrigin{ til::math::flooring, _currentWindowBounds };
// Get the offset (margin + tabs, etc..) of the control within the window
const til::point controlOrigin{ til::math::flooring,
this->TransformToVisual(nullptr).TransformPoint(Point(0, 0)) };
// The controlAbsoluteOrigin is the origin of the control relative to
// the origin of the displays. THIS IS IN DIPs
const til::point controlAbsoluteOrigin{ windowOrigin + controlOrigin };
// Convert the control origin to pixels
const til::point scaledFrameOrigin = controlAbsoluteOrigin * scaleFactor;
// Get the location of the cursor in the display, in pixels.
til::point screenCursorPos{ scaledFrameOrigin + clientCursorPos };
// GH #5007 - make sure to account for wrapping the IME composition at
// the right side of the viewport.
const ptrdiff_t textBlockHeight = ::base::ClampMul(_currentTextBlockHeight, scaleFactor);
// Get the bounds of the composition text, in pixels.
const til::rectangle textBounds{ til::point{ screenCursorPos.x(), screenCursorPos.y() },
til::size{ 0, textBlockHeight } };
_currentTextBounds = textBounds;
_currentControlBounds = Rect(screenCursorPos.x<float>(),
screenCursorPos.y<float>(),
0,
fontSize.height<float>());
}
// Method Description:
// - Handler for LayoutRequested event by CoreEditContext responsible
// for returning the current position the IME should be placed
// in screen coordinates on the screen. TSFInputControls internal
// XAML controls (TextBlock/Canvas) are also positioned and updated.
// NOTE: documentation says application should handle this event
// Arguments:
// - sender: CoreTextEditContext sending the request.
// - args: CoreTextLayoutRequestedEventArgs to be updated with position information.
// Return Value:
// - <none>
void TSFInputControl::_layoutRequestedHandler(CoreTextEditContext sender, CoreTextLayoutRequestedEventArgs const& args)
{
auto request = args.Request();
TryRedrawCanvas();
// Set the text block bounds
request.LayoutBounds().TextBounds(_currentTextBounds);
// Set the control bounds of the whole control
request.LayoutBounds().ControlBounds(_currentControlBounds);
}
// Method Description:
// - Handler for CompositionStarted event by CoreEditContext responsible
// for making internal TSFInputControl controls visible.
// Arguments:
// - sender: CoreTextEditContext sending the request. Not used in method.
// - args: CoreTextCompositionStartedEventArgs. Not used in method.
// Return Value:
// - <none>
void TSFInputControl::_compositionStartedHandler(CoreTextEditContext sender, CoreTextCompositionStartedEventArgs const& /*args*/)
{
_inComposition = true;
}
// Method Description:
// - Handler for CompositionCompleted event by CoreEditContext responsible
// for making internal TSFInputControl controls visible.
// Arguments:
// - sender: CoreTextEditContext sending the request. Not used in method.
// - args: CoreTextCompositionCompletedEventArgs. Not used in method.
// Return Value:
// - <none>
void TSFInputControl::_compositionCompletedHandler(CoreTextEditContext sender, CoreTextCompositionCompletedEventArgs const& /*args*/)
{
_inComposition = false;
// only need to do work if the current buffer has text
if (!_inputBuffer.empty())
{
_SendAndClearText();
}
}
// Method Description:
// - Handler for FocusRemoved event by CoreEditContext responsible
// for removing focus for the TSFInputControl control accordingly
// when focus was forcibly removed from text input control.
// NOTE: Documentation says application should handle this event
// Arguments:
// - sender: CoreTextEditContext sending the request. Not used in method.
// - object: CoreTextCompositionStartedEventArgs. Not used in method.
// Return Value:
// - <none>
void TSFInputControl::_focusRemovedHandler(CoreTextEditContext sender, winrt::Windows::Foundation::IInspectable const& /*object*/)
{
}
// Method Description:
// - Handler for TextRequested event by CoreEditContext responsible
// for returning the range of text requested.
// NOTE: Documentation says application should handle this event
// Arguments:
// - sender: CoreTextEditContext sending the request. Not used in method.
// - args: CoreTextTextRequestedEventArgs to be updated with requested range text.
// Return Value:
// - <none>
void TSFInputControl::_textRequestedHandler(CoreTextEditContext sender, CoreTextTextRequestedEventArgs const& args)
{
// the range the TSF wants to know about
const auto range = args.Request().Range();
try
{
const auto textEnd = ::base::ClampMin<size_t>(range.EndCaretPosition, _inputBuffer.length());
const auto length = ::base::ClampSub<size_t>(textEnd, range.StartCaretPosition);
const auto textRequested = _inputBuffer.substr(range.StartCaretPosition, length);
args.Request().Text(textRequested);
}
CATCH_LOG();
}
// Method Description:
// - Handler for SelectionRequested event by CoreEditContext responsible
// for returning the currently selected text.
// TSFInputControl currently doesn't allow selection, so nothing happens.
// NOTE: Documentation says application should handle this event
// Arguments:
// - sender: CoreTextEditContext sending the request. Not used in method.
// - args: CoreTextSelectionRequestedEventArgs for providing data for the SelectionRequested event. Not used in method.
// Return Value:
// - <none>
void TSFInputControl::_selectionRequestedHandler(CoreTextEditContext sender, CoreTextSelectionRequestedEventArgs const& /*args*/)
{
}
// Method Description:
// - Handler for SelectionUpdating event by CoreEditContext responsible
// for handling modifications to the range of text currently selected.
// TSFInputControl doesn't currently allow selection, so nothing happens.
// NOTE: Documentation says application should set its selection range accordingly
// Arguments:
// - sender: CoreTextEditContext sending the request. Not used in method.
// - args: CoreTextSelectionUpdatingEventArgs for providing data for the SelectionUpdating event. Not used in method.
// Return Value:
// - <none>
void TSFInputControl::_selectionUpdatingHandler(CoreTextEditContext sender, CoreTextSelectionUpdatingEventArgs const& /*args*/)
{
}
// Method Description:
// - Handler for TextUpdating event by CoreEditContext responsible
// for handling text updates.
// Arguments:
// - sender: CoreTextEditContext sending the request. Not used in method.
// - args: CoreTextTextUpdatingEventArgs contains new text to update buffer with.
// Return Value:
// - <none>
void TSFInputControl::_textUpdatingHandler(CoreTextEditContext sender, CoreTextTextUpdatingEventArgs const& args)
{
const auto incomingText = args.Text();
const auto range = args.Range();
try
{
// When a user deletes the last character in their current composition, some machines
// will fire a CompositionCompleted before firing a TextUpdating event that deletes the last character.
// The TextUpdating will have a lower StartCaretPosition, so in this scenario, _activeTextStart
// needs to update to be the StartCaretPosition.
// A known issue related to this behavior is that the last character that's deleted from a composition
// will get sent to the terminal before we receive the TextUpdate to delete the character.
// See GH #5054.
_activeTextStart = ::base::ClampMin(_activeTextStart, ::base::ClampedNumeric<size_t>(range.StartCaretPosition));
_inputBuffer = _inputBuffer.replace(
range.StartCaretPosition,
::base::ClampSub<size_t>(range.EndCaretPosition, range.StartCaretPosition),
incomingText);
// Emojis/Kaomojis/Symbols chosen through the IME without starting composition
// will be sent straight through to the terminal.
if (!_inComposition)
{
_SendAndClearText();
}
else
{
Canvas().Visibility(Visibility::Visible);
const auto text = _inputBuffer.substr(_activeTextStart);
TextBlock().Text(text);
}
// Notify the TSF that the update succeeded
args.Result(CoreTextTextUpdatingResult::Succeeded);
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
// indicate updating failed.
args.Result(CoreTextTextUpdatingResult::Failed);
}
}
// Method Description:
// - Send the portion of the textBuffer starting at _activeTextStart to the end of the buffer.
// Then clear the TextBlock and hide it until the next time text is received.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TSFInputControl::_SendAndClearText()
{
const auto text = _inputBuffer.substr(_activeTextStart);
_compositionCompletedHandlers(text);
_activeTextStart = _inputBuffer.length();
TextBlock().Text(L"");
// After we reset the TextBlock to empty string, we want to make sure
// ActualHeight reflects the respective height. It seems that ActualHeight
// isn't updated until there's new text in the TextBlock, so the next time a user
// invokes "Win+." for the emoji picker IME, it would end up
// using the pre-reset TextBlock().ActualHeight().
TextBlock().UpdateLayout();
// hide the controls until text input starts again
Canvas().Visibility(Visibility::Collapsed);
}
// Method Description:
// - Handler for FormatUpdating event by CoreEditContext responsible
// for handling different format updates for a particular range of text.
// TSFInputControl doesn't do anything with this event.
// Arguments:
// - sender: CoreTextEditContext sending the request. Not used in method.
// - args: CoreTextFormatUpdatingEventArgs Provides data for the FormatUpdating event. Not used in method.
// Return Value:
// - <none>
void TSFInputControl::_formatUpdatingHandler(CoreTextEditContext sender, CoreTextFormatUpdatingEventArgs const& /*args*/)
{
}
DEFINE_EVENT(TSFInputControl, CompositionCompleted, _compositionCompletedHandlers, TerminalControl::CompositionCompletedEventArgs);
}