| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #define INITGUID // required for GUID_PROP_INPUTSCOPE |
| #include "ui/base/ime/win/tsf_text_store.h" |
| |
| #include <InputScope.h> |
| #include <OleCtl.h> |
| #include <wrl/client.h> |
| |
| #include <algorithm> |
| |
| #include "base/logging.h" |
| #include "base/numerics/ranges.h" |
| #include "base/trace_event/trace_event.h" |
| #include "base/win/scoped_variant.h" |
| #include "ui/base/ime/text_input_client.h" |
| #include "ui/base/ime/win/tsf_input_scope.h" |
| #include "ui/display/win/screen_win.h" |
| #include "ui/events/event_dispatcher.h" |
| #include "ui/gfx/geometry/rect.h" |
| |
| namespace ui { |
| namespace { |
| |
| // We support only one view. |
| const TsViewCookie kViewCookie = 1; |
| |
| // Fetches the client rectangle, top left and bottom right points using the |
| // window handle in screen coordinates. |
| bool GetWindowClientRect(HWND window_handle, |
| POINT* left_top, |
| POINT* right_bottom) { |
| RECT client_rect = {}; |
| if (!IsWindow(window_handle)) |
| return false; |
| if (!GetClientRect(window_handle, &client_rect)) |
| return false; |
| *left_top = {client_rect.left, client_rect.top}; |
| *right_bottom = {client_rect.right, client_rect.bottom}; |
| if (!ClientToScreen(window_handle, left_top)) |
| return false; |
| if (!ClientToScreen(window_handle, right_bottom)) |
| return false; |
| return true; |
| } |
| |
| } // namespace |
| |
| TSFTextStore::TSFTextStore() { |
| TRACE_EVENT0("ime", "TSFTextStore::TSFTextStore"); |
| } |
| |
| TSFTextStore::~TSFTextStore() {} |
| |
| HRESULT TSFTextStore::Initialize() { |
| HRESULT hr = ::CoCreateInstance(CLSID_TF_CategoryMgr, nullptr, CLSCTX_ALL, |
| IID_PPV_ARGS(&category_manager_)); |
| if (FAILED(hr)) { |
| DVLOG(1) << "Failed to initialize CategoryMgr."; |
| return hr; |
| } |
| |
| hr = ::CoCreateInstance(CLSID_TF_DisplayAttributeMgr, nullptr, CLSCTX_ALL, |
| IID_PPV_ARGS(&display_attribute_manager_)); |
| if (FAILED(hr)) { |
| DVLOG(1) << "Failed to initialize DisplayAttributeMgr."; |
| return hr; |
| } |
| |
| return S_OK; |
| } |
| |
| ULONG STDMETHODCALLTYPE TSFTextStore::AddRef() { |
| return InterlockedIncrement(&ref_count_); |
| } |
| |
| ULONG STDMETHODCALLTYPE TSFTextStore::Release() { |
| const LONG count = InterlockedDecrement(&ref_count_); |
| if (!count) { |
| delete this; |
| return 0; |
| } |
| return static_cast<ULONG>(count); |
| } |
| |
| HRESULT TSFTextStore::QueryInterface(REFIID iid, void** result) { |
| if (iid == IID_IUnknown || iid == IID_ITextStoreACP) { |
| *result = static_cast<ITextStoreACP*>(this); |
| } else if (iid == IID_ITfContextOwnerCompositionSink) { |
| *result = static_cast<ITfContextOwnerCompositionSink*>(this); |
| } else if (iid == IID_ITfTextEditSink) { |
| *result = static_cast<ITfTextEditSink*>(this); |
| } else if (iid == IID_ITfKeyTraceEventSink) { |
| *result = static_cast<ITfKeyTraceEventSink*>(this); |
| } else { |
| *result = nullptr; |
| return E_NOINTERFACE; |
| } |
| AddRef(); |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::AdviseSink(REFIID iid, IUnknown* unknown, DWORD mask) { |
| if (!IsEqualGUID(iid, IID_ITextStoreACPSink)) |
| return E_INVALIDARG; |
| if (text_store_acp_sink_) { |
| if (text_store_acp_sink_.Get() == unknown) { |
| text_store_acp_sink_mask_ = mask; |
| return S_OK; |
| } else { |
| return CONNECT_E_ADVISELIMIT; |
| } |
| } |
| if (FAILED(unknown->QueryInterface(IID_PPV_ARGS(&text_store_acp_sink_)))) |
| return E_UNEXPECTED; |
| text_store_acp_sink_mask_ = mask; |
| |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::FindNextAttrTransition(LONG acp_start, |
| LONG acp_halt, |
| ULONG num_filter_attributes, |
| const TS_ATTRID* filter_attributes, |
| DWORD flags, |
| LONG* acp_next, |
| BOOL* found, |
| LONG* found_offset) { |
| if (!acp_next || !found || !found_offset) |
| return E_INVALIDARG; |
| // We don't support any attributes. |
| // So we always return "not found". |
| *acp_next = 0; |
| *found = FALSE; |
| *found_offset = 0; |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::GetACPFromPoint(TsViewCookie view_cookie, |
| const POINT* point, |
| DWORD flags, |
| LONG* acp) { |
| NOTIMPLEMENTED(); |
| if (view_cookie != kViewCookie) |
| return E_INVALIDARG; |
| return E_NOTIMPL; |
| } |
| |
| HRESULT TSFTextStore::GetActiveView(TsViewCookie* view_cookie) { |
| if (!view_cookie) |
| return E_INVALIDARG; |
| // We support only one view. |
| *view_cookie = kViewCookie; |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::GetEmbedded(LONG acp_pos, |
| REFGUID service, |
| REFIID iid, |
| IUnknown** unknown) { |
| // We don't support any embedded objects. |
| NOTIMPLEMENTED(); |
| if (!unknown) |
| return E_INVALIDARG; |
| *unknown = nullptr; |
| return E_NOTIMPL; |
| } |
| |
| HRESULT TSFTextStore::GetEndACP(LONG* acp) { |
| if (!acp) |
| return E_INVALIDARG; |
| if (!HasReadLock()) |
| return TS_E_NOLOCK; |
| *acp = string_buffer_document_.size(); |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::GetFormattedText(LONG acp_start, |
| LONG acp_end, |
| IDataObject** data_object) { |
| NOTIMPLEMENTED(); |
| return E_NOTIMPL; |
| } |
| |
| HRESULT TSFTextStore::GetScreenExt(TsViewCookie view_cookie, RECT* rect) { |
| if (view_cookie != kViewCookie) |
| return E_INVALIDARG; |
| if (!rect) |
| return E_INVALIDARG; |
| if (!text_input_client_) |
| return E_UNEXPECTED; |
| |
| // {0, 0, 0, 0} means that the document rect is not currently displayed. |
| SetRect(rect, 0, 0, 0, 0); |
| base::Optional<gfx::Rect> result_rect; |
| base::Optional<gfx::Rect> tmp_rect; |
| // If the EditContext is active, then fetch the layout bounds from |
| // the active EditContext, else get it from the focused element's |
| // bounding client rect. |
| text_input_client_->GetActiveTextInputControlLayoutBounds(&result_rect, |
| &tmp_rect); |
| if (result_rect) { |
| // This conversion is required for high dpi monitors. |
| *rect = display::win::ScreenWin::DIPToScreenRect(window_handle_, |
| result_rect.value()) |
| .ToRECT(); |
| } else { |
| // Default if the layout bounds are not present in text input client. |
| POINT left_top; |
| POINT right_bottom; |
| if (!GetWindowClientRect(window_handle_, &left_top, &right_bottom)) |
| return E_FAIL; |
| rect->left = left_top.x; |
| rect->top = left_top.y; |
| rect->right = right_bottom.x; |
| rect->bottom = right_bottom.y; |
| } |
| |
| TRACE_EVENT1("ime", "TSFTextStore::GetScreenExt", "control_bounding_rect", |
| gfx::Rect(*rect).ToString()); |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::GetSelection(ULONG selection_index, |
| ULONG selection_buffer_size, |
| TS_SELECTION_ACP* selection_buffer, |
| ULONG* fetched_count) { |
| if (!selection_buffer) |
| return E_INVALIDARG; |
| if (!fetched_count) |
| return E_INVALIDARG; |
| if (!HasReadLock()) |
| return TS_E_NOLOCK; |
| *fetched_count = 0; |
| if ((selection_buffer_size > 0) && |
| ((selection_index == 0) || (selection_index == TS_DEFAULT_SELECTION))) { |
| selection_buffer[0].acpStart = selection_.start(); |
| selection_buffer[0].acpEnd = selection_.end(); |
| selection_buffer[0].style.ase = TS_AE_END; |
| selection_buffer[0].style.fInterimChar = FALSE; |
| *fetched_count = 1; |
| } |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::GetStatus(TS_STATUS* status) { |
| if (!status) |
| return E_INVALIDARG; |
| |
| status->dwDynamicFlags |= TS_SD_INPUTPANEMANUALDISPLAYENABLE; |
| // We don't support hidden text. |
| // TODO(IME): Remove TS_SS_TRANSITORY to support Korean reconversion |
| status->dwStaticFlags = TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT; |
| |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::GetText(LONG acp_start, |
| LONG acp_end, |
| wchar_t* text_buffer, |
| ULONG text_buffer_size, |
| ULONG* text_buffer_copied, |
| TS_RUNINFO* run_info_buffer, |
| ULONG run_info_buffer_size, |
| ULONG* run_info_buffer_copied, |
| LONG* next_acp) { |
| if (!text_buffer_copied || !run_info_buffer_copied) |
| return E_INVALIDARG; |
| if (!text_buffer && text_buffer_size != 0) |
| return E_INVALIDARG; |
| if (!run_info_buffer && run_info_buffer_size != 0) |
| return E_INVALIDARG; |
| if (!next_acp) |
| return E_INVALIDARG; |
| if (!HasReadLock()) |
| return TF_E_NOLOCK; |
| const LONG string_buffer_size = string_buffer_document_.size(); |
| if (acp_end == -1) |
| acp_end = string_buffer_size; |
| if (!((0 <= acp_start) && (acp_start <= acp_end) && |
| (acp_end <= string_buffer_size))) { |
| return TF_E_INVALIDPOS; |
| } |
| acp_end = std::min(acp_end, acp_start + static_cast<LONG>(text_buffer_size)); |
| *text_buffer_copied = acp_end - acp_start; |
| |
| const base::string16& result = |
| string_buffer_document_.substr(acp_start, *text_buffer_copied); |
| for (size_t i = 0; i < result.size(); ++i) { |
| text_buffer[i] = result[i]; |
| } |
| |
| if (*text_buffer_copied > 0 && run_info_buffer_size) { |
| run_info_buffer[0].uCount = *text_buffer_copied; |
| run_info_buffer[0].type = TS_RT_PLAIN; |
| *run_info_buffer_copied = 1; |
| } else { |
| *run_info_buffer_copied = 0; |
| } |
| |
| *next_acp = acp_end; |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::GetTextExt(TsViewCookie view_cookie, |
| LONG acp_start, |
| LONG acp_end, |
| RECT* rect, |
| BOOL* clipped) { |
| if (!rect || !clipped) |
| return E_INVALIDARG; |
| if (!text_input_client_) |
| return E_UNEXPECTED; |
| if (view_cookie != kViewCookie) |
| return E_INVALIDARG; |
| if (!HasReadLock()) |
| return TS_E_NOLOCK; |
| if (!((static_cast<LONG>(composition_start_) <= acp_start) && |
| (acp_start <= acp_end) && |
| (acp_end <= static_cast<LONG>(string_buffer_document_.size())))) { |
| return TS_E_INVALIDPOS; |
| } |
| |
| // According to a behavior of notepad.exe and wordpad.exe, top left corner of |
| // rect indicates a first character's one, and bottom right corner of rect |
| // indicates a last character's one. |
| // TODO(IME): add tests for scenario that left position is bigger than right |
| // position. |
| base::Optional<gfx::Rect> result_rect; |
| base::Optional<gfx::Rect> tmp_opt_rect; |
| const uint32_t start_pos = acp_start - composition_start_; |
| const uint32_t end_pos = acp_end - composition_start_; |
| // If there is an active EditContext, then fetch the layout bounds from it. |
| text_input_client_->GetActiveTextInputControlLayoutBounds(&tmp_opt_rect, |
| &result_rect); |
| if (result_rect) { |
| *rect = display::win::ScreenWin::DIPToScreenRect(window_handle_, |
| result_rect.value()) |
| .ToRECT(); |
| *clipped = FALSE; |
| return S_OK; |
| } |
| |
| gfx::Rect tmp_rect; |
| if (start_pos == end_pos) { |
| if (text_input_client_->HasCompositionText()) { |
| // According to MSDN document, if |acp_start| and |acp_end| are equal it |
| // is OK to just return E_INVALIDARG. |
| // http://msdn.microsoft.com/en-us/library/ms538435 |
| // But when using Pinin IME of Windows 8, this method is called with the |
| // equal values of |acp_start| and |acp_end|. So we handle this condition. |
| if (start_pos == 0) { |
| if (text_input_client_->GetCompositionCharacterBounds(0, &tmp_rect)) { |
| tmp_rect.set_width(0); |
| result_rect = gfx::Rect(tmp_rect); |
| } else { |
| // PPAPI flash does not support GetCompositionCharacterBounds. We need |
| // to call GetCaretBounds instead to get correct text bounds info. |
| // TODO(https://crbug.com/963706): Remove this hack. |
| result_rect = gfx::Rect(text_input_client_->GetCaretBounds()); |
| } |
| } else if (text_input_client_->GetCompositionCharacterBounds( |
| start_pos - 1, &tmp_rect)) { |
| tmp_rect.set_x(tmp_rect.right()); |
| tmp_rect.set_width(0); |
| result_rect = gfx::Rect(tmp_rect); |
| |
| } else { |
| return TS_E_NOLAYOUT; |
| } |
| } else { |
| result_rect = gfx::Rect(text_input_client_->GetCaretBounds()); |
| } |
| } else { |
| if (text_input_client_->HasCompositionText()) { |
| if (text_input_client_->GetCompositionCharacterBounds(start_pos, |
| &tmp_rect)) { |
| result_rect = gfx::Rect(tmp_rect); |
| if (text_input_client_->GetCompositionCharacterBounds(end_pos - 1, |
| &tmp_rect)) { |
| result_rect->set_width(tmp_rect.x() - result_rect->x() + |
| tmp_rect.width()); |
| result_rect->set_height(tmp_rect.y() - result_rect->y() + |
| tmp_rect.height()); |
| } else { |
| // We may not be able to get the last character bounds, so we use the |
| // first character bounds instead of returning TS_E_NOLAYOUT. |
| } |
| } else { |
| // PPAPI flash does not support GetCompositionCharacterBounds. We need |
| // to call GetCaretBounds instead to get correct text bounds info. |
| // TODO(https://crbug.com/963706): Remove this hack. |
| if (start_pos == 0) { |
| result_rect = gfx::Rect(text_input_client_->GetCaretBounds()); |
| } else { |
| return TS_E_NOLAYOUT; |
| } |
| } |
| } else { |
| // Caret Bounds may be incorrect if focus is in flash control and |
| // |start_pos| is not equal to |end_pos|. In this case, it's better to |
| // return previous caret rectangle instead. |
| result_rect = gfx::Rect(text_input_client_->GetCaretBounds()); |
| } |
| } |
| *rect = display::win::ScreenWin::DIPToScreenRect(window_handle_, |
| result_rect.value()) |
| .ToRECT(); |
| *clipped = FALSE; |
| TRACE_EVENT1("ime", "TSFTextStore::GetTextExt", "selection_bounding_rect", |
| gfx::Rect(*rect).ToString()); |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::GetWnd(TsViewCookie view_cookie, HWND* window_handle) { |
| if (!window_handle) |
| return E_INVALIDARG; |
| if (view_cookie != kViewCookie) |
| return E_INVALIDARG; |
| *window_handle = window_handle_; |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::InsertEmbedded(DWORD flags, |
| LONG acp_start, |
| LONG acp_end, |
| IDataObject* data_object, |
| TS_TEXTCHANGE* change) { |
| // We don't support any embedded objects. |
| NOTIMPLEMENTED(); |
| return E_NOTIMPL; |
| } |
| |
| HRESULT TSFTextStore::InsertEmbeddedAtSelection(DWORD flags, |
| IDataObject* data_object, |
| LONG* acp_start, |
| LONG* acp_end, |
| TS_TEXTCHANGE* change) { |
| // We don't support any embedded objects. |
| NOTIMPLEMENTED(); |
| return E_NOTIMPL; |
| } |
| |
| HRESULT TSFTextStore::InsertTextAtSelection(DWORD flags, |
| const wchar_t* text_buffer, |
| ULONG text_buffer_size, |
| LONG* acp_start, |
| LONG* acp_end, |
| TS_TEXTCHANGE* text_change) { |
| const LONG start_pos = selection_.start(); |
| const LONG end_pos = selection_.end(); |
| const LONG new_end_pos = start_pos + text_buffer_size; |
| |
| if (flags & TS_IAS_QUERYONLY) { |
| if (!HasReadLock()) |
| return TS_E_NOLOCK; |
| if (acp_start) |
| *acp_start = start_pos; |
| if (acp_end) { |
| *acp_end = end_pos; |
| } |
| return S_OK; |
| } |
| |
| if (!HasReadWriteLock()) |
| return TS_E_NOLOCK; |
| if (!text_buffer) |
| return E_INVALIDARG; |
| |
| if (text_buffer_size >= 0) { |
| if (!new_text_inserted_) { |
| new_text_inserted_ = true; |
| replace_text_range_.set_start(start_pos); |
| replace_text_range_.set_end(end_pos); |
| replace_text_size_ = text_buffer_size; |
| } else { |
| // aggregate new replace text with previous replace text into one range. |
| LONG old_delta = (LONG)replace_text_range_.start() - |
| (LONG)replace_text_range_.end() + replace_text_size_; |
| LONG new_delta = start_pos - end_pos + text_buffer_size; |
| replace_text_range_.set_start( |
| std::min((uint32_t)start_pos, replace_text_range_.start())); |
| // New replacement text ends after previous replacement text. We need to |
| // use the new end after adjusting with previous delta. |
| if ((uint32_t)end_pos >= |
| replace_text_range_.start() + replace_text_size_) { |
| replace_text_range_.set_end(end_pos - old_delta); |
| } |
| replace_text_size_ = replace_text_range_.length() + old_delta + new_delta; |
| } |
| } |
| |
| DCHECK_LE(start_pos, end_pos); |
| string_buffer_document_ = |
| string_buffer_document_.substr(0, start_pos) + |
| base::string16(text_buffer, text_buffer + text_buffer_size) + |
| string_buffer_document_.substr(end_pos); |
| |
| // reconstruct string that needs to be inserted. |
| string_pending_insertion_ = |
| string_buffer_document_.substr(start_pos, text_buffer_size); |
| |
| if (acp_start) |
| *acp_start = start_pos; |
| if (acp_end) |
| *acp_end = new_end_pos; |
| if (text_change) { |
| text_change->acpStart = start_pos; |
| text_change->acpOldEnd = end_pos; |
| text_change->acpNewEnd = new_end_pos; |
| } |
| selection_.set_start(start_pos); |
| selection_.set_end(new_end_pos); |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::QueryInsert(LONG acp_test_start, |
| LONG acp_test_end, |
| ULONG text_size, |
| LONG* acp_result_start, |
| LONG* acp_result_end) { |
| if (!acp_result_start || !acp_result_end || acp_test_start > acp_test_end) |
| return E_INVALIDARG; |
| const LONG composition_start = static_cast<LONG>(composition_start_); |
| const LONG buffer_size = static_cast<LONG>(string_buffer_document_.size()); |
| *acp_result_start = |
| base::ClampToRange(acp_test_start, composition_start, buffer_size); |
| *acp_result_end = |
| base::ClampToRange(acp_test_end, composition_start, buffer_size); |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::QueryInsertEmbedded(const GUID* service, |
| const FORMATETC* format, |
| BOOL* insertable) { |
| if (!format) |
| return E_INVALIDARG; |
| // We don't support any embedded objects. |
| if (insertable) |
| *insertable = FALSE; |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::RequestAttrsAtPosition(LONG acp_pos, |
| ULONG attribute_buffer_size, |
| const TS_ATTRID* attribute_buffer, |
| DWORD flags) { |
| // We don't support any document attributes. |
| // This method just returns S_OK, and the subsequently called |
| // RetrieveRequestedAttrs() returns 0 as the number of supported attributes. |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::RequestAttrsTransitioningAtPosition( |
| LONG acp_pos, |
| ULONG attribute_buffer_size, |
| const TS_ATTRID* attribute_buffer, |
| DWORD flags) { |
| // We don't support any document attributes. |
| // This method just returns S_OK, and the subsequently called |
| // RetrieveRequestedAttrs() returns 0 as the number of supported attributes. |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::RequestLock(DWORD lock_flags, HRESULT* result) { |
| if (!text_input_client_) |
| return E_UNEXPECTED; |
| |
| if (!text_store_acp_sink_.Get()) |
| return E_FAIL; |
| if (!result) |
| return E_INVALIDARG; |
| |
| if (current_lock_type_ != 0) { |
| if (lock_flags & TS_LF_SYNC) { |
| // Can't lock synchronously. |
| *result = TS_E_SYNCHRONOUS; |
| return S_OK; |
| } |
| // Queue the lock request. |
| lock_queue_.push_back(lock_flags & TS_LF_READWRITE); |
| *result = TS_S_ASYNC; |
| return S_OK; |
| } |
| |
| // Lock |
| current_lock_type_ = (lock_flags & TS_LF_READWRITE); |
| |
| edit_flag_ = false; |
| // if there is not already some composition text, they we are about to start |
| // composition. we need to set last_composition_start to the selection start. |
| // Otherwise we are updating an existing composition, we should use the cached |
| // composition_start_ for reference. |
| const size_t last_composition_start = text_input_client_->HasCompositionText() |
| ? composition_start_ |
| : selection_.start(); |
| |
| // Grant the lock. |
| *result = text_store_acp_sink_->OnLockGranted(current_lock_type_); |
| |
| // Unlock |
| current_lock_type_ = 0; |
| |
| // Handles the pending lock requests. |
| while (!lock_queue_.empty()) { |
| current_lock_type_ = lock_queue_.front(); |
| lock_queue_.pop_front(); |
| text_store_acp_sink_->OnLockGranted(current_lock_type_); |
| current_lock_type_ = 0; |
| } |
| |
| // if nothing has changed from input service, then only need to |
| // compare our cache with latest textinputstate. |
| if (!edit_flag_) { |
| ResetCacheAfterEditSession(); |
| CalculateTextandSelectionDiffAndNotifyIfNeeded(); |
| return S_OK; |
| } |
| |
| if (!text_input_client_) |
| return E_UNEXPECTED; |
| |
| // If string_pending_insertion_ is empty, then there are three cases: |
| // 1. there is no composition We only need to do comparison between our |
| // cache and latest textinputstate and send notifications accordingly. |
| // 2. A new composition is about to start on existing text. We need to start |
| // composition on range from composition_range_. |
| // 3. There is composition. User cancels the composition by deleting all of |
| // the composing text, we need to reset the composition_start_ and call |
| // into blink to complete the existing composition(later in this method). |
| if (string_pending_insertion_.empty()) { |
| if (!text_input_client_->HasCompositionText()) { |
| if (has_composition_range_ && on_start_composition_called_) { |
| is_tic_write_in_progress_ = true; |
| // Remove replacing text first before starting composition. |
| if (new_text_inserted_ && !replace_text_range_.is_empty() && |
| !replace_text_size_) { |
| text_input_client_->SetEditableSelectionRange(replace_text_range_); |
| text_input_client_->ExtendSelectionAndDelete(0, 0); |
| } |
| string_pending_insertion_ = string_buffer_document_.substr( |
| composition_range_.GetMin(), composition_range_.length()); |
| StartCompositionOnExistingText(); |
| is_tic_write_in_progress_ = false; |
| } else { |
| composition_start_ = selection_.start(); |
| CalculateTextandSelectionDiffAndNotifyIfNeeded(); |
| } |
| ResetCacheAfterEditSession(); |
| return S_OK; |
| } else { |
| composition_start_ = last_composition_start; |
| } |
| } |
| |
| // If we saved a keydown event before this, now is the right time to fire it |
| // We should only fire JS key event during composition or OnStartComposition() |
| // is called during current edit session. |
| if ((has_composition_range_ || on_start_composition_called_) && |
| wparam_keydown_cached_ != 0 && lparam_keydown_cached_ != 0) { |
| DispatchKeyEvent(ui::ET_KEY_PRESSED, wparam_keydown_cached_, |
| lparam_keydown_cached_); |
| } |
| |
| // If the text store is edited in OnLockGranted(), we may need to call |
| // TextInputClient::InsertText() or TextInputClient::SetCompositionText(). |
| // Calculate the end location. we use the replace text end pos if there is no |
| // more active composition. |
| size_t new_composition_start = |
| !has_composition_range_ && new_text_inserted_ |
| ? replace_text_range_.start() + replace_text_size_ |
| : composition_start_; |
| |
| // There are several scenarios that we want to commit composition text. For |
| // those scenarios, we need to call TextInputClient::InsertText to complete |
| // the current composition. When there are some committed text. |
| // 1. If new_composition_start is greater than last_composition_start and |
| // there is active composition, then we know that there are some committed |
| // text. It is not necessarily true that composition_string is empty. We need |
| // to complete current composition with committed text and start new |
| // composition with composition_string. |
| // 2. If the replacement text is coming from on-screen keyboard, we should |
| // replace current selection with new text. |
| // 3. User commits current composition text. |
| if (((new_composition_start > last_composition_start && |
| text_input_client_->HasCompositionText()) || |
| !has_composition_range_) && |
| text_input_client_) { |
| is_tic_write_in_progress_ = true; |
| CommitTextAndEndCompositionIfAny(last_composition_start, |
| new_composition_start); |
| is_tic_write_in_progress_ = false; |
| } |
| |
| const base::string16& composition_string = string_buffer_document_.substr( |
| composition_range_.GetMin(), composition_range_.length()); |
| |
| // Only need to set composition if the current composition string |
| // (composition_string) is not the same as previous composition string |
| // (prev_composition_string_) during same composition or the composition |
| // string is the same for different composition or selection is changed during |
| // composition or IME spans are changed during same composition. If |
| // composition_string is empty and there is an existing composition going on, |
| // we still need to call into blink to complete the composition started by |
| // TSF. |
| if ((has_composition_range_ && |
| (previous_composition_start_ != composition_range_.start() || |
| previous_composition_string_ != composition_string || |
| !previous_composition_selection_range_.EqualsIgnoringDirection( |
| selection_) || |
| previous_text_spans_ != text_spans_)) || |
| ((wparam_keydown_fired_ != 0) && |
| text_input_client_->HasCompositionText() && |
| composition_string.empty())) { |
| previous_composition_string_ = composition_string; |
| previous_composition_start_ = composition_range_.start(); |
| previous_composition_selection_range_ = selection_; |
| previous_text_spans_ = text_spans_; |
| |
| // We need to remove replacing text first before starting new composition if |
| // there are any. |
| is_tic_write_in_progress_ = true; |
| if (new_text_inserted_ && !replace_text_range_.is_empty() && |
| !text_input_client_->HasCompositionText() && |
| last_composition_start > replace_text_range_.start()) { |
| text_input_client_->ExtendSelectionAndDelete( |
| last_composition_start - replace_text_range_.start(), 0); |
| } |
| |
| StartCompositionOnNewText(new_composition_start, composition_string); |
| is_tic_write_in_progress_ = false; |
| } |
| |
| ResetCacheAfterEditSession(); |
| CalculateTextandSelectionDiffAndNotifyIfNeeded(); |
| |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::RequestSupportedAttrs( |
| DWORD /* flags */, // Seems that we should ignore this. |
| ULONG attribute_buffer_size, |
| const TS_ATTRID* attribute_buffer) { |
| if (!attribute_buffer) |
| return E_INVALIDARG; |
| if (!text_input_client_) |
| return E_FAIL; |
| // We support only input scope attribute. |
| for (size_t i = 0; i < attribute_buffer_size; ++i) { |
| if (IsEqualGUID(GUID_PROP_INPUTSCOPE, attribute_buffer[i])) |
| return S_OK; |
| } |
| return E_FAIL; |
| } |
| |
| HRESULT TSFTextStore::RetrieveRequestedAttrs(ULONG attribute_buffer_size, |
| TS_ATTRVAL* attribute_buffer, |
| ULONG* attribute_buffer_copied) { |
| if (!attribute_buffer_copied) |
| return E_INVALIDARG; |
| if (!attribute_buffer) |
| return E_INVALIDARG; |
| if (!text_input_client_) |
| return E_UNEXPECTED; |
| // We support only input scope attribute. |
| *attribute_buffer_copied = 0; |
| if (attribute_buffer_size == 0) |
| return S_OK; |
| |
| attribute_buffer[0].dwOverlapId = 0; |
| attribute_buffer[0].idAttr = GUID_PROP_INPUTSCOPE; |
| attribute_buffer[0].varValue.vt = VT_UNKNOWN; |
| attribute_buffer[0].varValue.punkVal = |
| tsf_inputscope::CreateInputScope(text_input_client_->GetTextInputType(), |
| text_input_client_->GetTextInputMode(), |
| text_input_client_->ShouldDoLearning()); |
| attribute_buffer[0].varValue.punkVal->AddRef(); |
| *attribute_buffer_copied = 1; |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::SetSelection(ULONG selection_buffer_size, |
| const TS_SELECTION_ACP* selection_buffer) { |
| if (!HasReadWriteLock()) |
| return TF_E_NOLOCK; |
| if (selection_buffer_size > 0) { |
| const LONG start_pos = selection_buffer[0].acpStart; |
| const LONG end_pos = selection_buffer[0].acpEnd; |
| if (!((start_pos <= end_pos) && |
| (end_pos <= static_cast<LONG>(string_buffer_document_.size())))) { |
| return TF_E_INVALIDPOS; |
| } |
| selection_.set_start(start_pos); |
| selection_.set_end(end_pos); |
| is_selection_interim_char_ = selection_buffer[0].style.fInterimChar; |
| } |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::SetText(DWORD flags, |
| LONG acp_start, |
| LONG acp_end, |
| const wchar_t* text_buffer, |
| ULONG text_buffer_size, |
| TS_TEXTCHANGE* text_change) { |
| TRACE_EVENT0("ime", "TSFTextStore::SetText"); |
| if (!HasReadWriteLock()) |
| return TS_E_NOLOCK; |
| |
| TS_SELECTION_ACP selection; |
| selection.acpStart = acp_start; |
| selection.acpEnd = acp_end; |
| selection.style.ase = TS_AE_NONE; |
| selection.style.fInterimChar = 0; |
| |
| HRESULT ret; |
| ret = SetSelection(1, &selection); |
| if (ret != S_OK) |
| return ret; |
| |
| TS_TEXTCHANGE change; |
| ret = InsertTextAtSelection(0, text_buffer, text_buffer_size, &acp_start, |
| &acp_end, &change); |
| if (ret != S_OK) |
| return ret; |
| |
| if (text_change) |
| *text_change = change; |
| |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::UnadviseSink(IUnknown* unknown) { |
| if (text_store_acp_sink_.Get() != unknown) |
| return CONNECT_E_NOCONNECTION; |
| text_store_acp_sink_.Reset(); |
| text_store_acp_sink_mask_ = 0; |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::OnStartComposition(ITfCompositionView* composition_view, |
| BOOL* ok) { |
| TRACE_EVENT0("ime", "TSFTextStore::OnStartComposition"); |
| if (ok) |
| *ok = TRUE; |
| |
| on_start_composition_called_ = true; |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::OnUpdateComposition(ITfCompositionView* composition_view, |
| ITfRange* range) { |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::OnEndComposition(ITfCompositionView* composition_view) { |
| TRACE_EVENT0("ime", "TSFTextStore::OnEndComposition"); |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::OnKeyTraceDown(WPARAM wParam, LPARAM lParam) { |
| // fire the event right away if we're in composition |
| if (has_composition_range_) { |
| DispatchKeyEvent(ui::ET_KEY_PRESSED, wParam, lParam); |
| } else { |
| // we're not in composition but we might be starting it - remember these key |
| // events to fire when composition starts |
| wparam_keydown_cached_ = wParam; |
| lparam_keydown_cached_ = lParam; |
| } |
| return S_OK; |
| } |
| |
| HRESULT TSFTextStore::OnKeyTraceUp(WPARAM wParam, LPARAM lParam) { |
| if (has_composition_range_ || wparam_keydown_fired_ == wParam) { |
| DispatchKeyEvent(ui::ET_KEY_RELEASED, wParam, lParam); |
| } else if (wparam_keydown_cached_ == wParam) { |
| // If we didn't fire corresponding keydown event, then we need to clear the |
| // cached keydown wParam and lParam. |
| wparam_keydown_cached_ = 0; |
| lparam_keydown_cached_ = 0; |
| } |
| return S_OK; |
| } |
| |
| void TSFTextStore::DispatchKeyEvent(ui::EventType type, |
| WPARAM wparam, |
| LPARAM lparam) { |
| if (!text_input_client_) |
| return; |
| |
| if (type == ui::ET_KEY_PRESSED) { |
| // clear the saved values since we just fired a keydown |
| wparam_keydown_cached_ = 0; |
| lparam_keydown_cached_ = 0; |
| wparam_keydown_fired_ = wparam; |
| } else if (type == ui::ET_KEY_RELEASED) { |
| // clear the saved values since we just fired a keyup |
| wparam_keydown_fired_ = 0; |
| } else { |
| // shouldn't expect event other than et_key_pressed and et_key_released; |
| return; |
| } |
| |
| // prepare ui::KeyEvent. |
| UINT message = type == ui::ET_KEY_PRESSED ? WM_KEYDOWN : WM_KEYUP; |
| const MSG key_event_MSG = {window_handle_, message, VK_PROCESSKEY, lparam}; |
| ui::KeyEvent key_event = KeyEventFromMSG(key_event_MSG); |
| |
| if (input_method_delegate_) { |
| input_method_delegate_->DispatchKeyEventPostIME(&key_event); |
| } |
| } |
| |
| HRESULT TSFTextStore::OnEndEdit(ITfContext* context, |
| TfEditCookie read_only_edit_cookie, |
| ITfEditRecord* edit_record) { |
| TRACE_EVENT0("ime", "TSFTextStore::OnEndEdit"); |
| if (!context || !edit_record) |
| return E_INVALIDARG; |
| |
| size_t committed_size; |
| ImeTextSpans spans; |
| if (!GetCompositionStatus(context, read_only_edit_cookie, &committed_size, |
| &spans)) { |
| return S_OK; |
| } |
| text_spans_ = spans; |
| edit_flag_ = true; |
| |
| // This function is guaranteed to be called after each keystroke during |
| // composition Therefore we can use this function to update composition status |
| // after each keystroke. If there is existing composition range, we can cache |
| // the composition range and set composition start position as the start of |
| // composition range. If there is no existing composition range, then we know |
| // that there is no active composition, we then need to reset the cached |
| // composition range and set the new composition start as the current |
| // selection start. |
| DCHECK(context); |
| HRESULT hr = S_OK; |
| Microsoft::WRL::ComPtr<ITfContextComposition> context_composition; |
| hr = context->QueryInterface(IID_PPV_ARGS(&context_composition)); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| Microsoft::WRL::ComPtr<IEnumITfCompositionView> enum_composition_view; |
| hr = context_composition->EnumCompositions(&enum_composition_view); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| Microsoft::WRL::ComPtr<ITfCompositionView> composition_view; |
| Microsoft::WRL::ComPtr<ITfRange> range; |
| Microsoft::WRL::ComPtr<ITfRangeACP> range_acp; |
| if (enum_composition_view->Next(1, &composition_view, nullptr) == S_OK |
| && SUCCEEDED(composition_view->GetRange(&range)) |
| && SUCCEEDED(range->QueryInterface(IID_PPV_ARGS(&range_acp)))) { |
| LONG start = 0; |
| LONG length = 0; |
| // We should only consider it as a valid composition if the |
| // composition range is not collapsed (|length| > 0). |
| if (SUCCEEDED(range_acp->GetExtent(&start, &length)) && length > 0) { |
| composition_start_ = start; |
| has_composition_range_ = true; |
| composition_range_.set_start(start); |
| composition_range_.set_end(start + length); |
| return S_OK; |
| } |
| } |
| |
| composition_start_ = selection_.start(); |
| if (has_composition_range_) |
| ResetCompositionState(); |
| |
| return S_OK; |
| } |
| |
| bool TSFTextStore::GetDisplayAttribute(TfGuidAtom guid_atom, |
| TF_DISPLAYATTRIBUTE* attribute) { |
| TRACE_EVENT0("ime", "TSFTextStore::GetDisplayAttribute"); |
| GUID guid; |
| if (FAILED(category_manager_->GetGUID(guid_atom, &guid))) |
| return false; |
| |
| Microsoft::WRL::ComPtr<ITfDisplayAttributeInfo> display_attribute_info; |
| if (FAILED(display_attribute_manager_->GetDisplayAttributeInfo( |
| guid, &display_attribute_info, nullptr))) { |
| return false; |
| } |
| // Display Attribute can be null so query for attributes only when its |
| // available |
| if (display_attribute_info) |
| return SUCCEEDED(display_attribute_info->GetAttributeInfo(attribute)); |
| return false; |
| } |
| |
| bool TSFTextStore::GetCompositionStatus( |
| ITfContext* context, |
| const TfEditCookie read_only_edit_cookie, |
| size_t* committed_size, |
| ImeTextSpans* spans) { |
| DCHECK(context); |
| DCHECK(committed_size); |
| DCHECK(spans); |
| const GUID* rgGuids[2] = {&GUID_PROP_COMPOSING, &GUID_PROP_ATTRIBUTE}; |
| Microsoft::WRL::ComPtr<ITfReadOnlyProperty> track_property; |
| if (FAILED( |
| context->TrackProperties(rgGuids, 2, nullptr, 0, &track_property))) { |
| return false; |
| } |
| |
| *committed_size = 0; |
| spans->clear(); |
| Microsoft::WRL::ComPtr<ITfRange> start_to_end_range; |
| Microsoft::WRL::ComPtr<ITfRange> end_range; |
| if (FAILED(context->GetStart(read_only_edit_cookie, &start_to_end_range))) { |
| return false; |
| } |
| if (FAILED(context->GetEnd(read_only_edit_cookie, &end_range))) |
| return false; |
| if (FAILED(start_to_end_range->ShiftEndToRange( |
| read_only_edit_cookie, end_range.Get(), TF_ANCHOR_END))) { |
| return false; |
| } |
| |
| Microsoft::WRL::ComPtr<IEnumTfRanges> ranges; |
| if (FAILED(track_property->EnumRanges(read_only_edit_cookie, &ranges, |
| start_to_end_range.Get()))) { |
| return false; |
| } |
| |
| while (true) { |
| Microsoft::WRL::ComPtr<ITfRange> range; |
| if (ranges->Next(1, &range, nullptr) != S_OK) |
| break; |
| base::win::ScopedVariant value; |
| Microsoft::WRL::ComPtr<IEnumTfPropertyValue> enum_prop_value; |
| if (FAILED(track_property->GetValue(read_only_edit_cookie, range.Get(), |
| value.Receive()))) { |
| return false; |
| } |
| if (FAILED(value.AsInput()->punkVal->QueryInterface( |
| IID_PPV_ARGS(&enum_prop_value)))) |
| return false; |
| |
| TF_PROPERTYVAL property_value; |
| bool is_composition = false; |
| bool has_display_attribute = false; |
| TF_DISPLAYATTRIBUTE display_attribute = {}; |
| while (enum_prop_value->Next(1, &property_value, nullptr) == S_OK) { |
| if (IsEqualGUID(property_value.guidId, GUID_PROP_COMPOSING)) { |
| is_composition = (property_value.varValue.lVal == TRUE); |
| } else if (IsEqualGUID(property_value.guidId, GUID_PROP_ATTRIBUTE)) { |
| TfGuidAtom guid_atom = |
| static_cast<TfGuidAtom>(property_value.varValue.lVal); |
| if (GetDisplayAttribute(guid_atom, &display_attribute)) |
| has_display_attribute = true; |
| } |
| VariantClear(&property_value.varValue); |
| } |
| |
| Microsoft::WRL::ComPtr<ITfRangeACP> range_acp; |
| range.As(&range_acp); |
| LONG start_pos, length; |
| range_acp->GetExtent(&start_pos, &length); |
| if (!is_composition) { |
| if (*committed_size < static_cast<size_t>(start_pos + length)) |
| *committed_size = start_pos + length; |
| } else { |
| // Check for the formats of the actively composed text. |
| ImeTextSpan span; |
| span.start_offset = start_pos; |
| span.end_offset = start_pos + length; |
| span.underline_color = SK_ColorBLACK; |
| span.background_color = SK_ColorTRANSPARENT; |
| if (selection_.EqualsIgnoringDirection( |
| gfx::Range(span.start_offset, span.end_offset))) { |
| span.interim_char_selection = is_selection_interim_char_; |
| } |
| if (has_display_attribute) |
| GetStyle(display_attribute, &span); |
| spans->push_back(span); |
| } |
| } |
| return true; |
| } |
| |
| void TSFTextStore::ResetCompositionState() { |
| previous_composition_string_.clear(); |
| previous_composition_start_ = 0; |
| previous_composition_selection_range_ = gfx::Range::InvalidRange(); |
| previous_text_spans_.clear(); |
| |
| string_pending_insertion_.clear(); |
| has_composition_range_ = false; |
| composition_range_.set_start(0); |
| composition_range_.set_end(0); |
| |
| selection_ = gfx::Range(composition_from_client_.end(), |
| composition_from_client_.end()); |
| composition_start_ = selection_.end(); |
| } |
| |
| bool TSFTextStore::TerminateComposition() { |
| TRACE_EVENT0("ime", "TSFTextStore::TerminateComposition"); |
| if (context_ && has_composition_range_) { |
| Microsoft::WRL::ComPtr<ITfContextOwnerCompositionServices> service; |
| |
| if (SUCCEEDED(context_->QueryInterface(IID_PPV_ARGS(&service)))) { |
| service->TerminateComposition(nullptr); |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| void TSFTextStore::CalculateTextandSelectionDiffAndNotifyIfNeeded() { |
| // If this is a re-entrant call, then bail out early so we don't end up |
| // in an infinite loop of sending notifications as TSF calls back into us |
| // when we send a text/selection change notification. |
| if (!text_input_client_ || is_notification_in_progress_ || |
| is_tic_write_in_progress_) |
| return; |
| |
| // TODO(snianu) Perhaps we can do the diff at the TextInputManager layer and |
| // only report the diffs? |
| TRACE_EVENT0("ime", |
| "TSFTextStore::CalculateTextandSelectionDiffAndNotifyIfNeeded"); |
| gfx::Range latest_buffer_range_from_client; |
| base::string16 latest_buffer_from_client; |
| gfx::Range latest_selection_from_client; |
| |
| if (text_input_client_->GetTextRange(&latest_buffer_range_from_client) && |
| text_input_client_->GetTextFromRange(latest_buffer_range_from_client, |
| &latest_buffer_from_client) && |
| text_input_client_->GetEditableSelectionRange( |
| &latest_selection_from_client) && |
| latest_selection_from_client.IsBoundedBy( |
| latest_buffer_range_from_client)) { |
| gfx::Range latest_composition_from_client; |
| if (text_input_client_->HasCompositionText() && |
| text_input_client_->GetCompositionTextRange( |
| &latest_composition_from_client)) |
| composition_from_client_ = latest_composition_from_client; |
| else |
| composition_from_client_ = latest_selection_from_client; |
| // if the text and selection from text input client is the same as the text |
| // and buffer we got last time, either the state hasn't changed since last |
| // time we synced or the change hasn't completed yet. Either case we don't |
| // want to update our buffer and selection cache. We also don't notify |
| // input service about the change. |
| if (!buffer_from_client_.compare(latest_buffer_from_client) && |
| selection_from_client_.EqualsIgnoringDirection( |
| latest_selection_from_client)) { |
| return; |
| } |
| |
| // update cache value for next comparison. |
| buffer_from_client_ = latest_buffer_from_client; |
| selection_from_client_.set_start(latest_selection_from_client.start()); |
| selection_from_client_.set_end(latest_selection_from_client.end()); |
| |
| if (has_composition_range_) { |
| return; |
| } |
| |
| bool notify_text_change = |
| (text_store_acp_sink_mask_ & TS_AS_TEXT_CHANGE) != 0; |
| bool notify_selection_change = |
| (text_store_acp_sink_mask_ & TS_AS_SEL_CHANGE) != 0; |
| |
| bool text_changed = false; |
| bool selection_changed = false; |
| TS_TEXTCHANGE text_change = {}; |
| |
| if (latest_buffer_from_client.compare(string_buffer_document_)) { |
| |
| // Execute diffing algorithm only if we need to send notification. |
| if (notify_text_change) { |
| size_t acp_start = 0; |
| size_t acp_old_end = string_buffer_document_.size(); |
| size_t acp_new_end = latest_buffer_from_client.size(); |
| |
| // Compare two strings to find first difference. |
| for (; acp_start < std::min(latest_buffer_from_client.size(), |
| string_buffer_document_.size()); |
| acp_start++) { |
| if (latest_buffer_from_client.at(acp_start) != |
| string_buffer_document_.at(acp_start)) { |
| break; |
| } |
| } |
| |
| // Compare two strings to find last difference. |
| while (acp_old_end > 0 && acp_new_end > 0) { |
| acp_old_end--; |
| acp_new_end--; |
| if (acp_old_end >= acp_start && acp_new_end >= acp_start) { |
| if (latest_buffer_from_client.at(acp_new_end) != |
| string_buffer_document_.at(acp_old_end)) { |
| acp_old_end++; |
| acp_new_end++; |
| break; |
| } |
| } else { |
| acp_old_end++; |
| acp_new_end++; |
| break; |
| } |
| } |
| |
| text_change.acpStart = acp_start; |
| text_change.acpOldEnd = acp_old_end; |
| text_change.acpNewEnd = acp_new_end; |
| } |
| |
| string_buffer_document_ = latest_buffer_from_client; |
| text_changed = true; |
| } |
| |
| if (!selection_.EqualsIgnoringDirection(latest_selection_from_client)) { |
| selection_.set_start(latest_selection_from_client.GetMin()); |
| selection_.set_end(latest_selection_from_client.GetMax()); |
| |
| selection_changed = true; |
| } |
| |
| // We should notify input service about text/selection change only after |
| // the cache has already been updated because input service may call back |
| // into us during notification. |
| is_notification_in_progress_ = true; |
| if (notify_text_change && text_changed) { |
| TRACE_EVENT2( |
| "ime", "TSFTextStore::CalculateTextandSelectionDiffAndNotifyIfNeeded", |
| "text_change_start", std::to_string(text_change.acpStart), |
| "text_change_end", std::to_string(text_change.acpNewEnd)); |
| text_store_acp_sink_->OnTextChange(0, &text_change); |
| } |
| |
| if (notify_selection_change && selection_changed) { |
| TRACE_EVENT1( |
| "ime", "TSFTextStore::CalculateTextandSelectionDiffAndNotifyIfNeeded", |
| "new_selection", selection_.ToString()); |
| text_store_acp_sink_->OnSelectionChange(); |
| } |
| is_notification_in_progress_ = false; |
| } |
| } |
| |
| void TSFTextStore::OnContextInitialized(ITfContext* context) { |
| context_ = context; |
| } |
| |
| void TSFTextStore::SetFocusedTextInputClient( |
| HWND focused_window, |
| TextInputClient* text_input_client) { |
| window_handle_ = focused_window; |
| text_input_client_ = text_input_client; |
| } |
| |
| void TSFTextStore::RemoveFocusedTextInputClient( |
| TextInputClient* text_input_client) { |
| if (text_input_client_ == text_input_client) { |
| window_handle_ = nullptr; |
| text_input_client_ = nullptr; |
| } |
| } |
| |
| void TSFTextStore::SetInputMethodDelegate( |
| internal::InputMethodDelegate* delegate) { |
| input_method_delegate_ = delegate; |
| } |
| |
| void TSFTextStore::RemoveInputMethodDelegate() { |
| input_method_delegate_ = nullptr; |
| } |
| |
| bool TSFTextStore::CancelComposition() { |
| // This method should correspond to |
| // ImmNotifyIME(NI_COMPOSITIONSTR, CPS_CANCEL, 0) |
| // in IMM32 hence calling falling back to |ConfirmComposition()| is not |
| // technically correct, because |ConfirmComposition()| corresponds to |
| // |CPS_COMPLETE| rather than |CPS_CANCEL|. |
| // However in Chromium it seems that |InputMethod::CancelComposition()| |
| // might have already committed composing text despite its name. |
| // TODO(IME): Check other platforms to see if |CancelComposition()| is |
| // actually working or not. |
| |
| if (edit_flag_ || !text_input_client_) |
| return false; |
| |
| TRACE_EVENT0("ime", "TSFTextStore::CancelComposition"); |
| |
| ResetCompositionState(); |
| |
| return TerminateComposition(); |
| } |
| |
| bool TSFTextStore::ConfirmComposition() { |
| // If there is an on-going document lock, we must not edit the text. |
| if (edit_flag_) |
| return false; |
| |
| if (string_pending_insertion_.empty()) |
| return true; |
| |
| if (!text_input_client_) |
| return false; |
| |
| ResetCompositionState(); |
| |
| return TerminateComposition(); |
| } |
| |
| void TSFTextStore::SendOnLayoutChange() { |
| // A re-entrant call leads to infinite loop in TSF. |
| // We bail out if are in the process of notifying TSF about changes. |
| if (is_notification_in_progress_) |
| return; |
| CalculateTextandSelectionDiffAndNotifyIfNeeded(); |
| if (text_store_acp_sink_ && |
| (text_store_acp_sink_mask_ & TS_AS_LAYOUT_CHANGE)) { |
| TRACE_EVENT0("ime", "TSFTextStore::SendOnLayoutChange"); |
| text_store_acp_sink_->OnLayoutChange(TS_LC_CHANGE, 0); |
| } |
| } |
| |
| bool TSFTextStore::HasReadLock() const { |
| return (current_lock_type_ & TS_LF_READ) == TS_LF_READ; |
| } |
| |
| bool TSFTextStore::HasReadWriteLock() const { |
| return (current_lock_type_ & TS_LF_READWRITE) == TS_LF_READWRITE; |
| } |
| |
| void TSFTextStore::StartCompositionOnExistingText() const { |
| ui::ImeTextSpans text_spans = text_spans_; |
| // Adjusts the offset. |
| for (size_t i = 0; i < text_spans.size(); ++i) { |
| text_spans[i].start_offset -= composition_start_; |
| text_spans[i].end_offset -= composition_start_; |
| } |
| |
| text_input_client_->SetCompositionFromExistingText(composition_range_, |
| text_spans); |
| } |
| |
| void TSFTextStore::CommitTextAndEndCompositionIfAny(size_t old_size, |
| size_t new_size) const { |
| size_t new_committed_string_offset; |
| size_t new_committed_string_size; |
| if (new_text_inserted_ && !text_input_client_->HasCompositionText()) { |
| // This is a special case to handle text replacement scenarios during |
| // English typing when we are trying to replace an existing text with some |
| // new text. Some third-party IMEs also use SetText() API instead of |
| // InsertTextAtSelection() API to insert new text. |
| size_t new_text_size; |
| if (new_size == replace_text_range_.start()) { |
| // This usually happens when TSF is trying to replace a part of a string |
| // from the selection end |
| new_text_size = new_size; |
| } else { |
| new_text_size = new_size - replace_text_range_.start(); |
| } |
| // If |new_text_size| is 0, then we want to commit composition with current |
| // composition text if there is any. Construct |new_committed_string| to be |
| // current composition text so that |TextInputClient::InsertText| will |
| // commit current composition text. |
| // Also clamp the offsets if they are out of bounds of the buffer |
| new_committed_string_offset = |
| std::min(static_cast<ULONG>(replace_text_range_.start()), |
| static_cast<ULONG>(string_buffer_document_.size())); |
| new_committed_string_size = |
| (new_text_size == 0 && selection_.end() > new_committed_string_offset) |
| ? selection_.end() - new_committed_string_offset |
| : new_text_size; |
| // if the |replace_text_range_| start is greater than |old_size|, then we |
| // don't need to delete anything because the replacement text hasn't been |
| // inserted into blink yet. |
| if (old_size > replace_text_range_.start()) { |
| text_input_client_->ExtendSelectionAndDelete( |
| old_size - replace_text_range_.start(), 0); |
| } |
| } else { |
| new_committed_string_offset = old_size; |
| new_committed_string_size = new_size - old_size; |
| // This is a special case. We should only replace existing text and commit |
| // the new text if replacement text has already been inserted into Blink. |
| if (new_text_inserted_ && (old_size > replace_text_range_.start()) && |
| !replace_text_range_.is_empty()) { |
| new_committed_string_offset = replace_text_range_.start(); |
| new_committed_string_size = replace_text_size_; |
| } |
| // If |new_committed_string_size| is 0, then we want to commit composition |
| // with current composition text if there is any. Construct |
| // |new_committed_string| to be current composition text so that |
| // |TextInputClient::InsertText| will commit current composition text. |
| // Also clamp the offsets if they are out of bounds of the buffer |
| new_committed_string_offset = |
| std::min(static_cast<ULONG>(new_committed_string_offset), |
| static_cast<ULONG>(string_buffer_document_.size())); |
| new_committed_string_size = |
| (new_committed_string_size == 0 && |
| selection_.end() > new_committed_string_offset) |
| ? selection_.end() - new_committed_string_offset |
| : new_committed_string_size; |
| } |
| |
| // Construct string to be committed. |
| const base::string16& new_committed_string = string_buffer_document_.substr( |
| new_committed_string_offset, new_committed_string_size); |
| // TODO(crbug.com/978678): Unify the behavior of |
| // |TextInputClient::InsertText(text)| for the empty text. |
| if (!new_committed_string.empty()) { |
| // If composition was started and committed in one edit session, we still |
| // need to start the composition first and then commit it. |
| if (!text_input_client_->HasCompositionText() && |
| on_start_composition_called_) { |
| ImeTextSpans spans; |
| ImeTextSpan span; |
| span.start_offset = 0; |
| span.end_offset = new_committed_string.size(); |
| spans.push_back(span); |
| CompositionText composition_text; |
| composition_text.text = new_committed_string; |
| composition_text.ime_text_spans = spans; |
| composition_text.selection.set_start(new_committed_string.size()); |
| composition_text.selection.set_end(new_committed_string.size()); |
| text_input_client_->SetCompositionText(composition_text); |
| } |
| text_input_client_->InsertText( |
| new_committed_string, |
| ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); |
| } else { |
| text_input_client_->ClearCompositionText(); |
| } |
| // Notify accessibility about this committed composition |
| text_input_client_->SetActiveCompositionForAccessibility( |
| replace_text_range_, new_committed_string, |
| /*is_composition_committed*/ true); |
| } |
| |
| void TSFTextStore::StartCompositionOnNewText( |
| size_t start_offset, |
| const base::string16& composition_string) { |
| CompositionText composition_text; |
| composition_text.text = composition_string; |
| composition_text.ime_text_spans = text_spans_; |
| |
| for (size_t i = 0; i < composition_text.ime_text_spans.size(); ++i) { |
| composition_text.ime_text_spans[i].start_offset -= start_offset; |
| composition_text.ime_text_spans[i].end_offset -= start_offset; |
| } |
| |
| if (selection_.start() < start_offset) { |
| composition_text.selection.set_start(0); |
| } else { |
| composition_text.selection.set_start(selection_.start() - start_offset); |
| } |
| |
| if (selection_.end() < start_offset) { |
| composition_text.selection.set_end(0); |
| } else { |
| composition_text.selection.set_end(selection_.end() - start_offset); |
| } |
| |
| if (text_input_client_) { |
| text_input_client_->SetCompositionText(composition_text); |
| // Notify accessibility about this ongoing composition if the string is not |
| // empty |
| if (!composition_string.empty()) { |
| text_input_client_->SetActiveCompositionForAccessibility( |
| composition_range_, composition_string, |
| /*is_composition_committed*/ false); |
| } else { |
| // User wants to commit the current composition |
| const base::string16& committed_string = string_buffer_document_.substr( |
| composition_range_.GetMin(), composition_range_.length()); |
| text_input_client_->SetActiveCompositionForAccessibility( |
| composition_range_, committed_string, |
| /*is_composition_committed*/ true); |
| } |
| } |
| } |
| |
| void TSFTextStore::GetStyle(const TF_DISPLAYATTRIBUTE& attribute, |
| ImeTextSpan* span) { |
| // Use the display attribute to pick the right formats for the underline and |
| // text. |
| // Set the default values first and then check if display attribute has |
| // any style or not. |
| span->thickness = attribute.fBoldLine ? ImeTextSpan::Thickness::kThick |
| : ImeTextSpan::Thickness::kThin; |
| switch (attribute.lsStyle) { |
| case TF_LS_SOLID: { |
| span->underline_style = ImeTextSpan::UnderlineStyle::kSolid; |
| break; |
| } |
| case TF_LS_DOT: { |
| span->underline_style = ImeTextSpan::UnderlineStyle::kDot; |
| break; |
| } |
| case TF_LS_DASH: { |
| span->underline_style = ImeTextSpan::UnderlineStyle::kDash; |
| break; |
| } |
| case TF_LS_SQUIGGLE: { |
| span->underline_style = ImeTextSpan::UnderlineStyle::kSquiggle; |
| break; |
| } |
| case TF_LS_NONE: { |
| span->underline_style = ImeTextSpan::UnderlineStyle::kNone; |
| break; |
| } |
| default: { |
| span->underline_style = ImeTextSpan::UnderlineStyle::kSolid; |
| } |
| } |
| if (attribute.crText.type != TF_CT_NONE) { |
| span->text_color = SkColorSetRGB(GetRValue(attribute.crText.cr), |
| GetGValue(attribute.crText.cr), |
| GetBValue(attribute.crText.cr)); |
| } |
| if (attribute.crLine.type != TF_CT_NONE) { |
| span->underline_color = SkColorSetRGB(GetRValue(attribute.crLine.cr), |
| GetGValue(attribute.crLine.cr), |
| GetBValue(attribute.crLine.cr)); |
| } |
| } |
| |
| void TSFTextStore::ResetCacheAfterEditSession() { |
| // reset the flag since we've already inserted/replaced the text. |
| new_text_inserted_ = false; |
| is_selection_interim_char_ = false; |
| // reset |on_start_composition_called_| for next edit session. |
| on_start_composition_called_ = false; |
| |
| // reset string_buffer_ if composition is no longer active. |
| if (text_input_client_ && !text_input_client_->HasCompositionText()) |
| string_pending_insertion_.clear(); |
| } |
| |
| } // namespace ui |