blob: 9981fcad042ecfc22db1f42d90015bd708900c55 [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_ui_handler_conventional.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 <CommCtrl.h> // for CCSIZEOF_STRUCT
#include "base/logging.h"
#include "base/util.h"
#include "renderer/renderer_command.pb.h"
#include "renderer/win32/win32_renderer_client.h"
#include "win32/base/conversion_mode_util.h"
#include "win32/base/indicator_visibility_tracker.h"
#include "win32/base/input_state.h"
#include "win32/base/migration_util.h"
#include "win32/tip/tip_composition_util.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_ref_count.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_element_conventional.h"
#include "win32/tip/tip_ui_element_manager.h"
namespace mozc {
namespace win32 {
namespace tsf {
namespace {
using ATL::CComPtr;
using ATL::CComQIPtr;
using ::mozc::commands::CompositionMode;
using ::mozc::commands::Preedit;
using ::mozc::renderer::win32::Win32RendererClient;
typedef ::mozc::commands::Preedit_Segment Segment;
typedef ::mozc::commands::Preedit_Segment::Annotation Annotation;
typedef ::mozc::commands::RendererCommand_IndicatorInfo IndicatorInfo;
typedef ::mozc::commands::RendererCommand RendererCommand;
typedef ::mozc::commands::RendererCommand::ApplicationInfo ApplicationInfo;
const size_t kSizeOfGUIThreadInfoV1 = CCSIZEOF_STRUCT(GUITHREADINFO, rcCaret);
size_t GetTargetPos(const commands::Output &output) {
if (!output.has_candidates() || !output.candidates().has_category()) {
return 0;
}
switch (output.candidates().category()) {
case commands::PREDICTION:
case commands::SUGGESTION:
return 0;
case commands::CONVERSION: {
const Preedit &preedit = output.preedit();
size_t offset = 0;
for (int i = 0; i < preedit.segment_size(); ++i) {
const Segment &segment = preedit.segment(i);
const Annotation &annotation = segment.annotation();
if (annotation == Segment::HIGHLIGHT) {
return offset;
}
offset += Util::WideCharsLen(segment.value());
}
return offset;
}
default:
return 0;
}
}
bool FillVisibility(ITfUIElementMgr *ui_element_manager,
TipPrivateContext *private_context,
RendererCommand *command) {
command->set_visible(false);
if (private_context == nullptr) {
return false;
}
const bool show_suggest_window =
private_context->GetUiElementManager()->IsVisible(
ui_element_manager, TipUiElementManager::kSuggestWindow);
const bool show_candidate_window =
private_context->GetUiElementManager()->IsVisible(
ui_element_manager, TipUiElementManager::kCandidateWindow);
const commands::Output &output = private_context->last_output();
bool suggest_window_visible = false;
bool candidate_window_visible = false;
// Check if suggest window and candidate window are actually visible.
if (output.has_candidates() && output.candidates().has_category()) {
switch (output.candidates().category()) {
case commands::SUGGESTION:
suggest_window_visible = show_suggest_window;
break;
case commands::CONVERSION:
case commands::PREDICTION:
candidate_window_visible = show_candidate_window;
break;
default:
// do nothing.
break;
}
}
if (candidate_window_visible || suggest_window_visible) {
command->set_visible(true);
}
ApplicationInfo *app_info = command->mutable_application_info();
int visibility = ApplicationInfo::ShowUIDefault;
if (show_candidate_window) {
// Note that |ApplicationInfo::ShowCandidateWindow| represents that the
// application does not mind the IME showing its own candidate window.
// This bit does not mean that |command| requires the suggest window.
visibility |= ApplicationInfo::ShowCandidateWindow;
}
if (show_suggest_window) {
// Note that |ApplicationInfo::ShowCandidateWindow| represents that the
// application does not mind the IME showing its own candidate window.
// This bit does not mean that |command| requires the suggest window.
visibility |= ApplicationInfo::ShowSuggestWindow;
}
app_info->set_ui_visibilities(visibility);
return true;
}
bool FillCaretInfo(ApplicationInfo *app_info) {
GUITHREADINFO thread_info = {};
thread_info.cbSize = kSizeOfGUIThreadInfoV1;
if (::GetGUIThreadInfo(::GetCurrentThreadId(), &thread_info) == FALSE) {
return false;
}
RendererCommand::CaretInfo *caret = app_info->mutable_caret_info();
caret->set_blinking((thread_info.flags & GUI_CARETBLINKING) ==
GUI_CARETBLINKING);
// Set |caret_rect|
const RECT caret_rect = thread_info.rcCaret;
RendererCommand::Rectangle *rect = caret->mutable_caret_rect();
rect->set_left(thread_info.rcCaret.left);
rect->set_top(thread_info.rcCaret.top);
rect->set_right(thread_info.rcCaret.right);
rect->set_bottom(thread_info.rcCaret.bottom);
caret->set_target_window_handle(reinterpret_cast<uint32>(
thread_info.hwndCaret));
return true;
}
bool FillWindowHandle(ITfContext *context, ApplicationInfo *app_info) {
CComPtr<ITfContextView> context_view;
if (FAILED(context->GetActiveView(&context_view)) || !context_view) {
return false;
}
HWND window_handle = nullptr;
if (FAILED(context_view->GetWnd(&window_handle))) {
return false;
}
app_info->set_target_window_handle(reinterpret_cast<uint32>(window_handle));
return true;
}
CComPtr<ITfRange> GetCompositionRange(ITfContext *context,
TfEditCookie read_cookie) {
CComPtr<ITfCompositionView> composition_view =
TipCompositionUtil::GetComposition(context, read_cookie);
if (!composition_view) {
return nullptr;
}
CComPtr<ITfRange> composition_range;
if (FAILED(composition_view->GetRange(&composition_range))) {
return nullptr;
}
return composition_range;
}
CComPtr<ITfRange> GetSelectionRange(ITfContext *context,
TfEditCookie read_cookie) {
CComPtr<ITfRange> selection_range;
TfActiveSelEnd sel_end = TF_AE_NONE;
if (FAILED(TipRangeUtil::GetDefaultSelection(
context, read_cookie, &selection_range, &sel_end))) {
return nullptr;
}
return selection_range;
}
// This function updates RendererCommand::CharacterPosition to emulate
// IMM32-based client. Ideally we'd better to define new field for TSF Mozc
// into which the result of ITfContextView::GetTextExt is stored.
// TODO(yukawa): Replace FillCharPosition with new one designed for TSF.
bool FillCharPosition(TipPrivateContext *private_context,
ITfContext *context,
TfEditCookie read_cookie,
bool has_composition,
ApplicationInfo *app_info,
bool *no_layout) {
if (private_context == nullptr) {
return false;
}
bool dummy_no_layout = false;
if (no_layout == nullptr) {
no_layout = &dummy_no_layout;
}
*no_layout = false;
if (!app_info->has_target_window_handle()) {
return false;
}
const HWND window_handle =
reinterpret_cast<HWND>(app_info->target_window_handle());
CComPtr<ITfRange> range =
has_composition ? GetCompositionRange(context, read_cookie)
: GetSelectionRange(context, read_cookie);
if (!range) {
return false;
}
CComPtr<ITfRange> target_range;
if (FAILED(range->Clone(&target_range))) {
return false;
}
if (!target_range) {
return false;
}
const commands::Output &output = private_context->last_output();
LONG shifted = 0;
if (FAILED(target_range->Collapse(read_cookie, TF_ANCHOR_START))) {
return false;
}
const size_t target_pos = GetTargetPos(output);
if (FAILED(target_range->ShiftStart(
read_cookie, target_pos, &shifted, nullptr))) {
return false;
}
if (FAILED(target_range->ShiftEnd(
read_cookie, target_pos + 1, &shifted, nullptr))) {
return false;
}
CComPtr<ITfContextView> context_view;
if (FAILED(context->GetActiveView(&context_view)) || !context_view) {
return false;
}
RECT document_rect = {};
if (FAILED(context_view->GetScreenExt(&document_rect))) {
return false;
}
RECT text_rect = {};
bool clipped = false;
const HRESULT hr = TipRangeUtil::GetTextExt(
context_view, read_cookie, target_range, &text_rect, &clipped);
if (hr == TF_E_NOLAYOUT) {
// This is not a critical error but the layout information is not available.
*no_layout = true;
return true;
}
if (FAILED(hr)) {
// Any other errors are unexpected.
return false;
}
RendererCommand::Point *top_left=
app_info->mutable_composition_target()->mutable_top_left();
top_left->set_x(text_rect.left);
top_left->set_y(text_rect.top);
app_info->mutable_composition_target()->set_position(0);
app_info->mutable_composition_target()->set_line_height(
text_rect.bottom - text_rect.top);
RendererCommand::Rectangle *area=
app_info->mutable_composition_target()->mutable_document_area();
area->set_left(document_rect.left);
area->set_top(document_rect.top);
area->set_right(document_rect.right);
area->set_bottom(document_rect.bottom);
return true;
}
void UpdateCommand(TipTextService *text_service,
ITfContext *context,
TfEditCookie read_cookie,
RendererCommand *command,
bool *no_layout) {
command->Clear();
command->set_type(RendererCommand::UPDATE);
TipPrivateContext *private_context = text_service->GetPrivateContext(context);
if (private_context != nullptr) {
command->mutable_output()->CopyFrom(private_context->last_output());
private_context->GetUiElementManager()->OnUpdate(text_service, context);
}
ApplicationInfo *app_info = command->mutable_application_info();
app_info->set_input_framework(ApplicationInfo::TSF);
app_info->set_process_id(::GetCurrentProcessId());
app_info->set_thread_id(::GetCurrentThreadId());
app_info->set_receiver_handle(
reinterpret_cast<int32>(text_service->renderer_callback_window_handle()));
CComQIPtr<ITfUIElementMgr> ui_element_manager(
text_service->GetThreadManager());
FillVisibility(ui_element_manager, private_context, command);
FillWindowHandle(context, app_info);
FillCaretInfo(app_info);
FillCharPosition(private_context, context, read_cookie,
command->output().has_preedit(), app_info, no_layout);
if (private_context != nullptr) {
const TipInputModeManager *input_mode_manager =
text_service->GetThreadContext()->GetInputModeManager();
if (private_context->input_behavior().use_mode_indicator &&
input_mode_manager->IsIndicatorVisible()) {
command->set_visible(true);
IndicatorInfo *info = app_info->mutable_indicator_info();
info->mutable_status()->set_activated(
input_mode_manager->GetEffectiveOpenClose());
info->mutable_status()->set_mode(static_cast<CompositionMode>(
input_mode_manager->GetEffectiveConversionMode()));
}
}
// Regardless of the value of |command->visible()| here, we should hide
// all the UI elements whenever the current threads is not focused.
BOOL thread_focus = FALSE;
const HRESULT hr =
text_service->GetThreadManager()->IsThreadFocus(&thread_focus);
if (SUCCEEDED(hr) && (thread_focus == FALSE)) {
command->set_visible(false);
}
}
// This class is an implementation class for the ITfEditSession classes, which
// is an observer for exclusively read the date from the text store.
class UpdateUiEditSessionImpl : public ITfEditSession {
public:
~UpdateUiEditSessionImpl() {}
// 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 ULONG STDMETHODCALLTYPE AddRef() {
return ref_count_.AddRefImpl();
}
virtual ULONG STDMETHODCALLTYPE Release() {
const ULONG count = ref_count_.ReleaseImpl();
if (count == 0) {
delete this;
}
return count;
}
// The ITfEditSession interface method.
// This function is called back by the TSF thread manager when an edit
// request is granted.
virtual HRESULT STDMETHODCALLTYPE DoEditSession(TfEditCookie edit_cookie) {
RendererCommand command;
bool no_layout = false;
UpdateCommand(text_service_, context_, edit_cookie, &command, &no_layout);
if (!no_layout || !command.visible()) {
Win32RendererClient::OnUpdated(command);
}
return S_OK;
}
static bool BeginRequest(TipTextService *text_service, ITfContext *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<ITfEditSession> edit_session(new UpdateUiEditSessionImpl(
text_service, context));
HRESULT edit_session_result = S_OK;
const HRESULT result = context->RequestEditSession(
text_service->GetClientID(),
edit_session,
TF_ES_ASYNCDONTCARE | TF_ES_READ, &edit_session_result);
return SUCCEEDED(result);
}
private:
UpdateUiEditSessionImpl(TipTextService *text_service,
ITfContext *context)
: text_service_(text_service),
context_(context) {
}
TipRefCount ref_count_;
CComPtr<TipTextService> text_service_;
CComPtr<ITfContext> context_;
DISALLOW_COPY_AND_ASSIGN(UpdateUiEditSessionImpl);
};
HRESULT OnUpdateLanguageBar(TipTextService *text_service,
ITfDocumentMgr *document_manager) {
HRESULT result = S_OK;
ITfThreadMgr *thread_manager = text_service->GetThreadManager();
if (thread_manager == nullptr) {
return E_FAIL;
}
bool disabled = false;
{
if (document_manager == nullptr) {
// When |document_manager| is null, we should disable an IME like we
// disable it when ImmAssociateContext(window_handle, nullptr) is
// called.
disabled = true;
} else {
CComPtr<ITfContext> context;
result = document_manager->GetTop(&context);
if (SUCCEEDED(result)) {
disabled = TipStatus::IsDisabledContext(context);
}
}
}
const TipInputModeManager *input_mode_manager =
text_service->GetThreadContext()->GetInputModeManager();
const bool open = input_mode_manager->GetEffectiveOpenClose();
const CompositionMode mozc_mode =
open ? static_cast<CompositionMode>(
input_mode_manager->GetEffectiveConversionMode())
: commands::DIRECT;
text_service->UpdateLangbar(!disabled, static_cast<uint32>(mozc_mode));
return S_OK;
}
} // namespace
ITfUIElement *TipUiHandlerConventional::CreateUI(TipUiHandler::UiType type,
TipTextService *text_service,
ITfContext *context) {
switch (type) {
case TipUiHandler::kSuggestWindow:
return TipUiElementConventional::New(
TipUiElementConventional::kUnobservableSuggestWindow, text_service,
context);
case TipUiHandler::kCandidateWindow:
return TipUiElementConventional::New(
TipUiElementConventional::kCandidateWindow, text_service, context);
case TipUiHandler::kIndicatorWindow:
return TipUiElementConventional::New(
TipUiElementConventional::KIndicatorWindow, text_service, context);
default:
return nullptr;
}
}
void TipUiHandlerConventional::OnDestroyElement(ITfUIElement *element) {
// TipUiHandlerConventional does not have any hidden resource that is
// associated with |element|. So we have nothing to do here.
// Note that |element| will be destroyed by using ref count.
}
void TipUiHandlerConventional::OnActivate(TipTextService *text_service) {
static bool migrate_checked = false;
if (!migrate_checked) {
migrate_checked = true;
MigrationUtil::DisableLegacyMozcForCurrentUserOnWin8();
}
ITfThreadMgr *thread_mgr = text_service->GetThreadManager();
CComPtr<ITfDocumentMgr> document;
if (FAILED(thread_mgr->GetFocus(&document))) {
return;
}
OnFocusChange(text_service, document);
}
void TipUiHandlerConventional::OnDeactivate() {
Win32RendererClient::OnUIThreadUninitialized();
}
void TipUiHandlerConventional::OnFocusChange(
TipTextService *text_service, ITfDocumentMgr *focused_document_manager) {
if (!focused_document_manager) {
// Empty document. Hide the renderer.
RendererCommand command;
command.set_type(RendererCommand::UPDATE);
command.set_visible(false);
Win32RendererClient::OnUpdated(command);
// Update the langbar.
OnUpdateLanguageBar(text_service, focused_document_manager);
return;
}
CComPtr<ITfContext> context;
if (FAILED(focused_document_manager->GetBase(&context))) {
return;
}
if (!context) {
return;
}
OnUpdateLanguageBar(text_service, focused_document_manager);
UpdateUiEditSessionImpl::BeginRequest(text_service, context);
}
bool TipUiHandlerConventional::Update(TipTextService *text_service,
ITfContext *context,
TfEditCookie read_cookie) {
RendererCommand command;
const TipInputModeManager *input_mode_manager =
text_service->GetThreadContext()->GetInputModeManager();
const bool open = input_mode_manager->GetEffectiveOpenClose();
const CompositionMode mozc_mode = static_cast<CompositionMode>(
input_mode_manager->GetEffectiveConversionMode());
bool no_layout = false;
UpdateCommand(text_service, context, read_cookie, &command, &no_layout);
if (!no_layout || !command.visible()) {
Win32RendererClient::OnUpdated(command);
}
if (open) {
text_service->UpdateLangbar(true, mozc_mode);
} else {
text_service->UpdateLangbar(true, commands::DIRECT);
}
return true;
}
bool TipUiHandlerConventional::OnDllProcessAttach(HINSTANCE module_handle,
bool static_loading) {
Win32RendererClient::OnModuleLoaded(module_handle);
return true;
}
void TipUiHandlerConventional::OnDllProcessDetach(HINSTANCE module_handle,
bool process_shutdown) {
Win32RendererClient::OnModuleUnloaded();
}
} // namespace tsf
} // namespace win32
} // namespace mozc