mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-12 08:40:37 -06:00
As explained in detail in the diff. Closes #19562 (cherry picked from commit 1ca0c76bc74012fdd1ff6211b5c8f389a4efd9b4) Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzghp8js Service-Version: 1.24
806 lines
27 KiB
C++
806 lines
27 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
#include "Implementation.h"
|
|
|
|
#include "Handle.h"
|
|
#include "../buffer/out/TextAttribute.hpp"
|
|
#include "../renderer/base/renderer.hpp"
|
|
|
|
#pragma warning(disable : 4100) // '...': unreferenced formal parameter
|
|
|
|
using namespace Microsoft::Console::TSF;
|
|
|
|
static void TfSelectionClose(const TF_SELECTION* sel)
|
|
{
|
|
if (const auto r = sel->range)
|
|
{
|
|
r->Release();
|
|
}
|
|
}
|
|
using unique_tf_selection = wil::unique_struct<TF_SELECTION, decltype(&TfSelectionClose), &TfSelectionClose>;
|
|
|
|
static void TfPropertyvalClose(TF_PROPERTYVAL* val)
|
|
{
|
|
VariantClear(&val->varValue);
|
|
}
|
|
using unique_tf_propertyval = wil::unique_struct<TF_PROPERTYVAL, decltype(&TfPropertyvalClose), &TfPropertyvalClose>;
|
|
|
|
// The flags passed to ActivateEx don't replace the flags during previous calls.
|
|
// Instead, they're additive. So, if we pass flags that some concurrently running
|
|
// TSF clients don't expect we may blow them up accidentally.
|
|
//
|
|
// Such is the case with WPF (and TSF, which is actually at fault there).
|
|
// If you pass TF_TMAE_CONSOLE it'll instantly crash on Windows 10 on first text input.
|
|
// On Windows 11 it'll at least not crash but still make emoji input completely non-functional.
|
|
//
|
|
// --------
|
|
//
|
|
// In any case, we pass the same flags as conhost v1:
|
|
// - TF_TMAE_UIELEMENTENABLEDONLY: TSF activates only text services that are
|
|
// categorized in GUID_TFCAT_TIPCAP_UIELEMENTENABLED.
|
|
// - TF_TMAE_NOACTIVATEKEYBOARDLAYOUT: TSF does not sync the current keyboard layout
|
|
// while this method is called. The keyboard layout will be adjusted when the
|
|
// calling thread gets focus. This flag must be used with TF_TMAE_NOACTIVATETIP.
|
|
// - TF_TMAE_CONSOLE: A text service is activated for console usage.
|
|
// Some IMEs are known to use this as a hint. Particularly a Korean IME can benefit
|
|
// from this, because Korean relies on "recomposing" previously finished compositions.
|
|
// That can't work in a terminal, since we submit composed text to the shell immediately.
|
|
//
|
|
// I'm not sure what TF_TMAE_UIELEMENTENABLEDONLY does. I tried to figure it out but failed.
|
|
//
|
|
// For TF_TMAE_NOACTIVATEKEYBOARDLAYOUT, I'm 99% sure it doesn't do anything, including in
|
|
// conhost v1. This is because IMM will be initialized on WM_ACTIVATE, which calls ActivateEx(0).
|
|
// Any subsequent ActivateEx() calls will update the flags, _except_ for this one and
|
|
// TF_TMAE_NOACTIVATETIP which are explicitly filtered out.
|
|
//
|
|
// TF_TMAE_NOACTIVATETIP however is important. Without it, TIPs are immediately initialized.
|
|
static std::atomic<DWORD> s_activationFlags{ TF_TMAE_NOACTIVATETIP | TF_TMAE_UIELEMENTENABLEDONLY | TF_TMAE_NOACTIVATEKEYBOARDLAYOUT | TF_TMAE_CONSOLE };
|
|
void Implementation::AvoidBuggyTSFConsoleFlags() noexcept
|
|
{
|
|
s_activationFlags.fetch_and(~static_cast<DWORD>(TF_TMAE_CONSOLE), std::memory_order_relaxed);
|
|
}
|
|
|
|
static std::atomic<bool> s_wantsAnsiInputScope{ false };
|
|
void Implementation::SetDefaultScopeAlphanumericHalfWidth(bool enable) noexcept
|
|
{
|
|
s_wantsAnsiInputScope.store(enable, std::memory_order_relaxed);
|
|
}
|
|
|
|
void Implementation::Initialize()
|
|
{
|
|
_categoryMgr = wil::CoCreateInstance<ITfCategoryMgr>(CLSID_TF_CategoryMgr, CLSCTX_INPROC_SERVER);
|
|
_displayAttributeMgr = wil::CoCreateInstance<ITfDisplayAttributeMgr>(CLSID_TF_DisplayAttributeMgr);
|
|
|
|
// There's no point in calling TF_GetThreadMgr. ITfThreadMgr is a per-thread singleton.
|
|
_threadMgrEx = wil::CoCreateInstance<ITfThreadMgrEx>(CLSID_TF_ThreadMgr, CLSCTX_INPROC_SERVER);
|
|
|
|
THROW_IF_FAILED(_threadMgrEx->ActivateEx(&_clientId, s_activationFlags.load(std::memory_order_relaxed)));
|
|
THROW_IF_FAILED(_threadMgrEx->CreateDocumentMgr(_documentMgr.addressof()));
|
|
|
|
TfEditCookie ecTextStore;
|
|
THROW_IF_FAILED(_documentMgr->CreateContext(_clientId, 0, static_cast<ITfContextOwnerCompositionSink*>(this), _context.addressof(), &ecTextStore));
|
|
|
|
_ownerCompositionServices = _context.try_query<ITfContextOwnerCompositionServices>();
|
|
|
|
_contextSource = _context.query<ITfSource>();
|
|
THROW_IF_FAILED(_contextSource->AdviseSink(IID_ITfContextOwner, static_cast<ITfContextOwner*>(this), &_cookieContextOwner));
|
|
THROW_IF_FAILED(_contextSource->AdviseSink(IID_ITfTextEditSink, static_cast<ITfTextEditSink*>(this), &_cookieTextEditSink));
|
|
|
|
THROW_IF_FAILED(_documentMgr->Push(_context.get()));
|
|
}
|
|
|
|
void Implementation::Uninitialize() noexcept
|
|
{
|
|
_provider.reset();
|
|
|
|
if (_associatedHwnd)
|
|
{
|
|
wil::com_ptr<ITfDocumentMgr> prev;
|
|
std::ignore = _threadMgrEx->AssociateFocus(_associatedHwnd, nullptr, prev.addressof());
|
|
}
|
|
|
|
if (_cookieTextEditSink != TF_INVALID_COOKIE)
|
|
{
|
|
std::ignore = _contextSource->UnadviseSink(_cookieTextEditSink);
|
|
}
|
|
if (_cookieContextOwner != TF_INVALID_COOKIE)
|
|
{
|
|
std::ignore = _contextSource->UnadviseSink(_cookieContextOwner);
|
|
}
|
|
|
|
if (_documentMgr)
|
|
{
|
|
std::ignore = _documentMgr->Pop(TF_POPF_ALL);
|
|
}
|
|
if (_threadMgrEx)
|
|
{
|
|
std::ignore = _threadMgrEx->Deactivate();
|
|
}
|
|
}
|
|
|
|
HWND Implementation::FindWindowOfActiveTSF() noexcept
|
|
{
|
|
// We don't know what ITfContextOwner we're going to get in
|
|
// the code below and it may very well be us (this instance).
|
|
// It's also possible that our IDataProvider's GetHwnd()
|
|
// implementation calls this FindWindowOfActiveTSF() function.
|
|
// This can result in infinite recursion because we're calling
|
|
// GetWnd() below, which may call GetHwnd(), which may call
|
|
// FindWindowOfActiveTSF(), and so on.
|
|
// By temporarily clearing the _provider we fix that flaw.
|
|
const auto restore = wil::scope_exit([this, provider = std::move(_provider)]() mutable {
|
|
_provider = std::move(provider);
|
|
});
|
|
|
|
wil::com_ptr<IEnumTfDocumentMgrs> enumDocumentMgrs;
|
|
if (FAILED_LOG(_threadMgrEx->EnumDocumentMgrs(enumDocumentMgrs.addressof())))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
wil::com_ptr<ITfDocumentMgr> document;
|
|
if (FAILED_LOG(enumDocumentMgrs->Next(1, document.addressof(), nullptr)))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
wil::com_ptr<ITfContext> context;
|
|
if (FAILED_LOG(document->GetTop(context.addressof())))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
wil::com_ptr<ITfContextView> view;
|
|
if (FAILED_LOG(context->GetActiveView(view.addressof())))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
HWND hwnd;
|
|
if (FAILED_LOG(view->GetWnd(&hwnd)))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
return hwnd;
|
|
}
|
|
|
|
void Implementation::AssociateFocus(IDataProvider* provider)
|
|
{
|
|
_provider = provider;
|
|
_associatedHwnd = _provider->GetHwnd();
|
|
|
|
wil::com_ptr<ITfDocumentMgr> prev;
|
|
THROW_IF_FAILED(_threadMgrEx->AssociateFocus(_associatedHwnd, _documentMgr.get(), prev.addressof()));
|
|
}
|
|
|
|
void Implementation::Focus(IDataProvider* provider)
|
|
{
|
|
_provider = provider;
|
|
|
|
THROW_IF_FAILED(_threadMgrEx->SetFocus(_documentMgr.get()));
|
|
}
|
|
|
|
void Implementation::Unfocus(IDataProvider* provider)
|
|
{
|
|
if (!_provider || _provider != provider)
|
|
{
|
|
return;
|
|
}
|
|
|
|
{
|
|
const auto renderer = _provider->GetRenderer();
|
|
const auto renderData = renderer->GetRenderData();
|
|
|
|
renderData->LockConsole();
|
|
const auto unlock = wil::scope_exit([&]() {
|
|
renderData->UnlockConsole();
|
|
});
|
|
|
|
if (!renderData->tsfPreview.text.empty())
|
|
{
|
|
auto& comp = renderData->tsfPreview;
|
|
comp.text.clear();
|
|
comp.attributes.clear();
|
|
renderer->NotifyPaintFrame();
|
|
}
|
|
}
|
|
|
|
_provider.reset();
|
|
|
|
if (_compositions > 0 && _ownerCompositionServices)
|
|
{
|
|
std::ignore = _ownerCompositionServices->TerminateComposition(nullptr);
|
|
}
|
|
}
|
|
|
|
bool Implementation::HasActiveComposition() const noexcept
|
|
{
|
|
return _compositions > 0;
|
|
}
|
|
|
|
#pragma region IUnknown
|
|
|
|
STDMETHODIMP Implementation::QueryInterface(REFIID riid, void** ppvObj) noexcept
|
|
{
|
|
if (!ppvObj)
|
|
{
|
|
return E_POINTER;
|
|
}
|
|
|
|
if (IsEqualGUID(riid, IID_ITfContextOwner))
|
|
{
|
|
*ppvObj = static_cast<ITfContextOwner*>(this);
|
|
}
|
|
else if (IsEqualGUID(riid, IID_ITfContextOwnerCompositionSink))
|
|
{
|
|
*ppvObj = static_cast<ITfContextOwnerCompositionSink*>(this);
|
|
}
|
|
else if (IsEqualGUID(riid, IID_ITfTextEditSink))
|
|
{
|
|
*ppvObj = static_cast<ITfTextEditSink*>(this);
|
|
}
|
|
else if (IsEqualGUID(riid, IID_IUnknown))
|
|
{
|
|
*ppvObj = static_cast<IUnknown*>(static_cast<ITfContextOwner*>(this));
|
|
}
|
|
else
|
|
{
|
|
*ppvObj = nullptr;
|
|
return E_NOINTERFACE;
|
|
}
|
|
|
|
AddRef();
|
|
return S_OK;
|
|
}
|
|
|
|
ULONG STDMETHODCALLTYPE Implementation::AddRef() noexcept
|
|
{
|
|
return InterlockedIncrement(&_referenceCount);
|
|
}
|
|
|
|
ULONG STDMETHODCALLTYPE Implementation::Release() noexcept
|
|
{
|
|
const auto r = InterlockedDecrement(&_referenceCount);
|
|
if (r == 0)
|
|
{
|
|
delete this;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
#pragma endregion IUnknown
|
|
|
|
#pragma region ITfContextOwner
|
|
|
|
STDMETHODIMP Implementation::GetACPFromPoint(const POINT* ptScreen, DWORD dwFlags, LONG* pacp) noexcept
|
|
{
|
|
assert(false);
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
// The returned rectangle is used to position the TSF candidate window.
|
|
STDMETHODIMP Implementation::GetTextExt(LONG acpStart, LONG acpEnd, RECT* prc, BOOL* pfClipped) noexcept
|
|
try
|
|
{
|
|
if (prc)
|
|
{
|
|
*prc = _provider ? _provider->GetCursorPosition() : RECT{};
|
|
}
|
|
|
|
if (pfClipped)
|
|
{
|
|
*pfClipped = FALSE;
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
// The returned rectangle is used to activate the touch keyboard.
|
|
STDMETHODIMP Implementation::GetScreenExt(RECT* prc) noexcept
|
|
try
|
|
{
|
|
if (prc)
|
|
{
|
|
*prc = _provider ? _provider->GetViewport() : RECT{};
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
STDMETHODIMP Implementation::GetStatus(TF_STATUS* pdcs) noexcept
|
|
{
|
|
if (pdcs)
|
|
{
|
|
pdcs->dwDynamicFlags = 0;
|
|
// The use of TF_SS_TRANSITORY / TS_SS_TRANSITORY is incredibly important...
|
|
// ...and it has the least complete description:
|
|
// > TS_SS_TRANSITORY: The document is expected to have a short usage cycle.
|
|
//
|
|
// Proper documentation about the flag has been lost and can only be found via archive.org:
|
|
// http://web.archive.org/web/20140520210042/http://blogs.msdn.com/b/tsfaware/archive/2007/04/25/transitory-contexts.aspx
|
|
// It states:
|
|
// > The most significant difference is that Transitory contexts don't retain state - once you end the composition [...],
|
|
// > any knowledge of the document (or any previous insertions/modifications/etc.) is gone.
|
|
// In other words, non-transitory contexts expect access to previously completed contents, which is something we cannot provide.
|
|
// Because once some text has finished composition we'll immediately send it to the shell via HandleOutput(), which we cannot undo.
|
|
// It's also the primary reason why we cannot use the WinRT CoreTextServices APIs, as they don't set TS_SS_TRANSITORY.
|
|
//
|
|
// Additionally, "short usage cycle" also significantly undersells another importance of the flag:
|
|
// If set, it enables CUAS, the Cicero Unaware Application Support, which is an emulation layer that fakes IMM32.
|
|
// Cicero is the internal code name for TSF. In other words, "TS_SS_TRANSITORY" = "Disable modern TSF".
|
|
// This results in a couple modern composition features not working (Korean reconversion primarily),
|
|
// but it's a trade-off we're forced to make, because otherwise it doesn't work at all.
|
|
//
|
|
// TS_SS_NOHIDDENTEXT tells TSF that we don't support TS_RT_HIDDEN, which is used if a document contains hidden markup
|
|
// inside the text. For instance an HTML document contains tags which aren't visible, but nonetheless exist.
|
|
// It's not publicly documented, but allegedly specifying this flag results in a minor performance uplift.
|
|
// Ironically, the only two places that mention this flag internally state:
|
|
// > perf: we could check TS_SS_NOHIDDENTEXT for better perf
|
|
pdcs->dwStaticFlags = TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT;
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP Implementation::GetWnd(HWND* phwnd) noexcept
|
|
{
|
|
*phwnd = _provider ? _provider->GetHwnd() : nullptr;
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP Implementation::GetAttribute(REFGUID rguidAttribute, VARIANT* pvarValue) noexcept
|
|
{
|
|
if (s_wantsAnsiInputScope.load(std::memory_order_relaxed) && IsEqualGUID(rguidAttribute, GUID_PROP_INPUTSCOPE))
|
|
{
|
|
_ansiInputScope.AddRef();
|
|
pvarValue->vt = VT_UNKNOWN;
|
|
pvarValue->punkVal = &_ansiInputScope;
|
|
return S_OK;
|
|
}
|
|
|
|
pvarValue->vt = VT_EMPTY;
|
|
return S_OK;
|
|
}
|
|
|
|
#pragma endregion ITfContextOwner
|
|
|
|
#pragma region ITfContextOwnerCompositionSink
|
|
|
|
STDMETHODIMP Implementation::OnStartComposition(ITfCompositionView* pComposition, BOOL* pfOk) noexcept
|
|
try
|
|
{
|
|
_compositions++;
|
|
*pfOk = TRUE;
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
STDMETHODIMP Implementation::OnUpdateComposition(ITfCompositionView* pComposition, ITfRange* pRangeNew) noexcept
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP Implementation::OnEndComposition(ITfCompositionView* pComposition) noexcept
|
|
try
|
|
{
|
|
if (_compositions <= 0)
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
|
|
_compositions--;
|
|
if (_compositions == 0)
|
|
{
|
|
// https://learn.microsoft.com/en-us/windows/win32/api/msctf/nf-msctf-itfcontext-requesteditsession
|
|
// > A text service can request an edit session within the context of an existing edit session,
|
|
// > provided a write access session is not requested within a read-only session.
|
|
// --> Requires TF_ES_ASYNC to work properly. TF_ES_ASYNCDONTCARE randomly fails because... TSF.
|
|
std::ignore = _request(_editSessionCompositionUpdate, TF_ES_READWRITE | TF_ES_ASYNC);
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
#pragma endregion ITfContextOwnerCompositionSink
|
|
|
|
#pragma region ITfTextEditSink
|
|
|
|
STDMETHODIMP Implementation::OnEndEdit(ITfContext* pic, TfEditCookie ecReadOnly, ITfEditRecord* pEditRecord) noexcept
|
|
try
|
|
{
|
|
if (_compositions == 1)
|
|
{
|
|
// https://learn.microsoft.com/en-us/windows/win32/api/msctf/nf-msctf-itfcontext-requesteditsession
|
|
// > A text service can request an edit session within the context of an existing edit session,
|
|
// > provided a write access session is not requested within a read-only session.
|
|
// --> Requires TF_ES_ASYNC to work properly. TF_ES_ASYNCDONTCARE randomly fails because... TSF.
|
|
std::ignore = _request(_editSessionCompositionUpdate, TF_ES_READWRITE | TF_ES_ASYNC);
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
#pragma endregion ITfTextEditSink
|
|
|
|
Implementation::EditSessionProxyBase::EditSessionProxyBase(Implementation* self) noexcept :
|
|
self{ self }
|
|
{
|
|
}
|
|
|
|
STDMETHODIMP Implementation::EditSessionProxyBase::QueryInterface(REFIID riid, void** ppvObj) noexcept
|
|
{
|
|
if (!ppvObj)
|
|
{
|
|
return E_POINTER;
|
|
}
|
|
|
|
if (IsEqualGUID(riid, IID_ITfEditSession))
|
|
{
|
|
*ppvObj = static_cast<ITfEditSession*>(this);
|
|
}
|
|
else if (IsEqualGUID(riid, IID_IUnknown))
|
|
{
|
|
*ppvObj = static_cast<IUnknown*>(this);
|
|
}
|
|
else
|
|
{
|
|
*ppvObj = nullptr;
|
|
return E_NOINTERFACE;
|
|
}
|
|
|
|
AddRef();
|
|
return S_OK;
|
|
}
|
|
|
|
ULONG STDMETHODCALLTYPE Implementation::EditSessionProxyBase::AddRef() noexcept
|
|
{
|
|
InterlockedIncrement(&referenceCount);
|
|
return self->AddRef();
|
|
}
|
|
|
|
ULONG STDMETHODCALLTYPE Implementation::EditSessionProxyBase::Release() noexcept
|
|
{
|
|
InterlockedDecrement(&referenceCount);
|
|
return self->Release();
|
|
}
|
|
|
|
Implementation::AnsiInputScope::AnsiInputScope(Implementation* self) noexcept :
|
|
self{ self }
|
|
{
|
|
}
|
|
|
|
HRESULT Implementation::AnsiInputScope::QueryInterface(const IID& riid, void** ppvObj) noexcept
|
|
{
|
|
if (!ppvObj)
|
|
{
|
|
return E_POINTER;
|
|
}
|
|
|
|
if (IsEqualGUID(riid, IID_ITfInputScope))
|
|
{
|
|
*ppvObj = static_cast<ITfInputScope*>(this);
|
|
}
|
|
else if (IsEqualGUID(riid, IID_IUnknown))
|
|
{
|
|
*ppvObj = static_cast<IUnknown*>(this);
|
|
}
|
|
else
|
|
{
|
|
*ppvObj = nullptr;
|
|
return E_NOINTERFACE;
|
|
}
|
|
|
|
AddRef();
|
|
return S_OK;
|
|
}
|
|
|
|
ULONG Implementation::AnsiInputScope::AddRef() noexcept
|
|
{
|
|
return self->AddRef();
|
|
}
|
|
|
|
ULONG Implementation::AnsiInputScope::Release() noexcept
|
|
{
|
|
return self->Release();
|
|
}
|
|
|
|
HRESULT Implementation::AnsiInputScope::GetInputScopes(InputScope** pprgInputScopes, UINT* pcCount) noexcept
|
|
{
|
|
const auto scopes = static_cast<InputScope*>(CoTaskMemAlloc(1 * sizeof(InputScope)));
|
|
if (!scopes)
|
|
{
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
scopes[0] = IS_ALPHANUMERIC_HALFWIDTH;
|
|
|
|
*pprgInputScopes = scopes;
|
|
*pcCount = 1;
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT Implementation::AnsiInputScope::GetPhrase(BSTR** ppbstrPhrases, UINT* pcCount) noexcept
|
|
{
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
HRESULT Implementation::AnsiInputScope::GetRegularExpression(BSTR* pbstrRegExp) noexcept
|
|
{
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
HRESULT Implementation::AnsiInputScope::GetSRGS(BSTR* pbstrSRGS) noexcept
|
|
{
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
HRESULT Implementation::AnsiInputScope::GetXML(BSTR* pbstrXML) noexcept
|
|
{
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
[[nodiscard]] HRESULT Implementation::_request(EditSessionProxyBase& session, DWORD flags) const
|
|
{
|
|
// Some of the sessions are async, and we don't want to send another request if one is still in flight.
|
|
if (session.referenceCount)
|
|
{
|
|
return S_FALSE;
|
|
}
|
|
|
|
HRESULT hr = S_OK;
|
|
THROW_IF_FAILED(_context->RequestEditSession(_clientId, &session, flags, &hr));
|
|
RETURN_IF_FAILED(hr);
|
|
return S_OK;
|
|
}
|
|
|
|
void Implementation::_doCompositionUpdate(TfEditCookie ec)
|
|
{
|
|
wil::com_ptr<ITfRange> fullRange;
|
|
LONG fullRangeLength;
|
|
THROW_IF_FAILED(_context->GetStart(ec, fullRange.addressof()));
|
|
THROW_IF_FAILED(fullRange->ShiftEnd(ec, LONG_MAX, &fullRangeLength, nullptr));
|
|
|
|
std::wstring finalizedString;
|
|
std::wstring activeComposition;
|
|
til::small_vector<Render::CompositionRange, 2> activeCompositionRanges;
|
|
bool activeCompositionEncountered = false;
|
|
|
|
const GUID* guids[] = { &GUID_PROP_COMPOSING, &GUID_PROP_ATTRIBUTE };
|
|
wil::com_ptr<ITfReadOnlyProperty> props;
|
|
THROW_IF_FAILED(_context->TrackProperties(&guids[0], ARRAYSIZE(guids), nullptr, 0, props.addressof()));
|
|
|
|
wil::com_ptr<IEnumTfRanges> enumRanges;
|
|
THROW_IF_FAILED(props->EnumRanges(ec, enumRanges.addressof(), fullRange.get()));
|
|
|
|
// IEnumTfRanges::Next returns S_FALSE when it has reached the end of the list.
|
|
// This includes any call where the number of returned items is less than what was requested.
|
|
for (HRESULT nextResult = S_OK; nextResult == S_OK;)
|
|
{
|
|
ITfRange* ranges[8];
|
|
ULONG rangesCount;
|
|
nextResult = enumRanges->Next(ARRAYSIZE(ranges), &ranges[0], &rangesCount);
|
|
|
|
const auto cleanup = wil::scope_exit([&] {
|
|
for (ULONG i = 0; i < rangesCount; ++i)
|
|
{
|
|
ranges[i]->Release();
|
|
}
|
|
});
|
|
|
|
for (ULONG i = 0; i < rangesCount; ++i)
|
|
{
|
|
const auto range = ranges[i];
|
|
|
|
bool composing = false;
|
|
TfGuidAtom atom = TF_INVALID_GUIDATOM;
|
|
{
|
|
wil::unique_variant var;
|
|
THROW_IF_FAILED(props->GetValue(ec, range, var.addressof()));
|
|
|
|
wil::com_ptr<IEnumTfPropertyValue> propVal;
|
|
wil::com_query_to(var.punkVal, propVal.addressof());
|
|
|
|
unique_tf_propertyval propVals[2];
|
|
THROW_IF_FAILED(propVal->Next(2, propVals[0].addressof(), nullptr));
|
|
|
|
for (const auto& val : propVals)
|
|
{
|
|
if (IsEqualGUID(val.guidId, GUID_PROP_COMPOSING))
|
|
{
|
|
composing = V_VT(&val.varValue) == VT_I4 && V_I4(&val.varValue) != 0;
|
|
}
|
|
else if (IsEqualGUID(val.guidId, GUID_PROP_ATTRIBUTE))
|
|
{
|
|
atom = V_VT(&val.varValue) == VT_I4 ? static_cast<TfGuidAtom>(V_I4(&val.varValue)) : TF_INVALID_GUIDATOM;
|
|
}
|
|
}
|
|
}
|
|
|
|
size_t totalLen = 0;
|
|
for (;;)
|
|
{
|
|
// GetText() won't throw if the range is empty. It'll simply return len == 0.
|
|
// However, you'll likely never see this happen with a bufCap this large (try 16 instead or something).
|
|
// It seems TSF doesn't support such large compositions in any language.
|
|
static constexpr ULONG bufCap = 128;
|
|
WCHAR buf[bufCap];
|
|
ULONG len = bufCap;
|
|
THROW_IF_FAILED(range->GetText(ec, TF_TF_MOVESTART, buf, len, &len));
|
|
|
|
// Since we can't un-finalize finalized text, we only finalize text that's at the start of the document.
|
|
// In other words, don't put text that's in the middle between two active compositions into the finalized string.
|
|
if (!composing && !activeCompositionEncountered)
|
|
{
|
|
finalizedString.append(buf, len);
|
|
}
|
|
else
|
|
{
|
|
activeComposition.append(buf, len);
|
|
}
|
|
|
|
totalLen += len;
|
|
|
|
if (len < bufCap)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
const auto attr = _textAttributeFromAtom(atom);
|
|
activeCompositionRanges.emplace_back(totalLen, attr);
|
|
|
|
activeCompositionEncountered |= composing;
|
|
}
|
|
}
|
|
|
|
LONG cursorPos = LONG_MAX;
|
|
{
|
|
// According to the docs this may result in TF_E_NOSELECTION. While I haven't actually seen that happen myself yet,
|
|
// I don't want this to result in log-spam, which is why this doesn't use SUCCEEDED_LOG().
|
|
unique_tf_selection sel;
|
|
ULONG selCount;
|
|
if (SUCCEEDED(_context->GetSelection(ec, TF_DEFAULT_SELECTION, 1, &sel, &selCount)) && selCount == 1)
|
|
{
|
|
wil::com_ptr<ITfRange> start;
|
|
THROW_IF_FAILED(_context->GetStart(ec, start.addressof()));
|
|
|
|
TF_HALTCOND hc{
|
|
.pHaltRange = sel.range,
|
|
.aHaltPos = sel.style.ase == TF_AE_START ? TF_ANCHOR_START : TF_ANCHOR_END,
|
|
};
|
|
THROW_IF_FAILED(start->ShiftEnd(ec, LONG_MAX, &cursorPos, &hc));
|
|
}
|
|
|
|
// Compensate for the fact that we'll be erasing the start of the string below.
|
|
cursorPos -= static_cast<LONG>(finalizedString.size());
|
|
cursorPos = std::clamp(cursorPos, 0l, static_cast<LONG>(activeComposition.size()));
|
|
}
|
|
|
|
if (!finalizedString.empty())
|
|
{
|
|
// Erase the text that's done with composition from the context.
|
|
wil::com_ptr<ITfRange> range;
|
|
LONG cch;
|
|
THROW_IF_FAILED(_context->GetStart(ec, range.addressof()));
|
|
THROW_IF_FAILED(range->ShiftEnd(ec, static_cast<LONG>(finalizedString.size()), &cch, nullptr));
|
|
THROW_IF_FAILED(range->SetText(ec, 0, nullptr, 0));
|
|
}
|
|
|
|
if (_provider)
|
|
{
|
|
{
|
|
const auto renderer = _provider->GetRenderer();
|
|
const auto renderData = renderer->GetRenderData();
|
|
|
|
renderData->LockConsole();
|
|
const auto unlock = wil::scope_exit([&]() {
|
|
renderData->UnlockConsole();
|
|
});
|
|
|
|
auto& comp = renderData->tsfPreview;
|
|
comp.text = std::move(activeComposition);
|
|
comp.attributes = std::move(activeCompositionRanges);
|
|
// The code block above that calculates the `cursorPos` will clamp it to a positive number.
|
|
comp.cursorPos = static_cast<size_t>(cursorPos);
|
|
renderer->NotifyPaintFrame();
|
|
}
|
|
|
|
if (!finalizedString.empty())
|
|
{
|
|
_provider->HandleOutput(finalizedString);
|
|
}
|
|
}
|
|
}
|
|
|
|
TextAttribute Implementation::_textAttributeFromAtom(TfGuidAtom atom) const
|
|
{
|
|
TextAttribute attr;
|
|
|
|
// You get TF_INVALID_GUIDATOM by (for instance) using the Vietnamese Telex IME.
|
|
// A dashed underline is used because that's what Firefox used at the time and it
|
|
// looked kind of neat. In the past, conhost used a blue background and white text.
|
|
if (atom == TF_INVALID_GUIDATOM)
|
|
{
|
|
attr.SetUnderlineStyle(UnderlineStyle::DashedUnderlined);
|
|
return attr;
|
|
}
|
|
|
|
GUID guid;
|
|
if (FAILED_LOG(_categoryMgr->GetGUID(atom, &guid)))
|
|
{
|
|
return attr;
|
|
}
|
|
|
|
wil::com_ptr<ITfDisplayAttributeInfo> dai;
|
|
if (FAILED_LOG(_displayAttributeMgr->GetDisplayAttributeInfo(guid, dai.addressof(), nullptr)))
|
|
{
|
|
return attr;
|
|
}
|
|
|
|
TF_DISPLAYATTRIBUTE da;
|
|
THROW_IF_FAILED(dai->GetAttributeInfo(&da));
|
|
|
|
// The Tencent QQPinyin IME creates TF_CT_COLORREF attributes with a color of 0x000000 (black).
|
|
// We respect their wish, which results in the preview text being invisible.
|
|
// (Note that sending this COLORREF is incorrect, and not a bug in our handling.)
|
|
//
|
|
// After some discussion, we realized that an IME which sets only one color but not
|
|
// the others is likely not properly tested anyway, so we reject those cases.
|
|
// After all, what behavior do we expect, if the IME sends e.g. foreground=blue,
|
|
// without knowing whether our terminal theme already uses a blue background?
|
|
if (da.crText.type != TF_CT_NONE && da.crText.type == da.crBk.type)
|
|
{
|
|
attr.SetForeground(_colorFromDisplayAttribute(da.crText));
|
|
attr.SetBackground(_colorFromDisplayAttribute(da.crBk));
|
|
// I'm not sure what the best way to handle this is.
|
|
if (da.crText.type == da.crLine.type)
|
|
{
|
|
attr.SetUnderlineColor(_colorFromDisplayAttribute(da.crLine));
|
|
}
|
|
}
|
|
if (da.lsStyle >= TF_LS_NONE && da.lsStyle <= TF_LS_SQUIGGLE)
|
|
{
|
|
static constexpr UnderlineStyle lut[] = {
|
|
/* TF_LS_NONE */ UnderlineStyle::NoUnderline,
|
|
/* TF_LS_SOLID */ UnderlineStyle::SinglyUnderlined,
|
|
/* TF_LS_DOT */ UnderlineStyle::DottedUnderlined,
|
|
/* TF_LS_DASH */ UnderlineStyle::DashedUnderlined,
|
|
/* TF_LS_SQUIGGLE */ UnderlineStyle::CurlyUnderlined,
|
|
};
|
|
attr.SetUnderlineStyle(lut[da.lsStyle]);
|
|
}
|
|
// You can reproduce bold lines with the Japanese IME by typing "kyouhaishaheiku" and pressing space.
|
|
// The IME will allow you to navigate between the 3 parts of the composition and the current one is
|
|
// marked as fBoldLine. We don't support bold lines so we just use a double underline instead.
|
|
if (da.fBoldLine)
|
|
{
|
|
attr.SetUnderlineStyle(UnderlineStyle::DoublyUnderlined);
|
|
}
|
|
|
|
return attr;
|
|
}
|
|
|
|
COLORREF Implementation::_colorFromDisplayAttribute(TF_DA_COLOR color)
|
|
{
|
|
switch (color.type)
|
|
{
|
|
case TF_CT_SYSCOLOR:
|
|
return GetSysColor(color.nIndex);
|
|
case TF_CT_COLORREF:
|
|
return color.cr;
|
|
default:
|
|
// If you get here you either called this when .type is TF_CT_NONE
|
|
// (don't call in that case; there's no color to be had), or
|
|
// there's a new .type which you need to add.
|
|
assert(false);
|
|
return 0;
|
|
}
|
|
}
|