| // Copyright 2010-2015, Google Inc. |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above |
| // copyright notice, this list of conditions and the following disclaimer |
| // in the documentation and/or other materials provided with the |
| // distribution. |
| // * Neither the name of Google Inc. nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| #include "win32/tip/tip_surrounding_text.h" |
| |
| #include <Windows.h> |
| #define _ATL_NO_AUTOMATIC_NAMESPACE |
| #define _WTL_NO_AUTOMATIC_NAMESPACE |
| // Workaround against KB813540 |
| #include <atlbase_mozc.h> |
| #include <atlcom.h> |
| #include <msctf.h> |
| |
| #include <memory> |
| |
| #include "base/util.h" |
| #include "win32/base/imm_reconvert_string.h" |
| #include "win32/tip/tip_composition_util.h" |
| #include "win32/tip/tip_range_util.h" |
| #include "win32/tip/tip_ref_count.h" |
| #include "win32/tip/tip_text_service.h" |
| #include "win32/tip/tip_transitory_extension.h" |
| |
| namespace mozc { |
| namespace win32 { |
| namespace tsf { |
| |
| using ATL::CComPtr; |
| using ATL::CComQIPtr; |
| using ATL::CComVariant; |
| using std::unique_ptr; |
| |
| namespace { |
| |
| const int kMaxSurroundingLength = 20; |
| const int kMaxCharacterLength = 1024*1024; |
| |
| class SurroudingTextUpdater : public ITfEditSession { |
| public: |
| SurroudingTextUpdater(ITfContext *context, bool move_anchor) |
| : context_(context), |
| move_anchor_(move_anchor) { |
| } |
| |
| // Destructor is kept as non-virtual because this class is designed to be |
| // destroyed only by "delete this" in Release() method. |
| // TODO(yukawa): put "final" keyword to the class declaration when C++11 |
| // is allowed. |
| ~SurroudingTextUpdater() {} |
| |
| // The IUnknown interface methods. |
| virtual STDMETHODIMP QueryInterface(REFIID interface_id, void **object) { |
| if (!object) { |
| return E_INVALIDARG; |
| } |
| |
| // Find a matching interface from the ones implemented by this object. |
| // This object implements IUnknown and ITfEditSession. |
| if (::IsEqualIID(interface_id, IID_IUnknown)) { |
| *object = static_cast<IUnknown *>(this); |
| } else if (IsEqualIID(interface_id, IID_ITfEditSession)) { |
| *object = static_cast<ITfEditSession *>(this); |
| } else { |
| *object = nullptr; |
| return E_NOINTERFACE; |
| } |
| |
| AddRef(); |
| return S_OK; |
| } |
| |
| virtual STDMETHODIMP_(ULONG) AddRef() { |
| return ref_count_.AddRefImpl(); |
| } |
| |
| virtual STDMETHODIMP_(ULONG) Release() { |
| const ULONG count = ref_count_.ReleaseImpl(); |
| if (count == 0) { |
| delete this; |
| } |
| return count; |
| } |
| |
| const TipSurroundingTextInfo &result() const { |
| return result_; |
| } |
| |
| private: |
| virtual STDMETHODIMP DoEditSession(TfEditCookie edit_cookie) { |
| HRESULT result = S_OK; |
| { |
| TF_STATUS status = {}; |
| result = context_->GetStatus(&status); |
| if (FAILED(result)) { |
| return result; |
| } |
| result_.is_transitory = |
| ((status.dwStaticFlags & TF_SS_TRANSITORY) == TF_SS_TRANSITORY); |
| } |
| { |
| CComPtr<ITfCompositionView> composition_view = |
| TipCompositionUtil::GetComposition(context_, edit_cookie); |
| result_.in_composition = !!composition_view; |
| } |
| |
| CComPtr<ITfRange> selected_range; |
| { |
| result = TipRangeUtil::GetDefaultSelection( |
| context_, edit_cookie, &selected_range, nullptr); |
| if (FAILED(result)) { |
| return result; |
| } |
| |
| result = TipRangeUtil::GetText( |
| selected_range, edit_cookie, &result_.selected_text); |
| result_.has_selected_text = SUCCEEDED(result); |
| |
| // For reconversion, the active selection end should be moved to the |
| // front character. |
| if (move_anchor_) { |
| result = TipRangeUtil::SetSelection( |
| context_, edit_cookie, selected_range, TF_AE_START); |
| if (FAILED(result)) { |
| return result; |
| } |
| } |
| } |
| |
| const TF_HALTCOND halt_cond = {nullptr, TF_ANCHOR_START, TF_HF_OBJECT}; |
| |
| { |
| CComPtr<ITfRange> preceeding_range; |
| LONG preceeding_range_shifted = 0; |
| if (SUCCEEDED(selected_range->Clone(&preceeding_range)) && |
| SUCCEEDED(preceeding_range->Collapse(edit_cookie, |
| TF_ANCHOR_START)) && |
| SUCCEEDED(preceeding_range->ShiftStart(edit_cookie, |
| -kMaxSurroundingLength, |
| &preceeding_range_shifted, |
| &halt_cond))) { |
| result = TipRangeUtil::GetText( |
| preceeding_range, edit_cookie, &result_.preceding_text); |
| result_.has_preceding_text = SUCCEEDED(result); |
| } |
| } |
| |
| { |
| CComPtr<ITfRange> following_range; |
| LONG following_range_shifted = 0; |
| if (SUCCEEDED(selected_range->Clone(&following_range)) && |
| SUCCEEDED(following_range->Collapse(edit_cookie, |
| TF_ANCHOR_END)) && |
| SUCCEEDED(following_range->ShiftEnd(edit_cookie, |
| kMaxSurroundingLength, |
| &following_range_shifted, |
| &halt_cond))) { |
| result = TipRangeUtil::GetText( |
| following_range, edit_cookie, &result_.following_text); |
| result_.has_following_text = SUCCEEDED(result); |
| } |
| } |
| |
| return S_OK; |
| } |
| |
| TipRefCount ref_count_; |
| CComPtr<ITfContext> context_; |
| TipSurroundingTextInfo result_; |
| bool move_anchor_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SurroudingTextUpdater); |
| }; |
| |
| class PrecedingTextDeleter : public ITfEditSession { |
| public: |
| PrecedingTextDeleter( |
| ITfContext *context, size_t num_characters_in_ucs4) |
| : context_(context), |
| num_characters_in_ucs4_(num_characters_in_ucs4) { |
| } |
| |
| // Destructor is kept as non-virtual because this class is designed to be |
| // destroyed only by "delete this" in Release() method. |
| // TODO(yukawa): put "final" keyword to the class declaration when C++11 |
| // is allowed. |
| ~PrecedingTextDeleter() {} |
| |
| // The IUnknown interface methods. |
| virtual STDMETHODIMP QueryInterface(REFIID interface_id, void **object) { |
| if (!object) { |
| return E_INVALIDARG; |
| } |
| |
| // Find a matching interface from the ones implemented by this object. |
| // This object implements IUnknown and ITfEditSession. |
| if (::IsEqualIID(interface_id, IID_IUnknown)) { |
| *object = static_cast<IUnknown *>(this); |
| } else if (IsEqualIID(interface_id, IID_ITfEditSession)) { |
| *object = static_cast<ITfEditSession *>(this); |
| } else { |
| *object = nullptr; |
| return E_NOINTERFACE; |
| } |
| |
| AddRef(); |
| return S_OK; |
| } |
| |
| virtual STDMETHODIMP_(ULONG) AddRef() { |
| return ref_count_.AddRefImpl(); |
| } |
| |
| virtual STDMETHODIMP_(ULONG) Release() { |
| const ULONG count = ref_count_.ReleaseImpl(); |
| if (count == 0) { |
| delete this; |
| } |
| return count; |
| } |
| |
| private: |
| virtual STDMETHODIMP DoEditSession(TfEditCookie edit_cookie) { |
| HRESULT result = S_OK; |
| |
| CComPtr<ITfRange> selected_range; |
| result = TipRangeUtil::GetDefaultSelection( |
| context_, edit_cookie, &selected_range, nullptr); |
| if (FAILED(result)) { |
| return result; |
| } |
| |
| const TF_HALTCOND halt_cond = {nullptr, TF_ANCHOR_START, 0}; |
| |
| CComPtr<ITfRange> preceeding_range; |
| if (FAILED(selected_range->Clone(&preceeding_range))) { |
| return E_FAIL; |
| } |
| if (FAILED(preceeding_range->Collapse(edit_cookie, TF_ANCHOR_START))) { |
| return E_FAIL; |
| } |
| |
| // If all the characters are surrogate-pair, |num_characters_in_ucs4_| * 2 |
| // is required. |
| if (num_characters_in_ucs4_ >= kMaxCharacterLength) { |
| return E_UNEXPECTED; |
| } |
| const LONG initial_offset_utf16 = |
| -static_cast<LONG>(num_characters_in_ucs4_) * 2; |
| LONG preceeding_range_shifted = 0; |
| if (FAILED(preceeding_range->ShiftStart(edit_cookie, |
| initial_offset_utf16, |
| &preceeding_range_shifted, |
| &halt_cond))) { |
| return E_FAIL; |
| } |
| wstring total_string; |
| if (FAILED(TipRangeUtil::GetText( |
| preceeding_range, edit_cookie, &total_string))) { |
| return E_FAIL; |
| } |
| if (total_string.empty()) { |
| return E_FAIL; |
| } |
| |
| size_t len_in_utf16 = 0; |
| if (!TipSurroundingTextUtil::MeasureCharactersBackward( |
| total_string, num_characters_in_ucs4_, &len_in_utf16)) { |
| return E_FAIL; |
| } |
| |
| const LONG final_offset = total_string.size() - len_in_utf16; |
| if (FAILED(preceeding_range->ShiftStart(edit_cookie, |
| final_offset, |
| &preceeding_range_shifted, |
| &halt_cond))) { |
| return E_FAIL; |
| } |
| if (final_offset != preceeding_range_shifted) { |
| return E_FAIL; |
| } |
| if (FAILED(preceeding_range->SetText(edit_cookie, 0, L"", 0))) { |
| return E_FAIL; |
| } |
| |
| return S_OK; |
| } |
| |
| TipRefCount ref_count_; |
| CComPtr<ITfContext> context_; |
| size_t num_characters_in_ucs4_; |
| |
| DISALLOW_COPY_AND_ASSIGN(PrecedingTextDeleter); |
| }; |
| |
| |
| bool PrepareForReconversionIMM32(ITfContext *context, |
| TipSurroundingTextInfo *info) { |
| CComPtr<ITfContextView> context_view; |
| if (FAILED(context->GetActiveView(&context_view))) { |
| return false; |
| } |
| if (context_view == nullptr) { |
| return false; |
| } |
| HWND attached_window = nullptr; |
| if (FAILED(context_view->GetWnd(&attached_window))) { |
| return false; |
| } |
| |
| LRESULT result = ::SendMessage( |
| attached_window, WM_IME_REQUEST, IMR_RECONVERTSTRING, 0); |
| if (result == 0) { |
| // IMR_RECONVERTSTRING is not supported. |
| return false; |
| } |
| |
| const size_t buffer_size = static_cast<size_t>(result); |
| unique_ptr<BYTE[]> buffer(new BYTE[buffer_size]); |
| |
| RECONVERTSTRING *reconvert_string = |
| reinterpret_cast<RECONVERTSTRING *>(buffer.get()); |
| reconvert_string->dwSize = buffer_size; |
| reconvert_string->dwVersion = 0; |
| |
| result = ::SendMessage( |
| attached_window, WM_IME_REQUEST, IMR_RECONVERTSTRING, |
| reinterpret_cast<LPARAM>(reconvert_string)); |
| if (result == 0) { |
| return false; |
| } |
| |
| wstring preceding_text; |
| wstring preceding_composition; |
| wstring target; |
| wstring following_composition; |
| wstring following_text; |
| if (!ReconvertString::Decompose( |
| reconvert_string, &preceding_text, &preceding_composition, &target, |
| &following_composition, &following_text)) { |
| return false; |
| } |
| info->in_composition = false; |
| info->is_transitory = false; |
| info->has_preceding_text = true; |
| info->preceding_text = preceding_text; |
| info->has_selected_text = true; |
| info->selected_text = preceding_composition + target + following_composition; |
| info->has_following_text = true; |
| info->following_text = following_text; |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| TipSurroundingTextInfo::TipSurroundingTextInfo() |
| : has_preceding_text(false), |
| has_selected_text(false), |
| has_following_text(false), |
| is_transitory(false), |
| in_composition(false) { |
| } |
| |
| bool TipSurroundingText::Get(TipTextService *text_service, |
| ITfContext *context, |
| TipSurroundingTextInfo *info) { |
| if (info == nullptr) { |
| return false; |
| } |
| *info = TipSurroundingTextInfo(); |
| |
| // Use Transitory Extensions when supported. Common controls provides |
| // surrounding text via Transitory Extensions. |
| CComPtr<ITfContext> target_context( |
| TipTransitoryExtension::ToParentContextIfExists(context)); |
| |
| // When RequestEditSession fails, it does not maintain the reference count. |
| // So we need to ensure that AddRef/Release should be called at least once |
| // per object. |
| CComPtr<SurroudingTextUpdater> updater( |
| new SurroudingTextUpdater(target_context, false)); |
| |
| HRESULT edit_session_result = S_OK; |
| const HRESULT hr = target_context->RequestEditSession( |
| text_service->GetClientID(), |
| updater, |
| TF_ES_SYNC | TF_ES_READ, |
| &edit_session_result); |
| if (FAILED(hr)) { |
| return false; |
| } |
| if (FAILED(edit_session_result)) { |
| return false; |
| } |
| |
| *info = updater->result(); |
| |
| return true; |
| } |
| |
| bool PrepareForReconversionTSF(TipTextService *text_service, |
| ITfContext *context, |
| TipSurroundingTextInfo *info) { |
| // Use Transitory Extensions when supported. Common controls provides |
| // surrounding text via Transitory Extensions. |
| CComPtr<ITfContext> target_context( |
| TipTransitoryExtension::ToParentContextIfExists(context)); |
| |
| // When RequestEditSession fails, it does not maintain the reference count. |
| // So we need to ensure that AddRef/Release should be called at least once |
| // per oject. |
| CComPtr<SurroudingTextUpdater> updater( |
| new SurroudingTextUpdater(target_context, true)); |
| |
| HRESULT edit_session_result = S_OK; |
| const HRESULT hr = target_context->RequestEditSession( |
| text_service->GetClientID(), |
| updater, |
| TF_ES_SYNC | TF_ES_READWRITE, |
| &edit_session_result); |
| if (FAILED(hr)) { |
| return false; |
| } |
| if (FAILED(edit_session_result)) { |
| return false; |
| } |
| |
| *info = updater->result(); |
| return true; |
| } |
| |
| bool TipSurroundingText::PrepareForReconversionFromIme( |
| TipTextService *text_service, |
| ITfContext *context, |
| TipSurroundingTextInfo *info, |
| bool *need_async_reconversion) { |
| if (info == nullptr) { |
| return false; |
| } |
| if (need_async_reconversion == nullptr) { |
| return false; |
| } |
| *info = TipSurroundingTextInfo(); |
| *need_async_reconversion = false; |
| if (PrepareForReconversionTSF(text_service, context, info)) { |
| // Here we assume selection text info is valid iff |info->is_transitory| is |
| // false. |
| // TODO(yukawa): Investigate more reliable method to determine this. |
| if (!info->is_transitory) { |
| return true; |
| } |
| } |
| if (!PrepareForReconversionIMM32(context, info)) { |
| return false; |
| } |
| // IMM32-like reconversion requires async edit session. |
| *need_async_reconversion = true; |
| return true; |
| } |
| |
| bool TipSurroundingText::DeletePrecedingText( |
| TipTextService *text_service, |
| ITfContext *context, |
| size_t num_characters_to_be_deleted_in_ucs4) { |
| // Use Transitory Extensions when supported. Common controls provides |
| // surrounding text via Transitory Extensions. |
| CComPtr<ITfContext> target_context( |
| TipTransitoryExtension::ToParentContextIfExists(context)); |
| |
| // When RequestEditSession fails, it does not maintain the reference count. |
| // So we need to ensure that AddRef/Release should be called at least once |
| // per oject. |
| CComPtr<ITfEditSession> edit_session(new PrecedingTextDeleter( |
| target_context, num_characters_to_be_deleted_in_ucs4)); |
| |
| HRESULT edit_session_result = S_OK; |
| const HRESULT hr = target_context->RequestEditSession( |
| text_service->GetClientID(), |
| edit_session, |
| TF_ES_SYNC | TF_ES_READWRITE, |
| &edit_session_result); |
| if (FAILED(hr)) { |
| return false; |
| } |
| if (FAILED(edit_session_result)) { |
| return false; |
| } |
| return true; |
| } |
| |
| bool TipSurroundingTextUtil::MeasureCharactersBackward( |
| const wstring &text, |
| size_t characters_in_ucs4, |
| size_t *characters_in_utf16) { |
| if (characters_in_utf16 == nullptr) { |
| return false; |
| } |
| *characters_in_utf16 = 0; |
| |
| // Count characters from the end of |text| with taking surrogate pair into |
| // consideration. Finally, we will find that |num_char_in_ucs4| characters |
| // consist of |checked_len_in_utf16| UTF16 elements. |
| size_t checked_len_in_utf16 = 0; |
| size_t num_char_in_ucs4 = 0; |
| while (true) { |
| if (num_char_in_ucs4 >= characters_in_ucs4) { |
| break; |
| } |
| if (checked_len_in_utf16 + 1 > text.size()) { |
| break; |
| } |
| ++checked_len_in_utf16; |
| const size_t index_low = text.size() - checked_len_in_utf16; |
| if (IS_LOW_SURROGATE(text[index_low])) { |
| if (checked_len_in_utf16 + 1 <= text.size()) { |
| const size_t index_high = text.size() - checked_len_in_utf16 - 1; |
| if (IS_HIGH_SURROGATE(text[index_high])) { |
| ++checked_len_in_utf16; |
| } |
| } |
| } |
| ++num_char_in_ucs4; |
| } |
| |
| if (num_char_in_ucs4 != characters_in_ucs4) { |
| return false; |
| } |
| *characters_in_utf16 = checked_len_in_utf16; |
| return true; |
| } |
| |
| } // namespace tsf |
| } // namespace win32 |
| } // namespace mozc |