chromium/ui/base/ime/win/tsf_event_router.cc

// 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