| // Copyright 2010-2014, 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/ime/ime_core.h" |
| |
| #include <ime.h> |
| #include <windows.h> |
| |
| #include <memory> |
| |
| #include "base/logging.h" |
| #include "base/util.h" |
| #include "client/client_interface.h" |
| #include "config/config_handler.h" |
| #include "session/commands.pb.h" |
| #include "session/output_util.h" |
| #include "win32/base/conversion_mode_util.h" |
| #include "win32/base/deleter.h" |
| #include "win32/base/input_state.h" |
| #include "win32/base/keyboard.h" |
| #include "win32/base/imm_reconvert_string.h" |
| #include "win32/ime/ime_candidate_info.h" |
| #include "win32/ime/ime_composition_string.h" |
| #include "win32/ime/ime_private_context.h" |
| #include "win32/ime/ime_scoped_context.h" |
| #include "win32/ime/ime_ui_context.h" |
| #include "win32/ime/ime_ui_visibility_tracker.h" |
| |
| namespace mozc { |
| namespace win32 { |
| |
| using commands::KeyEvent; |
| using commands::Output; |
| using commands::SessionCommand; |
| using std::unique_ptr; |
| |
| namespace { |
| |
| const size_t kReconvertStringSizeLimit = 1024*64; |
| // An embedded object in the RichEdit will be replaced with this character. |
| // See b/3406434 for details. |
| const wchar_t kObjectReplacementCharacter = L'\uFFFC'; |
| |
| bool GetNextState(HIMC himc, const commands::Output &output, |
| mozc::win32::InputState *next_state) { |
| DCHECK(next_state); |
| |
| UIContext context(himc); |
| bool next_open_status = false; |
| DWORD next_logical_mode = 0; |
| DWORD next_visible_mode = 0; |
| if (!output.has_status()) { |
| // |output| does not have |status|. Preserve the current status. |
| next_open_status = context.GetOpenStatus(); |
| if (!context.GetConversionMode(&next_logical_mode)) { |
| return false; |
| } |
| next_visible_mode = next_logical_mode; |
| } else if (!mozc::win32::ConversionModeUtil::ConvertStatusFromMozcToNative( |
| output.status(), context.IsKanaInputPreferred(), |
| &next_open_status, &next_logical_mode, &next_visible_mode)) { |
| return false; |
| } |
| |
| next_state->open = next_open_status; |
| next_state->logical_conversion_mode = next_logical_mode; |
| next_state->visible_conversion_mode = next_visible_mode; |
| return true; |
| } |
| |
| bool UpdateInputContext( |
| HIMC himc, const commands::Output &output, bool generate_message) { |
| InputState next_state; |
| if (!GetNextState(himc, output, &next_state)) { |
| return false; |
| } |
| if (!generate_message) { |
| if (!ImeCore::UpdateContext(himc, next_state, output, nullptr)) { |
| return false; |
| } |
| return true; |
| } |
| MessageQueue message_queue(himc); |
| if (!ImeCore::UpdateContext(himc, next_state, output, &message_queue)) { |
| return FALSE; |
| } |
| return message_queue.Send(); |
| } |
| |
| HIMCC EnsureHIMCCSize(HIMCC himcc, DWORD size) { |
| if (himcc == nullptr) { |
| return ::ImmCreateIMCC(size); |
| } |
| const DWORD current_size = ::ImmGetIMCCSize(himcc); |
| if (current_size == size) { |
| return himcc; |
| } |
| return ::ImmReSizeIMCC(himcc, size); |
| } |
| |
| bool UpdateCompositionString(HIMC himc, |
| const commands::Output &output, |
| vector<UIMessage> *messages) { |
| ScopedHIMC<InputContext> context(himc); |
| |
| // When the string is inserted from Tablet Input Panel, MSCTF shrinks the |
| // CompositionString buffer that we allocated in ImeSelect(). Thus we need |
| // to resize the buffer when necessary. b/6841008 |
| // TODO(yukawa): Move this logic into more appropriate place. |
| const HIMCC composition_string_handle = EnsureHIMCCSize( |
| context->hCompStr, sizeof(CompositionString)); |
| if (composition_string_handle == nullptr) { |
| return false; |
| } |
| ScopedHIMCC<CompositionString> compstr(composition_string_handle); |
| if (!compstr->Update(output, messages)) { |
| return false; |
| } |
| return true; |
| } |
| |
| bool UpdateCompositionStringAndPushMessages( |
| HIMC himc, |
| const commands::Output &output, |
| MessageQueue *message_queue) { |
| ScopedHIMC<InputContext> context(himc); |
| ScopedHIMCC<PrivateContext> private_context(context->hPrivate); |
| vector<UIMessage> messages; |
| |
| if (!UpdateCompositionString(himc, output, &messages)) { |
| return false; |
| } |
| |
| const bool generate_message = (message_queue != nullptr); |
| if (!generate_message) { |
| return true; |
| } |
| |
| for (vector<UIMessage>::const_iterator it = messages.begin(); |
| it != messages.end(); ++it) { |
| if (UIVisibilityTracker::IsVisibilityTestMessageForComposiwionWindow( |
| it->message(), it->wparam(), it->lparam())) { |
| private_context->ui_visibility_tracker-> |
| BeginVisibilityTestForCompositionWindow(); |
| } |
| message_queue->AddMessage(it->message(), it->wparam(), it->lparam()); |
| } |
| return true; |
| } |
| |
| bool GetReconvertString(const RECONVERTSTRING *reconvert_string, |
| string *total_composition_in_utf8) { |
| DCHECK(total_composition_in_utf8); |
| total_composition_in_utf8->clear(); |
| |
| if (reconvert_string->dwCompStrLen == 0) { |
| // There is no text selection. Reconversion cannot be started. |
| return false; |
| } |
| |
| wstring preceding_composition; |
| wstring target_text; |
| wstring following_composition; |
| if (!ReconvertString::Decompose( |
| reconvert_string, nullptr, &preceding_composition, |
| &target_text, &following_composition, nullptr)) { |
| DLOG(INFO) << "ReconvertString::Decompose failed."; |
| return false; |
| } |
| |
| const wstring total_composition = |
| preceding_composition + target_text + following_composition; |
| |
| // Like other Japanese IMEs (MS-IME, ATOK), Mozc does not support |
| // reconversion when the composition string contains any embedded object |
| // because it is too complicated to restore the original state when the |
| // reconversion is canceled. See b/3406434 for details. |
| if (total_composition.find(kObjectReplacementCharacter) != wstring::npos) { |
| return false; |
| } |
| |
| if ((Util::WideToUTF8(total_composition, total_composition_in_utf8) == 0) || |
| total_composition_in_utf8->empty()) { |
| DLOG(INFO) << "Composition string is empty."; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool QueryDocumentFeed(HIMC himc, |
| wstring *preceding_text, |
| wstring *following_text) { |
| LRESULT result = ::ImmRequestMessageW(himc, IMR_DOCUMENTFEED, 0); |
| if (result == 0) { |
| // IMR_DOCUMENTFEED is not supported. |
| return false; |
| } |
| const size_t buffer_size = static_cast<size_t>(result); |
| if (buffer_size > kReconvertStringSizeLimit) { |
| LOG(ERROR) << "Too large RECONVERTSTRING."; |
| return false; |
| } |
| |
| 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 = ::ImmRequestMessageW(himc, IMR_DOCUMENTFEED, |
| reinterpret_cast<LPARAM>(reconvert_string)); |
| if (result == 0) { |
| DLOG(ERROR) << "RECONVERTSTRING is nullptr."; |
| return false; |
| } |
| |
| return ReconvertString::Decompose( |
| reconvert_string, preceding_text, nullptr, nullptr, nullptr, |
| following_text); |
| } |
| |
| } // namespace |
| |
| void ImeCore::UpdateContextWithSurroundingText(HIMC himc, |
| commands::Context *context) { |
| if (context == nullptr) { |
| return; |
| } |
| context->clear_preceding_text(); |
| context->clear_following_text(); |
| wstring preceding_text; |
| wstring following_text; |
| if (!QueryDocumentFeed(himc, &preceding_text, &following_text)) { |
| return; |
| } |
| Util::WideToUTF8(preceding_text, context->mutable_preceding_text()); |
| Util::WideToUTF8(following_text, context->mutable_following_text()); |
| } |
| |
| KeyEventHandlerResult ImeCore::ImeProcessKey( |
| mozc::client::ClientInterface *client, |
| const VirtualKey &virtual_key, |
| const LParamKeyInfo &lparam, |
| const KeyboardStatus &keyboard_status, |
| const InputBehavior &behavior, |
| const InputState &initial_state, |
| const mozc::commands::Context &context, |
| InputState *next_state, |
| commands::Output *output) { |
| unique_ptr<Win32KeyboardInterface> |
| keyboard(Win32KeyboardInterface::CreateDefault()); |
| return KeyEventHandler::ImeProcessKey( |
| virtual_key, lparam.GetScanCode(), lparam.IsKeyDownInImeProcessKey(), |
| keyboard_status, behavior, initial_state, context, client, keyboard.get(), |
| next_state, output); |
| } |
| |
| KeyEventHandlerResult ImeCore::ImeToAsciiEx( |
| mozc::client::ClientInterface *client, |
| const VirtualKey &virtual_key, |
| BYTE scan_code, |
| bool is_key_down, |
| const KeyboardStatus &keyboard_status, |
| const InputBehavior &behavior, |
| const InputState &initial_state, |
| const commands::Context &context, |
| InputState *next_state, |
| commands::Output *output) { |
| unique_ptr<Win32KeyboardInterface> |
| keyboard(Win32KeyboardInterface::CreateDefault()); |
| return KeyEventHandler::ImeToAsciiEx( |
| virtual_key, scan_code, is_key_down, keyboard_status, behavior, |
| initial_state, context, client, keyboard.get(), next_state, output); |
| } |
| |
| bool ImeCore::OpenIME(mozc::client::ClientInterface *client, DWORD next_mode) { |
| commands::CompositionMode mode = commands::DIRECT; |
| if (!ConversionModeUtil::GetMozcModeFromNativeMode(next_mode, &mode)) { |
| return false; |
| } |
| |
| SessionCommand command; |
| command.set_type(commands::SessionCommand::TURN_ON_IME); |
| command.set_composition_mode(mode); |
| commands::Output output; |
| if (!client->SendCommand(command, &output)) { |
| return false; |
| } |
| if (!output.consumed()) { |
| return false; |
| } |
| return true; |
| } |
| |
| bool ImeCore::CloseIME(mozc::client::ClientInterface *client, |
| DWORD next_mode, commands::Output *output) { |
| commands::CompositionMode mode = commands::DIRECT; |
| if (!ConversionModeUtil::GetMozcModeFromNativeMode(next_mode, &mode)) { |
| return false; |
| } |
| |
| SessionCommand command; |
| command.set_type(commands::SessionCommand::TURN_OFF_IME); |
| command.set_composition_mode(mode); |
| |
| if (!client->SendCommand(command, output)) { |
| return false; |
| } |
| return true; |
| } |
| |
| bool ImeCore::SubmitComposition(HIMC himc, bool generate_message) { |
| UIContext context(himc); |
| mozc::commands::Output output; |
| mozc::commands::SessionCommand command; |
| command.set_type(mozc::commands::SessionCommand::SUBMIT); |
| if (!context.client()->SendCommand(command, &output)) { |
| return false; |
| } |
| return UpdateInputContext(himc, output, generate_message); |
| } |
| |
| bool ImeCore::CancelComposition(HIMC himc, bool generate_message) { |
| UIContext context(himc); |
| mozc::commands::Output output; |
| mozc::commands::SessionCommand command; |
| command.set_type(mozc::commands::SessionCommand::REVERT); |
| if (!context.client()->SendCommand(command, &output)) { |
| return false; |
| } |
| return UpdateInputContext(himc, output, generate_message); |
| } |
| |
| bool ImeCore::SwitchInputMode( |
| HIMC himc, DWORD native_mode, bool generate_message) { |
| UIContext context(himc); |
| |
| const bool open = context.GetOpenStatus(); |
| if (!open) { |
| return true; |
| } |
| mozc::commands::Output output; |
| mozc::commands::SessionCommand command; |
| command.set_type(mozc::commands::SessionCommand::SWITCH_INPUT_MODE); |
| |
| commands::CompositionMode mozc_mode = commands::HIRAGANA; |
| if (!ConversionModeUtil::ToMozcMode(native_mode, &mozc_mode)) { |
| return false; |
| } |
| |
| command.set_composition_mode(mozc_mode); |
| if (!context.client()->SendCommand(command, &output)) { |
| return false; |
| } |
| return UpdateInputContext(himc, output, generate_message); |
| } |
| |
| bool ImeCore::SendCallbackCommand(HIMC himc, bool generate_message) { |
| UIContext context(himc); |
| commands::Output last_output; |
| context.GetLastOutput(&last_output); |
| |
| if (!last_output.has_callback()) { |
| return false; |
| } |
| |
| mozc::commands::Output output; |
| if (!context.client()->SendCommand(last_output.callback().session_command(), |
| &output)) { |
| return false; |
| } |
| return UpdateInputContext(himc, output, generate_message); |
| } |
| |
| DWORD ImeCore::GetSupportableConversionMode(DWORD raw_conversion_mode) { |
| // If the initial |fdwConversion| is not a supported combination of flags, |
| // we have to update it and then send the IMN_SETCONVERSIONMODE message. |
| // See b/2914115 for details. |
| const DWORD kHiragana = IME_CMODE_NATIVE | IME_CMODE_FULLSHAPE; |
| const DWORD kFullKatakana = IME_CMODE_NATIVE | IME_CMODE_FULLSHAPE | |
| IME_CMODE_KATAKANA; |
| const DWORD kHalfKatakana = IME_CMODE_NATIVE | IME_CMODE_KATAKANA; |
| const DWORD kFullAlpha = IME_CMODE_ALPHANUMERIC | IME_CMODE_FULLSHAPE; |
| const DWORD kHalfAlpha = IME_CMODE_ALPHANUMERIC; |
| |
| // Separate Roman flag |
| DWORD roman_flag = (raw_conversion_mode & IME_CMODE_ROMAN); |
| const DWORD original_mode = (raw_conversion_mode & ~IME_CMODE_ROMAN); |
| |
| DWORD next_mode = 0; |
| switch (original_mode) { |
| case kHiragana: |
| case kFullKatakana: |
| case kHalfKatakana: |
| case kFullAlpha: |
| case kHalfAlpha: |
| // OK, this is one of Well-known modes. |
| next_mode = original_mode; |
| break; |
| default: |
| // Unknown combination. |
| // TODO(yukawa): use most similar mode instead of always choosing |
| // Roman-Hiragana |
| next_mode = kHiragana; |
| roman_flag = IME_CMODE_ROMAN; |
| break; |
| } |
| |
| // Restore Roman Flag |
| next_mode |= roman_flag; |
| |
| return next_mode; |
| } |
| |
| DWORD ImeCore::GetSupportableSentenceMode(DWORD raw_sentence_mode) { |
| // If the initial |fdwSentence| is not a supported combination of flags, |
| // we have to update it and then send the IMN_SETSENTENCEMODE message as we |
| // did in b/2914115 for conversion mode. |
| |
| // Always returns IME_SMODE_PHRASEPREDICT. |
| // See b/2913510, b/2954777, and b/2955175 for details. |
| return IME_SMODE_PHRASEPREDICT; |
| } |
| |
| bool ImeCore::IsInputContextInitialized(HIMC himc) { |
| if (himc == nullptr) { |
| return false; |
| } |
| ScopedHIMC<InputContext> context(himc); |
| // For some reason, we fail to lock input context. See b/3088049 for |
| // details. |
| if (context.get() == nullptr) { |
| return false; |
| } |
| if (!PrivateContextUtil::IsValidPrivateContext(context->hPrivate)) { |
| return false; |
| } |
| ScopedHIMCC<PrivateContext> private_context(context->hPrivate); |
| if (private_context->ime_behavior->disabled) { |
| return false; |
| } |
| return true; |
| } |
| |
| void ImeCore::SortIMEMessages( |
| const vector<UIMessage> &composition_messages, |
| const vector<UIMessage> &candidate_messages, |
| bool previous_open_status, |
| DWORD previous_conversion_mode, |
| bool next_open_status, |
| DWORD next_conversion_mode, |
| vector<UIMessage> *sorted_messages) { |
| DCHECK(sorted_messages); |
| sorted_messages->clear(); |
| |
| const bool open_status_changed = |
| (previous_open_status != next_open_status); |
| const bool conversion_mode_changed = |
| (previous_conversion_mode != next_conversion_mode); |
| |
| // Notify IMN_SETOPENSTATUS for IME-ON. |
| if (open_status_changed && next_open_status) { |
| sorted_messages->push_back(UIMessage(WM_IME_NOTIFY, IMN_SETOPENSTATUS, 0)); |
| } |
| |
| // Notify IMN_SETCONVERSIONMODE. |
| if (conversion_mode_changed) { |
| sorted_messages->push_back(UIMessage( |
| WM_IME_NOTIFY, IMN_SETCONVERSIONMODE, 0)); |
| } |
| |
| // Notify IMN_CLOSECANDIDATE. |
| vector<UIMessage> other_candidate_messages; |
| for (vector<UIMessage>::const_iterator it = candidate_messages.begin(); |
| it != candidate_messages.end(); ++it) { |
| const bool is_close_candidate = ((it->message() == WM_IME_NOTIFY) && |
| (it->wparam() == IMN_CLOSECANDIDATE)); |
| if (!is_close_candidate) { |
| other_candidate_messages.push_back(*it); |
| continue; |
| } |
| sorted_messages->push_back(*it); |
| } |
| |
| // Notify all composition UI messages except for WM_IME_ENDCOMPOSITION. |
| // Typically WM_IME_STARTCOMPOSITION / WM_IME_COMPOSITION will be handled. |
| vector<UIMessage> end_composition_messages; |
| for (vector<UIMessage>::const_iterator it = composition_messages.begin(); |
| it != composition_messages.end(); ++it) { |
| if (it->message() == WM_IME_ENDCOMPOSITION) { |
| end_composition_messages.push_back(*it); |
| continue; |
| } |
| sorted_messages->push_back(*it); |
| } |
| |
| // Notify all other candidate UI messages. |
| // Typically IMN_OPENCANDIDATE and IMN_CHANGECANDIDATE will be handled. |
| for (vector<UIMessage>::const_iterator it = other_candidate_messages.begin(); |
| it != other_candidate_messages.end(); ++it) { |
| DCHECK(!((it->message() == WM_IME_NOTIFY) && |
| (it->wparam() == IMN_CLOSECANDIDATE))); |
| sorted_messages->push_back(*it); |
| } |
| |
| // Notify WM_IME_ENDCOMPOSITION |
| for (vector<UIMessage>::const_iterator it = end_composition_messages.begin(); |
| it != end_composition_messages.end(); ++it) { |
| DCHECK_EQ(WM_IME_ENDCOMPOSITION, it->message()); |
| sorted_messages->push_back(*it); |
| } |
| |
| // Notify IMN_SETOPENSTATUS for IME-OFF. |
| if (open_status_changed && !next_open_status) { |
| sorted_messages->push_back(UIMessage(WM_IME_NOTIFY, IMN_SETOPENSTATUS, 0)); |
| } |
| |
| sorted_messages->push_back( |
| UIMessage(WM_IME_NOTIFY, IMN_PRIVATE, kNotifyUpdateUI)); |
| } |
| |
| bool ImeCore::UpdateContext(HIMC himc, |
| const InputState &next_state, |
| const commands::Output &new_output, |
| MessageQueue *message_queue) { |
| if (!IsInputContextInitialized(himc)) { |
| return false; |
| } |
| |
| if (!new_output.has_callback() || |
| !new_output.callback().has_session_command()) { |
| // Callback is not requested. |
| return UpdateContextMain(himc, next_state, new_output, message_queue); |
| } |
| |
| // Delayed callback exists. |
| if (new_output.callback().has_delay_millisec()) { |
| message_queue->AddMessage(WM_IME_NOTIFY, |
| IMN_PRIVATE, |
| kNotifyDelayedCallback); |
| return UpdateContextMain(himc, next_state, new_output, message_queue); |
| } |
| |
| // Immediate callback exists. |
| const SessionCommand &callback_command = |
| new_output.callback().session_command(); |
| |
| // Callback of CONVERT_REVERSE is an exception. |
| // We ignore all other fields in |callback_command| in this case. |
| // The UI handler will invoke reconversion later as other Japanese IMEs do. |
| if (callback_command.has_type() && |
| callback_command.type() == SessionCommand::CONVERT_REVERSE) { |
| if (message_queue != nullptr) { |
| message_queue->AddMessage(WM_IME_NOTIFY, IMN_PRIVATE, |
| kNotifyReconvertFromIME); |
| } |
| return true; |
| } |
| |
| // Otherwise, use |callback_output| and |callback_state| instead of |
| // |new_output| and |next_mode|, respectively. |
| UIContext context(himc); |
| Output callback_output; |
| if (!context.client()->SendCommand(callback_command, &callback_output)) { |
| return false; |
| } |
| InputState callback_state; |
| if (!GetNextState(himc, callback_output, &callback_state)) { |
| return false; |
| } |
| return UpdateContextMain(himc, callback_state, |
| callback_output, message_queue); |
| } |
| |
| bool ImeCore::UpdateContextMain(HIMC himc, |
| const InputState &next_state, |
| const commands::Output &new_output, |
| MessageQueue *message_queue) { |
| DCHECK(IsInputContextInitialized(himc)); |
| const bool generate_message = (message_queue != nullptr); |
| ScopedHIMC<InputContext> context(himc); |
| ScopedHIMCC<PrivateContext> private_context(context->hPrivate); |
| |
| // If the deletion range matches commands::Capability::DELETE_PRECEDING_TEXT, |
| // initialize the deleter. |
| if (generate_message && |
| new_output.has_consumed() && new_output.has_deletion_range() && |
| new_output.deletion_range().has_length() && |
| new_output.deletion_range().has_offset() && |
| (new_output.deletion_range().length() == |
| -new_output.deletion_range().offset())) { |
| // If there remains an ongoing composition, it should be cleared before |
| // VK_BACKs are delivered. (b/3423449) |
| UIContext uicontext(himc); |
| if (!uicontext.IsCompositionStringEmpty()) { |
| commands::Output empty_output; |
| if (!UpdateCompositionStringAndPushMessages( |
| himc, empty_output, message_queue)) { |
| return false; |
| } |
| } |
| |
| // Make sure the pending output does not have |deletion_range|. |
| // Otherwise, an infinite loop will be created. |
| commands::Output output; |
| output.CopyFrom(new_output); |
| output.clear_deletion_range(); |
| private_context->deleter->BeginDeletion( |
| new_output.deletion_range().length(), output, next_state); |
| return true; |
| } |
| |
| if (new_output.has_consumed()) { |
| private_context->last_output->CopyFrom(new_output); |
| } |
| |
| *private_context->ime_state = next_state; |
| const bool previous_open = (context->fOpen != FALSE); |
| const DWORD previous_conversion = context->fdwConversion; |
| const commands::Output &output = |
| *private_context->last_output; |
| |
| // Update context. |
| context->fOpen = next_state.open ? TRUE : FALSE; |
| context->fdwConversion = next_state.logical_conversion_mode; |
| |
| vector<UIMessage> composition_messages; |
| if (!UpdateCompositionString(himc, output, &composition_messages)) { |
| return false; |
| } |
| |
| vector<UIMessage> candidate_messages; |
| context->hCandInfo = |
| mozc::win32::CandidateInfoUtil::Update(context->hCandInfo, |
| output, &candidate_messages); |
| if (context->hCandInfo == nullptr) { |
| return false; |
| } |
| |
| if (generate_message) { |
| // In order to minimize the risk of application compatibility problem, |
| // we might want to send these messages in the the same order to MS-IME. |
| // See b/3488848 for details. |
| vector<UIMessage> sorted_messages; |
| SortIMEMessages(composition_messages, |
| candidate_messages, |
| previous_open, |
| previous_conversion, |
| next_state.open, |
| next_state.logical_conversion_mode, |
| &sorted_messages); |
| |
| // Allow visibility trackers to track if each UI message will be |
| UIVisibilityTracker *ui_visibility_tracker = |
| private_context->ui_visibility_tracker; |
| for (vector<UIMessage>::const_iterator it = sorted_messages.begin(); |
| it != sorted_messages.end(); ++it) { |
| if (UIVisibilityTracker::IsVisibilityTestMessageForCandidateWindow( |
| it->message(), it->wparam(), it->lparam())) { |
| ui_visibility_tracker->BeginVisibilityTestForCandidateWindow(); |
| } |
| if (UIVisibilityTracker::IsVisibilityTestMessageForComposiwionWindow( |
| it->message(), it->wparam(), it->lparam())) { |
| ui_visibility_tracker->BeginVisibilityTestForCompositionWindow(); |
| } |
| message_queue->AddMessage(it->message(), it->wparam(), it->lparam()); |
| } |
| } |
| |
| return true; |
| } |
| |
| BOOL ImeCore::IMEOff(HIMC himc, bool generate_message) { |
| if (!IsInputContextInitialized(himc)) { |
| return FALSE; |
| } |
| |
| mozc::win32::UIContext context(himc); |
| |
| DWORD logical_conversion_mode = 0; |
| if (!context.GetLogicalConversionMode(&logical_conversion_mode)) { |
| return FALSE; |
| } |
| |
| commands::Output output; |
| if (!ImeCore::CloseIME(context.client(), logical_conversion_mode, |
| &output)) { |
| return FALSE; |
| } |
| bool next_open_status = false; |
| DWORD next_logical_mode = 0; |
| DWORD next_visible_mode = 0; |
| if (!output.has_status()) { |
| mozc::win32::UIContext uicontext(himc); |
| if (!uicontext.GetConversionMode(&next_logical_mode)) { |
| return FALSE; |
| } |
| next_visible_mode = next_logical_mode; |
| } else if (!mozc::win32::ConversionModeUtil:: |
| ConvertStatusFromMozcToNative( |
| output.status(), context.IsKanaInputPreferred(), |
| &next_open_status, &next_logical_mode, &next_visible_mode)) { |
| return FALSE; |
| } |
| InputState next_state; |
| next_state.open = false; // We ignore the returned status. |
| next_state.logical_conversion_mode = next_logical_mode; |
| next_state.visible_conversion_mode = next_visible_mode; |
| if (!generate_message) { |
| if (!UpdateContext(himc, next_state, output, nullptr)) { |
| return FALSE; |
| } |
| return TRUE; |
| } |
| |
| MessageQueue message_queue(himc); |
| if (!UpdateContext(himc, next_state, output, &message_queue)) { |
| return FALSE; |
| } |
| return (message_queue.Send() ? TRUE : FALSE); |
| } |
| |
| BOOL ImeCore::HighlightCandidate( |
| HIMC himc, int32 candidate_index, bool generate_message) { |
| if (!IsInputContextInitialized(himc)) { |
| return FALSE; |
| } |
| |
| UIContext context(himc); |
| if (context.IsEmpty()) { |
| return FALSE; |
| } |
| |
| int32 next_candidate_id = 0; |
| { |
| commands::Output last_output; |
| if (!context.GetLastOutput(&last_output)) { |
| return FALSE; |
| } |
| |
| if (!OutputUtil::GetCandidateIdByIndex( |
| last_output, candidate_index, &next_candidate_id)) { |
| return FALSE; |
| } |
| |
| // Stop sending HIGHLIGHT_CANDIDATE if the given candidate is already |
| // selected. If the |last_output| does not have focused candidate, |
| // HIGHLIGHT_CANDIDATE is always be sent. |
| int32 focused_candidate_id = 0; |
| if (OutputUtil::GetFocusedCandidateId( |
| last_output, &focused_candidate_id) && |
| (next_candidate_id == focused_candidate_id)) { |
| // Already highlighted. |
| return TRUE; |
| } |
| } |
| |
| commands::Output output; |
| // TODO(yukawa, komatsu): Make a function in client dir. |
| { |
| mozc::commands::SessionCommand command; |
| command.set_type(mozc::commands::SessionCommand::HIGHLIGHT_CANDIDATE); |
| command.set_id(next_candidate_id); |
| if (!context.client()->SendCommand(command, &output)) { |
| return FALSE; |
| } |
| } |
| |
| return UpdateInputContext(himc, output, generate_message); |
| } |
| |
| BOOL ImeCore::CloseCandidate(HIMC himc, bool generate_message) { |
| if (!IsInputContextInitialized(himc)) { |
| return FALSE; |
| } |
| |
| UIContext context(himc); |
| if (context.IsEmpty()) { |
| return FALSE; |
| } |
| |
| int32 focused_candidate_id = 0; |
| { |
| commands::Output last_output; |
| if (!context.GetLastOutput(&last_output)) { |
| return FALSE; |
| } |
| |
| if (!last_output.has_all_candidate_words()) { |
| // already closed. |
| return TRUE; |
| } |
| |
| // Although we should not handle CloseCandidate when a suggest window is |
| // displayed, currently we need this path to support mouse clicking for |
| // suggest window. |
| |
| if (!OutputUtil::GetFocusedCandidateId( |
| last_output, &focused_candidate_id)) { |
| return FALSE; |
| } |
| } |
| |
| commands::Output output; |
| // TODO(yukawa, komatsu): Make a function in client dir. |
| { |
| mozc::commands::SessionCommand command; |
| command.set_type(mozc::commands::SessionCommand::SELECT_CANDIDATE); |
| command.set_id(focused_candidate_id); |
| if (!context.client()->SendCommand(command, &output)) { |
| return FALSE; |
| } |
| } |
| |
| return UpdateInputContext(himc, output, generate_message); |
| } |
| |
| bool ImeCore::IsActiveContext(HIMC himc) { |
| bool is_active = false; |
| const HWND focus_window = ::GetFocus(); |
| if (focus_window != nullptr && |
| ::IsWindow(focus_window) != FALSE) { |
| const HIMC active_himc = ::ImmGetContext(focus_window); |
| is_active = (himc == active_himc); |
| ::ImmReleaseContext(focus_window, active_himc); |
| } |
| return is_active; |
| } |
| |
| bool ImeCore::TurnOnIMEAndTryToReconvertFromIME(HIMC himc) { |
| UIContext context(himc); |
| if (context.IsEmpty()) { |
| return false; |
| } |
| if (context.input_context() == nullptr) { |
| return false; |
| } |
| if (!context.IsCompositionStringEmpty()) { |
| // TODO(yukawa): Use Mozc server to determine the behavior when any |
| // appropriate protocol becomes available. |
| DLOG(INFO) << "Ongoing composition exists."; |
| return false; |
| } |
| |
| const string &text_utf8 = GetTextForReconversionFromIME(himc); |
| if (text_utf8.empty()) { |
| if (context.GetOpenStatus()) { |
| return true; |
| } |
| // Currently Mozc server will not turn on IME when |text_utf8| is empty but |
| // people expect IME will be turned on even when the reconversion does |
| // nothing. b/4225148. |
| return (::ImmSetOpenStatus(himc, TRUE) != FALSE); |
| } |
| |
| commands::Output output; |
| { |
| mozc::commands::SessionCommand command; |
| command.set_type(mozc::commands::SessionCommand::CONVERT_REVERSE); |
| command.set_text(text_utf8); |
| if (!context.client()->SendCommand(command, &output)) { |
| LOG(ERROR) << "SendCommand failed."; |
| return false; |
| } |
| } |
| |
| return UpdateInputContext(himc, output, true); |
| } |
| |
| string ImeCore::GetTextForReconversionFromIME(HIMC himc) { |
| // Implementation Note: |
| // In order to implement IMM32 reconversion, IME is responsible to update |
| // the following fields in RECONVERTSTRING. |
| // - dwCompStrLen |
| // - dwCompStrOffset |
| // - dwTargetStrOffset |
| // - dwTargetStrLen |
| // However, current Mozc server supports only "pre-segmented" reconversion. |
| // So the IME module assumes that the entire range pointed by |
| // |RECONVERTSTRING::dwTargetStrOffset| and |RECONVERTSTRING::dwTargetStrLen| |
| // is to be reconverted. Technically most of the following processes should |
| // be done at the server-side. |
| LRESULT result = ::ImmRequestMessageW(himc, IMR_RECONVERTSTRING, 0); |
| if (result == 0) { |
| DLOG(INFO) << "IMR_RECONVERTSTRING is not supported."; |
| return ""; |
| } |
| |
| const size_t buffer_size = static_cast<size_t>(result); |
| if (buffer_size > kReconvertStringSizeLimit) { |
| LOG(ERROR) << "Too large RECONVERTSTRING."; |
| return ""; |
| } |
| |
| 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 = ::ImmRequestMessageW(himc, IMR_RECONVERTSTRING, |
| reinterpret_cast<LPARAM>(reconvert_string)); |
| if (result == 0) { |
| DLOG(ERROR) << "RECONVERTSTRING is nullptr."; |
| return ""; |
| } |
| |
| unique_ptr<BYTE[]> copied_buffer(new BYTE[buffer_size]); |
| for (size_t i = 0; i < buffer_size; ++i) { |
| copied_buffer[i] = buffer[i]; |
| } |
| RECONVERTSTRING *expanded_reconvert_string = |
| reinterpret_cast<RECONVERTSTRING *>(copied_buffer.get()); |
| |
| // Expand the composition range if necessary. |
| if (!ReconvertString::EnsureCompositionIsNotEmpty( |
| expanded_reconvert_string)) { |
| return ""; |
| } |
| result = ::ImmRequestMessageW( |
| himc, IMR_CONFIRMRECONVERTSTRING, |
| reinterpret_cast<LPARAM>(expanded_reconvert_string)); |
| if (result != FALSE) { |
| // The application accepted |expanded_reconvert_string|. |
| reconvert_string = expanded_reconvert_string; |
| } |
| |
| string total_composition_utf8; |
| if (!GetReconvertString(reconvert_string, &total_composition_utf8)) { |
| return ""; |
| } |
| |
| return total_composition_utf8; |
| } |
| |
| bool ImeCore::QueryReconversionFromApplication( |
| HIMC himc, RECONVERTSTRING *composition_info, |
| RECONVERTSTRING *reading_info) { |
| // Currently, we are ignoring |reading_info|. |
| // TODO(yukawa): Support |reading_info|. |
| |
| if (!ReconvertString::EnsureCompositionIsNotEmpty(composition_info)) { |
| return false; |
| } |
| |
| string total_composition_utf8; |
| if (!GetReconvertString(composition_info, &total_composition_utf8)) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool ImeCore::ReconversionFromApplication( |
| HIMC himc, const RECONVERTSTRING *composition_info, |
| const RECONVERTSTRING *reading_info) { |
| // Currently, we are ignoring |reading_info|. |
| // TODO(yukawa): Support |reading_info|. |
| |
| UIContext context(himc); |
| if (context.IsEmpty()) { |
| return false; |
| } |
| if (context.input_context() == nullptr) { |
| return false; |
| } |
| |
| if (!context.IsCompositionStringEmpty()) { |
| // TODO(yukawa): Use Mozc server to determine the behavior when any |
| // appropriate protocol becomes available. |
| DLOG(INFO) << "Ongoing composition exists."; |
| return false; |
| } |
| |
| if (composition_info->dwCompStrLen == 0) { |
| // There is no text selection. Reconversion cannot be started. |
| return false; |
| } |
| |
| string total_composition_utf8; |
| if (!GetReconvertString(composition_info, &total_composition_utf8)) { |
| return false; |
| } |
| |
| commands::Output output; |
| mozc::commands::SessionCommand command; |
| command.set_type(mozc::commands::SessionCommand::CONVERT_REVERSE); |
| command.set_text(total_composition_utf8); |
| if (!context.client()->SendCommand(command, &output)) { |
| LOG(ERROR) << "SendCommand failed."; |
| return false; |
| } |
| |
| return UpdateInputContext(himc, output, true); |
| } |
| |
| } // namespace win32 |
| } // namespace mozc |