blob: c5d8ef6f31029924d8e6c0adc20c07e96cc9af18 [file] [log] [blame]
// 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