| // 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_edit_session_impl.h" |
| |
| #include <Windows.h> |
| #define _ATL_NO_AUTOMATIC_NAMESPACE |
| #define _WTL_NO_AUTOMATIC_NAMESPACE |
| #include <atlbase.h> |
| #include <atlcom.h> |
| #include <msctf.h> |
| |
| #include <string> |
| |
| #include "base/util.h" |
| #include "client/client_interface.h" |
| #include "session/commands.pb.h" |
| #include "win32/base/conversion_mode_util.h" |
| #include "win32/base/input_state.h" |
| #include "win32/base/string_util.h" |
| #include "win32/tip/tip_composition_util.h" |
| #include "win32/tip/tip_edit_session.h" |
| #include "win32/tip/tip_input_mode_manager.h" |
| #include "win32/tip/tip_private_context.h" |
| #include "win32/tip/tip_range_util.h" |
| #include "win32/tip/tip_status.h" |
| #include "win32/tip/tip_text_service.h" |
| #include "win32/tip/tip_thread_context.h" |
| #include "win32/tip/tip_ui_handler.h" |
| |
| namespace mozc { |
| namespace win32 { |
| namespace tsf { |
| |
| using ATL::CComBSTR; |
| using ATL::CComPtr; |
| using ATL::CComQIPtr; |
| using ATL::CComVariant; |
| using ::mozc::commands::Output; |
| using ::mozc::commands::Preedit; |
| using ::mozc::commands::Result; |
| using ::mozc::commands::SessionCommand; |
| using ::mozc::commands::Status; |
| typedef ::mozc::commands::CompositionMode CompositionMode; |
| typedef ::mozc::commands::Preedit::Segment Segment; |
| typedef ::mozc::commands::Preedit::Segment::Annotation Annotation; |
| |
| namespace { |
| |
| HRESULT SetReadingProperties(ITfContext *context, |
| ITfRange *range, |
| const string &reading_string_utf8, |
| TfEditCookie write_cookie) { |
| HRESULT result = S_OK; |
| |
| // Get out the reading property |
| CComPtr<ITfProperty> reading_property; |
| result = context->GetProperty(GUID_PROP_READING, &reading_property); |
| if (FAILED(result)) { |
| return result; |
| } |
| |
| const wstring &canonical_reading_string = |
| StringUtil::KeyToReading(reading_string_utf8); |
| CComVariant reading(CComBSTR(canonical_reading_string.c_str())); |
| result = reading_property->SetValue(write_cookie, range, &reading); |
| if (FAILED(result)) { |
| return result; |
| } |
| return result; |
| } |
| |
| HRESULT ClearReadingProperties(ITfContext *context, |
| ITfRange *range, |
| TfEditCookie write_cookie) { |
| HRESULT result = S_OK; |
| |
| // Get out the reading property |
| CComPtr<ITfProperty> reading_property; |
| result = context->GetProperty(GUID_PROP_READING, &reading_property); |
| if (FAILED(result)) { |
| return result; |
| } |
| // Clear existing attributes. |
| result = reading_property->Clear(write_cookie, range); |
| if (FAILED(result)) { |
| return result; |
| } |
| return result; |
| } |
| |
| CComPtr<ITfComposition> CreateComposition(TipTextService *text_service, |
| ITfContext *context, |
| TfEditCookie write_cookie) { |
| CComQIPtr<ITfContextComposition> composition_context = context; |
| if (!composition_context) { |
| return nullptr; |
| } |
| TfActiveSelEnd sel_end = TF_AE_NONE; |
| CComQIPtr<ITfInsertAtSelection> insert_selection = context; |
| if (!insert_selection) { |
| return nullptr; |
| } |
| CComPtr<ITfRange> insertion_pos; |
| if (FAILED(insert_selection->InsertTextAtSelection( |
| write_cookie, TF_IAS_QUERYONLY, nullptr, 0, &insertion_pos))) { |
| return nullptr; |
| } |
| CComPtr<ITfComposition> composition; |
| if (FAILED(composition_context->StartComposition( |
| write_cookie, insertion_pos, |
| text_service->CreateCompositionSink(context), &composition))) { |
| return nullptr; |
| } |
| return composition; |
| } |
| |
| // Note: Committing a text is a tricky part in TSF/CUAS. Basically it should be |
| // done as following steps. |
| // 1. Create a composition (if not exists). |
| // 2. Replace the text stored in the composition range with the text to be |
| // committed. Note that CUAS updates GCS_RESULTCLAUSE and |
| // GCS_RESULTREADCLAUSE by using the segment structure of GUID_PROP_READING |
| // property. For example, CUAS generates two segments for the following |
| // reading text structure. |
| // "今日は(きょうは)/晴天(せいてん)" |
| // 3. Call ITfComposition::ShiftStart to shrink the composition range. Note |
| // that the text that is pushed out from the composition range is |
| // interpreted as the "committed text". |
| // 4. Update the caret position explicitly. Note that some applications |
| // such as WPF's TextBox do not update the caret position automatically |
| // when an composition is commited. |
| // See also b/8406545 and b/9747361. |
| CComPtr<ITfComposition> CommitText(TipTextService *text_service, |
| ITfContext *context, |
| TfEditCookie write_cookie, |
| CComPtr<ITfComposition> composition, |
| const Output &output) { |
| if (!composition) { |
| composition = CreateComposition(text_service, context, write_cookie); |
| } |
| if (!composition) { |
| return nullptr; |
| } |
| |
| HRESULT result = S_OK; |
| |
| CComPtr<ITfRange> composition_range; |
| result = composition->GetRange(&composition_range); |
| if (FAILED(result)) { |
| return nullptr; |
| } |
| |
| wstring result_text; |
| Util::UTF8ToWide(output.result().value(), &result_text); |
| |
| wstring composition_text; |
| TipRangeUtil::GetText(composition_range, write_cookie, &composition_text); |
| |
| // Make sure that |composition_text| begins with |result_text| so that |
| // CUAS can generate an appropriate GCS_RESULTREADCLAUSE information. |
| // See b/8406545 |
| if (composition_text.find(result_text) != 0) { |
| result = composition_range->SetText( |
| write_cookie, 0, result_text.c_str(), result_text.size()); |
| if (FAILED(result)) { |
| return nullptr; |
| } |
| result = SetReadingProperties(context, composition_range, |
| output.result().key(), write_cookie); |
| if (FAILED(result)) { |
| return nullptr; |
| } |
| } |
| |
| CComPtr<ITfRange> new_composition_start; |
| result = composition_range->Clone(&new_composition_start); |
| if (FAILED(result)) { |
| return nullptr; |
| } |
| LONG moved = 0; |
| result = new_composition_start->ShiftStart( |
| write_cookie, result_text.size(), &moved, nullptr); |
| if (FAILED(result)) { |
| return nullptr; |
| } |
| result = new_composition_start->Collapse(write_cookie, TF_ANCHOR_START); |
| if (FAILED(result)) { |
| return nullptr; |
| } |
| result = composition->ShiftStart(write_cookie, new_composition_start); |
| if (FAILED(result)) { |
| return nullptr; |
| } |
| // We need to update the caret position manually for WPF's TextBox, where |
| // caret position is not updated automatically when a composition text is |
| // committed by ITfComposition::ShiftStart. |
| result = TipRangeUtil::SetSelection( |
| context, write_cookie, new_composition_start, TF_AE_END); |
| if (FAILED(result)) { |
| return nullptr; |
| } |
| return composition; |
| } |
| |
| HRESULT UpdateComposition(TipTextService *text_service, |
| ITfContext *context, |
| CComPtr<ITfComposition> composition, |
| TfEditCookie write_cookie, |
| const Output &output) { |
| HRESULT result = S_OK; |
| |
| // Clear composition |
| if (composition) { |
| CComPtr<ITfRange> composition_range; |
| result = composition->GetRange(&composition_range); |
| if (FAILED(result)) { |
| return result; |
| } |
| BOOL is_empty = FALSE; |
| result = composition_range->IsEmpty(write_cookie, &is_empty); |
| if (FAILED(result)) { |
| return result; |
| } |
| if (is_empty != TRUE) { |
| wstring str; |
| TipRangeUtil::GetText(composition_range, write_cookie, &str); |
| result = composition_range->SetText(write_cookie, 0, L"", 0); |
| if (FAILED(result)) { |
| return result; |
| } |
| result = ClearReadingProperties(context, composition_range, write_cookie); |
| if (FAILED(result)) { |
| return result; |
| } |
| } |
| } |
| |
| if (!output.has_preedit()) { |
| if (composition) { |
| result = composition->EndComposition(write_cookie); |
| if (FAILED(result)) { |
| return result; |
| } |
| } |
| return S_OK; |
| } |
| |
| DCHECK(output.has_preedit()); |
| |
| if (!composition) { |
| CComQIPtr<ITfInsertAtSelection> insert_selection = context; |
| if (!insert_selection) { |
| return E_FAIL; |
| } |
| CComPtr<ITfRange> insertion_pos; |
| result = insert_selection->InsertTextAtSelection( |
| write_cookie, TF_IAS_QUERYONLY, nullptr, 0, &insertion_pos); |
| if (FAILED(result)) { |
| return result; |
| } |
| composition = CreateComposition(text_service, context, write_cookie); |
| } |
| if (!composition) { |
| return E_FAIL; |
| } |
| CComPtr<ITfRange> composition_range; |
| result = composition->GetRange(&composition_range); |
| if (FAILED(result)) { |
| return result; |
| } |
| |
| const Preedit &preedit = output.preedit(); |
| const wstring &preedit_text = StringUtil::ComposePreeditText(preedit); |
| result = composition_range->SetText( |
| write_cookie, 0, preedit_text.c_str(), preedit_text.size()); |
| if (FAILED(result)) { |
| return result; |
| } |
| |
| // Get out the display attribute property |
| CComPtr<ITfProperty> display_attribute; |
| result = context->GetProperty(GUID_PROP_ATTRIBUTE, &display_attribute); |
| if (FAILED(result)) { |
| return result; |
| } |
| |
| // Get out the reading property |
| CComPtr<ITfProperty> reading_property; |
| result = context->GetProperty(GUID_PROP_READING, &reading_property); |
| if (FAILED(result)) { |
| return result; |
| } |
| |
| // Set each segment's display attribute |
| int start = 0; |
| int end = 0; |
| for (int i = 0; i < preedit.segment_size(); ++i) { |
| const Preedit::Segment &segment = preedit.segment(i); |
| end = start + Util::WideCharsLen(segment.value()); |
| const Preedit::Segment::Annotation &annotation = |
| segment.annotation(); |
| TfGuidAtom attribute = TF_INVALID_GUIDATOM; |
| if (annotation == Preedit::Segment::UNDERLINE) { |
| attribute = text_service->input_attribute(); |
| } else if (annotation == Preedit::Segment::HIGHLIGHT) { |
| attribute = text_service->converted_attribute(); |
| } else { // mozc::commands::Preedit::Segment::NONE or unknown value |
| continue; |
| } |
| |
| CComPtr<ITfRange> segment_range; |
| result = composition_range->Clone(&segment_range); |
| if (FAILED(result)) { |
| return result; |
| } |
| result = segment_range->Collapse(write_cookie, TF_ANCHOR_START); |
| if (FAILED(result)) { |
| return result; |
| } |
| LONG shift = 0; |
| result = segment_range->ShiftEnd(write_cookie, end, &shift, nullptr); |
| if (FAILED(result)) { |
| return result; |
| } |
| result = segment_range->ShiftStart(write_cookie, start, &shift, nullptr); |
| if (FAILED(result)) { |
| return result; |
| } |
| CComVariant var; |
| // set the value over the range |
| var.vt = VT_I4; |
| var.lVal = attribute; |
| result = display_attribute->SetValue(write_cookie, segment_range, &var); |
| if (segment.has_key()) { |
| const wstring &reading_string = StringUtil::KeyToReading(segment.key()); |
| CComVariant reading(CComBSTR(reading_string.c_str())); |
| result = reading_property->SetValue( |
| write_cookie, segment_range, &reading); |
| } |
| start = end; |
| } |
| |
| // Update cursor. |
| { |
| string preedit_text; |
| for (int i = 0; i < preedit.segment_size(); ++i) { |
| preedit_text += preedit.segment(i).value(); |
| } |
| |
| CComPtr<ITfRange> cursor_range; |
| result = composition_range->Clone(&cursor_range); |
| if (FAILED(result)) { |
| return result; |
| } |
| // |output.preedit().cursor()| is in the unit of UTF-32. We need to convert |
| // it to UTF-16 for TSF. |
| const uint32 cursor_pos_utf16 = Util::WideCharsLen(Util::SubString( |
| preedit_text, 0, preedit.cursor())); |
| |
| result = cursor_range->Collapse(write_cookie, TF_ANCHOR_START); |
| if (FAILED(result)) { |
| return result; |
| } |
| LONG shift = 0; |
| result = cursor_range->ShiftEnd( |
| write_cookie, cursor_pos_utf16, &shift, nullptr); |
| if (FAILED(result)) { |
| return result; |
| } |
| result = cursor_range->ShiftStart( |
| write_cookie, cursor_pos_utf16, &shift, nullptr); |
| if (FAILED(result)) { |
| return result; |
| } |
| result = TipRangeUtil::SetSelection( |
| context, write_cookie, cursor_range, TF_AE_END); |
| } |
| return result; |
| } |
| |
| HRESULT UpdatePrivateContext(TipTextService *text_service, |
| ITfContext *context, |
| TfEditCookie write_cookie, |
| const Output &output) { |
| TipPrivateContext *private_context = text_service->GetPrivateContext(context); |
| if (private_context == nullptr) { |
| return S_FALSE; |
| } |
| private_context->mutable_last_output()->CopyFrom(output); |
| if (!output.has_status()) { |
| return S_FALSE; |
| } |
| |
| const Status &status = output.status(); |
| TipInputModeManager *input_mode_manager = |
| text_service->GetThreadContext()->GetInputModeManager(); |
| const TipInputModeManager::NotifyActionSet action_set = |
| input_mode_manager->OnReceiveCommand(status.activated(), |
| status.comeback_mode(), |
| status.mode()); |
| if ((action_set & TipInputModeManager::kNotifySystemOpenClose) == |
| TipInputModeManager::kNotifySystemOpenClose) { |
| TipStatus::SetIMEOpen(text_service->GetThreadManager(), |
| text_service->GetClientID(), |
| input_mode_manager->GetEffectiveOpenClose()); |
| } |
| |
| if ((action_set & TipInputModeManager::kNotifySystemConversionMode) == |
| TipInputModeManager::kNotifySystemConversionMode) { |
| const CompositionMode mozc_mode = static_cast<CompositionMode>( |
| input_mode_manager->GetEffectiveConversionMode()); |
| uint32 native_mode = 0; |
| if (ConversionModeUtil::ToNativeMode( |
| mozc_mode, |
| private_context->input_behavior().prefer_kana_input, |
| &native_mode)) { |
| TipStatus::SetInputModeConversion(text_service->GetThreadManager(), |
| text_service->GetClientID(), |
| native_mode); |
| } |
| } |
| return S_OK; |
| } |
| |
| HRESULT UpdatePreeditAndComposition(TipTextService *text_service, |
| ITfContext *context, |
| TfEditCookie write_cookie, |
| const Output &output) { |
| CComPtr<ITfComposition> composition = CComQIPtr<ITfComposition>( |
| TipCompositionUtil::GetComposition(context, write_cookie)); |
| |
| // Clear the display attributes first. |
| if (composition) { |
| const HRESULT result = TipCompositionUtil::ClearDisplayAttributes( |
| context, composition, write_cookie); |
| if (FAILED(result)) { |
| return result; |
| } |
| } |
| |
| if (output.has_result()) { |
| CComPtr<ITfComposition> new_composition = CommitText( |
| text_service, context, write_cookie, composition, output); |
| composition = new_composition; |
| if (!new_composition) { |
| return E_FAIL; |
| } |
| } |
| |
| return UpdateComposition( |
| text_service, context, composition, write_cookie, output); |
| } |
| |
| HRESULT DoEditSessionInComposition(TipTextService *text_service, |
| ITfContext *context, |
| TfEditCookie write_cookie, |
| const Output &output) { |
| const HRESULT result = UpdatePrivateContext( |
| text_service, context, write_cookie, output); |
| if (FAILED(result)) { |
| return result; |
| } |
| return UpdatePreeditAndComposition( |
| text_service, context, write_cookie, output); |
| } |
| |
| HRESULT DoEditSessionAfterComposition(TipTextService *text_service, |
| ITfContext *context, |
| TfEditCookie write_cookie, |
| const Output &output) { |
| return UpdatePrivateContext( |
| text_service, context, write_cookie, output); |
| } |
| |
| HRESULT OnEndEditImpl(TipTextService *text_service, |
| ITfContext *context, |
| TfEditCookie write_cookie, |
| ITfEditRecord *edit_record, |
| bool *update_ui) { |
| bool dummy_bool = false; |
| if (update_ui == nullptr) { |
| update_ui = &dummy_bool; |
| } |
| *update_ui = false; |
| |
| HRESULT result = S_OK; |
| |
| { |
| CComPtr<ITfRange> selection_range; |
| TfActiveSelEnd active_sel_end = TF_AE_NONE; |
| result = TipRangeUtil::GetDefaultSelection( |
| context, write_cookie, &selection_range, &active_sel_end); |
| if (FAILED(result)) { |
| return result; |
| } |
| vector<InputScope> input_scopes; |
| result = TipRangeUtil::GetInputScopes( |
| selection_range, write_cookie, &input_scopes); |
| TipInputModeManager *input_mode_manager = |
| text_service->GetThreadContext()->GetInputModeManager(); |
| const auto actions = input_mode_manager->OnChangeInputScope(input_scopes); |
| if (actions == TipInputModeManager::kUpdateUI) { |
| *update_ui = true; |
| } |
| // If the indicator is visible, update UI just in case. |
| if (input_mode_manager->IsIndicatorVisible()) { |
| *update_ui = true; |
| } |
| } |
| |
| CComPtr<ITfCompositionView> composition_view = |
| TipCompositionUtil::GetComposition(context, write_cookie); |
| if (!composition_view) { |
| // If there is no composition, nothing to check. |
| return S_OK; |
| } |
| CComPtr<ITfComposition> composition; |
| result = composition_view.QueryInterface(&composition); |
| if (FAILED(result)) { |
| return result; |
| } |
| |
| if (!composition) { |
| // Nothing to do. |
| return S_OK; |
| } |
| |
| CComPtr<ITfRange> composition_range; |
| result = composition->GetRange(&composition_range); |
| if (FAILED(result)) { |
| return result; |
| } |
| |
| BOOL selection_changed = FALSE; |
| result = edit_record->GetSelectionStatus(&selection_changed); |
| if (FAILED(result)) { |
| return result; |
| } |
| if (selection_changed) { |
| // When the selection is changed, make sure the new selection range is |
| // covered by the composition range. Otherwise, terminate the composition. |
| CComPtr<ITfRange> selected_range; |
| TfActiveSelEnd active_sel_end = TF_AE_NONE; |
| result = TipRangeUtil::GetDefaultSelection( |
| context, write_cookie, &selected_range, &active_sel_end); |
| if (FAILED(result)) { |
| return result; |
| } |
| if (!TipRangeUtil::IsRangeCovered( |
| write_cookie, selected_range, composition_range)) { |
| // We enqueue another edit session to sync the composition state between |
| // the application and Mozc server because we are already in |
| // ITfTextEditSink::OnEndEdit and some operations (e.g., |
| // ITfComposition::EndComposition) result in failure in this edit |
| // session. |
| result = TipEditSession::SubmitAsync(text_service, context); |
| if (FAILED(result)) { |
| return result; |
| } |
| // Cancels further operations. |
| return S_OK; |
| } |
| } |
| |
| BOOL is_empty = FALSE; |
| result = composition_range->IsEmpty(write_cookie, &is_empty); |
| if (FAILED(result)) { |
| return result; |
| } |
| if (is_empty) { |
| // When the composition range is empty, we assume the composition is |
| // canceled by the application or something. Actually CUAS does this when |
| // it receives NI_COMPOSITIONSTR/CPS_CANCEL. You can see this as Excel's |
| // auto-completion. If this happens, send REVERT command to the server to |
| // keep the state consistent. See b/1793331 for details. |
| |
| // We enqueue another edit session to sync the composition state between |
| // the application and Mozc server because we are already in |
| // ITfTextEditSink::OnEndEdit and some operations (e.g., |
| // ITfComposition::EndComposition) result in failure in this edit session. |
| result = TipEditSession::CancelCompositionAsync(text_service, context); |
| *update_ui = false; |
| if (FAILED(result)) { |
| return result; |
| } |
| } |
| return S_OK; |
| } |
| |
| } // namespace |
| |
| HRESULT TipEditSessionImpl::OnEndEdit(TipTextService *text_service, |
| ITfContext *context, |
| TfEditCookie write_cookie, |
| ITfEditRecord *edit_record) { |
| bool update_ui = false; |
| const HRESULT result = OnEndEditImpl( |
| text_service, context, write_cookie, edit_record, &update_ui); |
| if (update_ui) { |
| TipEditSessionImpl::UpdateUI(text_service, context, write_cookie); |
| } |
| return result; |
| } |
| |
| HRESULT TipEditSessionImpl::OnCompositionTerminated( |
| TipTextService *text_service, |
| ITfContext *context, |
| ITfComposition *composition, |
| TfEditCookie write_cookie) { |
| if (text_service == nullptr) { |
| return E_FAIL; |
| } |
| if (context == nullptr) { |
| return E_FAIL; |
| } |
| |
| // Clear the display attributes first. |
| if (composition) { |
| const HRESULT result = TipCompositionUtil::ClearDisplayAttributes( |
| context, composition, write_cookie); |
| if (FAILED(result)) { |
| return result; |
| } |
| } |
| |
| SessionCommand command; |
| command.set_type(SessionCommand::SUBMIT); |
| Output output; |
| TipPrivateContext *private_context = text_service->GetPrivateContext(context); |
| if (private_context == nullptr) { |
| return E_FAIL; |
| } |
| if (!private_context->GetClient()->SendCommand(command, &output)) { |
| return E_FAIL; |
| } |
| const HRESULT result = DoEditSessionAfterComposition( |
| text_service, context, write_cookie, output); |
| UpdateUI(text_service, context, write_cookie); |
| return result; |
| } |
| |
| HRESULT TipEditSessionImpl::UpdateContext( |
| TipTextService *text_service, |
| ITfContext *context, |
| TfEditCookie write_cookie, |
| const commands::Output &output) { |
| const HRESULT result = DoEditSessionInComposition( |
| text_service, context, write_cookie, output); |
| UpdateUI(text_service, context, write_cookie); |
| return result; |
| } |
| |
| void TipEditSessionImpl::UpdateUI(TipTextService *text_service, |
| ITfContext *context, |
| TfEditCookie read_cookie) { |
| TipUiHandler::Update(text_service, context, read_cookie); |
| } |
| |
| } // namespace tsf |
| } // namespace win32 |
| } // namespace mozc |