mirror of
https://github.com/microsoft/terminal.git
synced 2026-04-14 10:11:00 +00:00
464 lines
20 KiB
C++
464 lines
20 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.h"
|
|
#include "TSFInputControl.h"
|
|
#include "TSFInputControl.g.cpp"
|
|
|
|
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::Control::implementation
|
|
{
|
|
TSFInputControl::TSFInputControl()
|
|
{
|
|
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 AlphanumericHalfWidth in order to facilitate those CJK input methods to open in English mode by default.
|
|
// AlphanumericHalfWidth scope doesn't prevent input method from switching to composition mode, it accepts any character too.
|
|
// Besides, Text scope turns on typing intelligence, but that doesn't work in this project.
|
|
_editContext.InputScope(CoreTextInputScope::AlphanumericHalfWidth);
|
|
|
|
_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()
|
|
{
|
|
_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()
|
|
{
|
|
_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())
|
|
{
|
|
_inputBuffer.clear();
|
|
_selection = {};
|
|
_activeTextStart = 0;
|
|
_editContext.NotifyTextChanged({ 0, INT32_MAX }, 0, _selection);
|
|
TextBlock().Text({});
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
try
|
|
{
|
|
if (!_focused || !Canvas())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get the cursor position in text buffer position
|
|
auto cursorArgs = winrt::make_self<CursorPositionEventArgs>();
|
|
_CurrentCursorPositionHandlers(*this, *cursorArgs);
|
|
const til::point cursorPos{ til::math::flooring, cursorArgs->CurrentPosition() };
|
|
|
|
const auto actualCanvasWidth{ Canvas().ActualWidth() };
|
|
const auto 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();
|
|
}
|
|
CATCH_LOG()
|
|
|
|
// 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 auto clientCursorPos{ _currentTerminalCursorPos * fontSize };
|
|
|
|
// Get scale factor for view
|
|
const auto scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel();
|
|
|
|
const til::point clientCursorInDips{ til::math::flooring, clientCursorPos.x / scaleFactor, clientCursorPos.y / scaleFactor };
|
|
|
|
// Position our TextBlock at the cursor position
|
|
Canvas().SetLeft(TextBlock(), clientCursorInDips.x);
|
|
Canvas().SetTop(TextBlock(), clientCursorInDips.y);
|
|
|
|
// calculate FontSize in pixels from Points
|
|
const double fontSizePx = (fontSize.height * 72) / USER_DEFAULT_SCREEN_DPI;
|
|
const auto 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;
|
|
// 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.X, _currentWindowBounds.Y };
|
|
|
|
// 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 auto controlAbsoluteOrigin{ windowOrigin + controlOrigin };
|
|
|
|
// Convert the control origin to pixels
|
|
const auto scaledFrameOrigin = til::point{ til::math::flooring, controlAbsoluteOrigin.x * scaleFactor, controlAbsoluteOrigin.y * scaleFactor };
|
|
|
|
// Get the location of the cursor in the display, in pixels.
|
|
auto screenCursorPos{ scaledFrameOrigin + clientCursorPos };
|
|
|
|
// GH #5007 - make sure to account for wrapping the IME composition at
|
|
// the right side of the viewport.
|
|
const auto textBlockHeight = ::base::ClampMul(_currentTextBlockHeight, scaleFactor);
|
|
|
|
// Get the bounds of the composition text, in pixels.
|
|
const til::rect textBounds{ til::point{ screenCursorPos.x, screenCursorPos.y },
|
|
til::size{ 0, textBlockHeight } };
|
|
|
|
_currentTextBounds = textBounds.to_winrt_rect();
|
|
|
|
_currentControlBounds = Rect(static_cast<float>(screenCursorPos.x),
|
|
static_cast<float>(screenCursorPos.y),
|
|
0.0f,
|
|
static_cast<float>(fontSize.height));
|
|
}
|
|
|
|
// 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, const CoreTextLayoutRequestedEventArgs& 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, const CoreTextCompositionStartedEventArgs& /*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, const CoreTextCompositionCompletedEventArgs& /*args*/)
|
|
{
|
|
_inComposition = false;
|
|
_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, const winrt::Windows::Foundation::IInspectable& /*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, const CoreTextTextRequestedEventArgs& args)
|
|
{
|
|
try
|
|
{
|
|
const auto range = args.Request().Range();
|
|
const auto text = _inputBuffer.substr(
|
|
range.StartCaretPosition,
|
|
range.EndCaretPosition - range.StartCaretPosition);
|
|
args.Request().Text(text);
|
|
}
|
|
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, const CoreTextSelectionRequestedEventArgs& args)
|
|
{
|
|
args.Request().Selection(_selection);
|
|
}
|
|
|
|
// 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, const CoreTextSelectionUpdatingEventArgs& args)
|
|
{
|
|
_selection = args.Selection();
|
|
args.Result(CoreTextSelectionUpdatingResult::Succeeded);
|
|
}
|
|
|
|
// 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, const CoreTextTextUpdatingEventArgs& args)
|
|
{
|
|
try
|
|
{
|
|
const auto incomingText = args.Text();
|
|
const auto range = args.Range();
|
|
|
|
_inputBuffer = _inputBuffer.replace(
|
|
range.StartCaretPosition,
|
|
range.EndCaretPosition - range.StartCaretPosition,
|
|
incomingText);
|
|
_selection = args.NewSelection();
|
|
// GH#5054: Pressing backspace might move the caret before the _activeTextStart.
|
|
_activeTextStart = std::min(_activeTextStart, _inputBuffer.size());
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
void TSFInputControl::_SendAndClearText()
|
|
{
|
|
const auto text = _inputBuffer.substr(_activeTextStart);
|
|
if (text.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
_CompositionCompletedHandlers(text);
|
|
|
|
_activeTextStart = _inputBuffer.size();
|
|
|
|
TextBlock().Text({});
|
|
|
|
// 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, const CoreTextFormatUpdatingEventArgs& /*args*/)
|
|
{
|
|
}
|
|
|
|
void TSFInputControl::ManuallyDisplayText(const winrt::hstring& text)
|
|
{
|
|
_focused = !text.empty();
|
|
Canvas().Visibility(text.empty() ? Visibility::Collapsed : Visibility::Visible);
|
|
|
|
_inputBuffer.clear();
|
|
// _editContext.NotifyFocusLeave();
|
|
_activeTextStart = 0;
|
|
_inComposition = false;
|
|
|
|
// HACK trim off leading DEL chars.
|
|
std::wstring_view view{ text.c_str() };
|
|
const auto strBegin = view.find_first_not_of(L"\x7f");
|
|
if (strBegin != std::wstring::npos)
|
|
{
|
|
view = view.substr(strBegin*2);
|
|
}
|
|
|
|
TextBlock().Text(winrt::hstring{ view });
|
|
TextBlock().UpdateLayout();
|
|
TryRedrawCanvas();
|
|
}
|
|
}
|