c6879d75af
## 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.
368 lines
16 KiB
C++
368 lines
16 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 }
|
|
{
|
|
InitializeComponent();
|
|
|
|
// Create a CoreTextEditingContext for since we are acting like a custom edit control
|
|
auto manager = Core::CoreTextServicesManager::GetForCurrentView();
|
|
_editContext = manager.CreateEditContext();
|
|
|
|
// sets the Input Pane display policy to Manual for now so that it can manually show the
|
|
// software keyboard when the control gains focus and dismiss it when the control loses focus.
|
|
// TODO GitHub #3639: Should Input Pane display policy be Automatic
|
|
_editContext.InputPaneDisplayPolicy(Core::CoreTextInputPaneDisplayPolicy::Manual);
|
|
|
|
// set the input scope to Text because this control is for any text.
|
|
_editContext.InputScope(Core::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.
|
|
_layoutRequestedRevoker.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();
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
|
|
// 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:
|
|
// - 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();
|
|
|
|
// Get window in screen coordinates, this is the entire window including tabs
|
|
const auto windowBounds = CoreWindow::GetForCurrentThread().Bounds();
|
|
|
|
// Get the cursor position in text buffer position
|
|
auto cursorArgs = winrt::make_self<CursorPositionEventArgs>();
|
|
_CurrentCursorPositionHandlers(*this, *cursorArgs);
|
|
const COORD cursorPos = { ::base::ClampedNumeric<short>(cursorArgs->CurrentPosition().X), ::base::ClampedNumeric<short>(cursorArgs->CurrentPosition().Y) };
|
|
|
|
// 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 auto fontWidth = fontArgs->FontSize().Width;
|
|
const auto fontHeight = fontArgs->FontSize().Height;
|
|
|
|
// Convert text buffer cursor position to client coordinate position within the window
|
|
COORD clientCursorPos;
|
|
clientCursorPos.X = ::base::ClampMul(cursorPos.X, ::base::ClampedNumeric<short>(fontWidth));
|
|
clientCursorPos.Y = ::base::ClampMul(cursorPos.Y, ::base::ClampedNumeric<short>(fontHeight));
|
|
|
|
// Convert from client coordinate to screen coordinate by adding window position
|
|
COORD screenCursorPos;
|
|
screenCursorPos.X = ::base::ClampAdd(clientCursorPos.X, ::base::ClampedNumeric<short>(windowBounds.X));
|
|
screenCursorPos.Y = ::base::ClampAdd(clientCursorPos.Y, ::base::ClampedNumeric<short>(windowBounds.Y));
|
|
|
|
// get any offset (margin + tabs, etc..) of the control within the window
|
|
const auto offsetPoint = this->TransformToVisual(nullptr).TransformPoint(winrt::Windows::Foundation::Point(0, 0));
|
|
|
|
// add the margin offsets if any
|
|
screenCursorPos.X = ::base::ClampAdd(screenCursorPos.X, ::base::ClampedNumeric<short>(offsetPoint.X));
|
|
screenCursorPos.Y = ::base::ClampAdd(screenCursorPos.Y, ::base::ClampedNumeric<short>(offsetPoint.Y));
|
|
|
|
// Get scale factor for view
|
|
const double scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel();
|
|
|
|
// Set the selection layout bounds
|
|
Rect selectionRect = Rect(screenCursorPos.X, screenCursorPos.Y, 0, fontHeight);
|
|
request.LayoutBounds().TextBounds(ScaleRect(selectionRect, scaleFactor));
|
|
|
|
// Set the control bounds of the whole control
|
|
Rect controlRect = Rect(screenCursorPos.X, screenCursorPos.Y, 0, fontHeight);
|
|
request.LayoutBounds().ControlBounds(ScaleRect(controlRect, scaleFactor));
|
|
|
|
// position textblock to cursor position
|
|
Canvas().SetLeft(TextBlock(), clientCursorPos.X);
|
|
Canvas().SetTop(TextBlock(), ::base::ClampedNumeric<double>(clientCursorPos.Y));
|
|
|
|
TextBlock().Height(fontHeight);
|
|
// calculate FontSize in pixels from DIPs
|
|
const double fontSizePx = (fontHeight * 72) / USER_DEFAULT_SCREEN_DPI;
|
|
TextBlock().FontSize(fontSizePx);
|
|
|
|
TextBlock().FontFamily(Media::FontFamily(fontArgs->FontFace()));
|
|
}
|
|
|
|
// 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
|
|
{
|
|
_inputBuffer = _inputBuffer.replace(
|
|
range.StartCaretPosition,
|
|
::base::ClampSub<size_t>(range.EndCaretPosition, range.StartCaretPosition),
|
|
incomingText);
|
|
|
|
// If we receive tabbed IME input like emoji, kaomojis, and symbols, send it to the terminal immediately.
|
|
// They aren't composition, so we don't want to wait for the user to start and finish a composition to send the text.
|
|
if (!_inComposition)
|
|
{
|
|
_SendAndClearText();
|
|
}
|
|
else
|
|
{
|
|
Canvas().Visibility(Visibility::Visible);
|
|
const auto text = _inputBuffer.substr(range.StartCaretPosition, range.EndCaretPosition - range.StartCaretPosition + 1);
|
|
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, _inputBuffer.length() - _activeTextStart);
|
|
|
|
_compositionCompletedHandlers(text);
|
|
|
|
_activeTextStart = _inputBuffer.length();
|
|
|
|
TextBlock().Text(L"");
|
|
|
|
// 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);
|
|
}
|