// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/base/ime/win/tsf_event_router.h"
#include <msctf.h>
#include <set>
#include <utility>
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/win/atl.h"
#include "ui/base/win/atl_module.h"
#include "ui/gfx/range/range.h"
namespace ui {
// TSFEventRouter::Delegate ------------------------------------
// The implementation class of ITfUIElementSink, whose member functions will be
// called back by TSF when the UI element status is changed, for example when
// the candidate window is opened or closed. This class also implements
// ITfTextEditSink, whose member function is called back by TSF when the text
// editting session is finished.
class ATL_NO_VTABLE TSFEventRouter::Delegate
: public ATL::CComObjectRootEx<CComSingleThreadModel>,
public ITfUIElementSink,
public ITfTextEditSink {
public:
BEGIN_COM_MAP(Delegate)
COM_INTERFACE_ENTRY(ITfUIElementSink)
COM_INTERFACE_ENTRY(ITfTextEditSink)
END_COM_MAP()
Delegate();
Delegate(const Delegate&) = delete;
Delegate& operator=(const Delegate&) = delete;
~Delegate();
// ITfTextEditSink:
IFACEMETHODIMP OnEndEdit(ITfContext* context,
TfEditCookie read_only_cookie,
ITfEditRecord* edit_record) override;
// ITfUiElementSink:
IFACEMETHODIMP BeginUIElement(DWORD element_id, BOOL* is_show) override;
IFACEMETHODIMP UpdateUIElement(DWORD element_id) override;
IFACEMETHODIMP EndUIElement(DWORD element_id) override;
// Sets |thread_manager| to be monitored. |thread_manager| can be nullptr.
void SetManager(ITfThreadMgr* thread_manager);
// Returns true if the IME is composing text.
bool IsImeComposing();
// Sets |router| to be forwarded TSF-related events.
void SetRouter(TSFEventRouter* router);
private:
// Returns current composition range. Returns gfx::Range::InvalidRange if
// there is no composition.
static gfx::Range GetCompositionRange(ITfContext* context);
// Returns true if the given |element_id| represents the candidate window.
bool IsCandidateWindowInternal(DWORD element_id);
// A context associated with this class.
Microsoft::WRL::ComPtr<ITfContext> context_;
// The ITfSource associated with |context_|.
Microsoft::WRL::ComPtr<ITfSource> context_source_;
// The cookie for |context_source_|.
DWORD context_source_cookie_;
// A UIElementMgr associated with this class.
Microsoft::WRL::ComPtr<ITfUIElementMgr> ui_element_manager_;
// The ITfSouce associated with |ui_element_manager_|.
Microsoft::WRL::ComPtr<ITfSource> ui_source_;
// The set of currently opened candidate window ids.
std::set<DWORD> open_candidate_window_ids_;
// The cookie for |ui_source_|.
DWORD ui_source_cookie_ = TF_INVALID_COOKIE;
raw_ptr<TSFEventRouter> router_ = nullptr;
gfx::Range previous_composition_range_;
};
TSFEventRouter::Delegate::Delegate()
: previous_composition_range_(gfx::Range::InvalidRange()) {}
TSFEventRouter::Delegate::~Delegate() = default;
void TSFEventRouter::Delegate::SetRouter(TSFEventRouter* router) {
router_ = router;
}
HRESULT TSFEventRouter::Delegate::OnEndEdit(ITfContext* context,
TfEditCookie read_only_cookie,
ITfEditRecord* edit_record) {
if (!edit_record || !context)
return E_INVALIDARG;
if (!router_)
return S_OK;
// |edit_record| can be used to obtain updated ranges in terms of text
// contents and/or text attributes. Here we are interested only in text update
// so we use TF_GTP_INCL_TEXT and check if there is any range which contains
// updated text.
Microsoft::WRL::ComPtr<IEnumTfRanges> ranges;
if (FAILED(edit_record->GetTextAndPropertyUpdates(TF_GTP_INCL_TEXT, nullptr,
0, &ranges)))
return S_OK; // Don't care about failures.
ULONG fetched_count = 0;
Microsoft::WRL::ComPtr<ITfRange> range;
if (FAILED(ranges->Next(1, &range, &fetched_count)))
return S_OK; // Don't care about failures.
const gfx::Range composition_range = GetCompositionRange(context);
if (!previous_composition_range_.IsValid() && composition_range.IsValid())
router_->OnTSFStartComposition();
// |fetched_count| != 0 means there is at least one range that contains
// updated text.
if (fetched_count != 0)
router_->OnTextUpdated(composition_range);
if (previous_composition_range_.IsValid() && !composition_range.IsValid())
router_->OnTSFEndComposition();
previous_composition_range_ = composition_range;
return S_OK;
}
HRESULT TSFEventRouter::Delegate::BeginUIElement(DWORD element_id,
BOOL* is_show) {
if (is_show)
*is_show = TRUE; // Without this the UI element will not be shown.
if (!IsCandidateWindowInternal(element_id))
return S_OK;
std::pair<std::set<DWORD>::iterator, bool> insert_result =
open_candidate_window_ids_.insert(element_id);
// Don't call if |router_| is null or |element_id| is already handled.
if (router_ && insert_result.second)
router_->OnCandidateWindowCountChanged(open_candidate_window_ids_.size());
return S_OK;
}
HRESULT TSFEventRouter::Delegate::UpdateUIElement(DWORD element_id) {
return S_OK;
}
HRESULT TSFEventRouter::Delegate::EndUIElement(DWORD element_id) {
if ((open_candidate_window_ids_.erase(element_id) != 0) && router_)
router_->OnCandidateWindowCountChanged(open_candidate_window_ids_.size());
return S_OK;
}
void TSFEventRouter::Delegate::SetManager(ITfThreadMgr* thread_manager) {
context_.Reset();
if (context_source_) {
context_source_->UnadviseSink(context_source_cookie_);
context_source_.Reset();
}
context_source_cookie_ = TF_INVALID_COOKIE;
ui_element_manager_.Reset();
if (ui_source_) {
ui_source_->UnadviseSink(ui_source_cookie_);
ui_source_.Reset();
}
ui_source_cookie_ = TF_INVALID_COOKIE;
if (!thread_manager)
return;
Microsoft::WRL::ComPtr<ITfDocumentMgr> document_manager;
if (FAILED(thread_manager->GetFocus(&document_manager)) ||
!document_manager.Get() || FAILED(document_manager->GetBase(&context_)) ||
FAILED(context_.As(&context_source_)))
return;
context_source_->AdviseSink(IID_ITfTextEditSink,
static_cast<ITfTextEditSink*>(this),
&context_source_cookie_);
if (FAILED(
thread_manager->QueryInterface(IID_PPV_ARGS(&ui_element_manager_))) ||
FAILED(ui_element_manager_.As(&ui_source_)))
return;
ui_source_->AdviseSink(IID_ITfUIElementSink,
static_cast<ITfUIElementSink*>(this),
&ui_source_cookie_);
}
bool TSFEventRouter::Delegate::IsImeComposing() {
return context_ && GetCompositionRange(context_.Get()).IsValid();
}
// static
gfx::Range TSFEventRouter::Delegate::GetCompositionRange(ITfContext* context) {
DCHECK(context);
Microsoft::WRL::ComPtr<ITfContextComposition> context_composition;
if (FAILED(context->QueryInterface(IID_PPV_ARGS(&context_composition))))
return gfx::Range::InvalidRange();
Microsoft::WRL::ComPtr<IEnumITfCompositionView> enum_composition_view;
if (FAILED(context_composition->EnumCompositions(&enum_composition_view)))
return gfx::Range::InvalidRange();
Microsoft::WRL::ComPtr<ITfCompositionView> composition_view;
if (enum_composition_view->Next(1, &composition_view, nullptr) != S_OK)
return gfx::Range::InvalidRange();
Microsoft::WRL::ComPtr<ITfRange> range;
if (FAILED(composition_view->GetRange(&range)))
return gfx::Range::InvalidRange();
Microsoft::WRL::ComPtr<ITfRangeACP> range_acp;
if (FAILED(range.As(&range_acp)))
return gfx::Range::InvalidRange();
LONG start = 0;
LONG length = 0;
if (FAILED(range_acp->GetExtent(&start, &length)))
return gfx::Range::InvalidRange();
return gfx::Range(start, start + length);
}
bool TSFEventRouter::Delegate::IsCandidateWindowInternal(DWORD element_id) {
DCHECK(ui_element_manager_.Get());
Microsoft::WRL::ComPtr<ITfUIElement> ui_element;
if (FAILED(ui_element_manager_->GetUIElement(element_id, &ui_element)))
return false;
Microsoft::WRL::ComPtr<ITfCandidateListUIElement> candidate_list_ui_element;
return SUCCEEDED(ui_element.As(&candidate_list_ui_element));
}
// TSFEventRouter ------------------------------------------------------------
TSFEventRouter::TSFEventRouter(TSFEventRouterObserver* observer)
: observer_(observer) {
DCHECK(observer_);
CComObject<Delegate>* delegate;
ui::win::CreateATLModuleIfNeeded();
if (SUCCEEDED(CComObject<Delegate>::CreateInstance(&delegate))) {
delegate_ = delegate;
delegate_->SetRouter(this);
}
}
TSFEventRouter::~TSFEventRouter() {
if (delegate_) {
delegate_->SetManager(nullptr);
delegate_->SetRouter(nullptr);
}
}
bool TSFEventRouter::IsImeComposing() {
return delegate_->IsImeComposing();
}
void TSFEventRouter::OnCandidateWindowCountChanged(size_t window_count) {
observer_->OnCandidateWindowCountChanged(window_count);
}
void TSFEventRouter::OnTSFStartComposition() {
observer_->OnTSFStartComposition();
}
void TSFEventRouter::OnTextUpdated(const gfx::Range& composition_range) {
observer_->OnTextUpdated(composition_range);
}
void TSFEventRouter::OnTSFEndComposition() {
observer_->OnTSFEndComposition();
}
void TSFEventRouter::SetManager(ITfThreadMgr* thread_manager) {
delegate_->SetManager(thread_manager);
}
} // namespace ui