blob: dd8ff2b8c9abefad0d55c3a13d0b58e8cf96f31f [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 "renderer/win32/win32_renderer_util.h"
#define _ATL_NO_AUTOMATIC_NAMESPACE
#define _WTL_NO_AUTOMATIC_NAMESPACE
// Workaround against KB813540
#include <atlbase_mozc.h>
#include <atlapp.h>
#include <atlgdi.h>
#include <atlmisc.h>
// undef min macro, which conflicts with std::numeric_limits<int>::min().
#if defined(min)
#undef min
#endif // min
// undef max macro, which conflicts with std::numeric_limits<int>::max().
#if defined(max)
#undef max
#endif // max
#include <limits>
#include <memory>
#include <string>
#include <vector>
#include "base/logging.h"
#include "base/util.h"
#include "base/win_util.h"
#include "renderer/renderer_command.pb.h"
#include "renderer/win32/win32_font_util.h"
namespace mozc {
namespace renderer {
namespace win32 {
using ATL::CWindow;
using WTL::CDC;
using WTL::CDCHandle;
using WTL::CSize;
using WTL::CRect;
using WTL::CPoint;
using WTL::CFont;
using WTL::CFontHandle;
using WTL::CLogFont;
using std::unique_ptr;
typedef mozc::commands::RendererCommand::CompositionForm CompositionForm;
typedef mozc::commands::RendererCommand::CandidateForm CandidateForm;
namespace {
// This template class is used to represent rendering information which may
// or may not be available depends on the application and operations.
template <typename T>
class Optional {
public:
Optional()
: value_(T()),
has_value_(false) {}
explicit Optional(const T &src)
: value_(src),
has_value_(true) {}
const T &value() const {
DCHECK(has_value_);
return value_;
}
T *mutable_value() {
has_value_ = true;
return &value_;
}
bool has_value() const {
return has_value_;
}
void Clear() {
has_value_ = false;
}
private:
T value_;
bool has_value_;
};
// A set of rendering information relevant to the target application. Note
// that all of the positional fields are (logical) screen coordinates.
struct CandidateWindowLayoutParams {
Optional<HWND> window_handle;
Optional<IMECHARPOSITION> char_pos;
Optional<CPoint> composition_form_topleft;
Optional<CandidateWindowLayout> candidate_form;
Optional<CRect> caret_rect;
Optional<CLogFont> composition_font;
Optional<CLogFont> default_gui_font;
Optional<CRect> client_rect;
Optional<bool> vertical_writing;
};
bool IsValidRect(const commands::RendererCommand::Rectangle &rect) {
return rect.has_left() && rect.has_top() &&
rect.has_right() && rect.has_bottom();
}
CRect ToRect(const commands::RendererCommand::Rectangle &rect) {
DCHECK(IsValidRect(rect));
return CRect(rect.left(), rect.top(), rect.right(), rect.bottom());
}
bool IsValidPoint(const commands::RendererCommand::Point &point) {
return point.has_x() && point.has_y();
}
CPoint ToPoint(const commands::RendererCommand::Point &point) {
DCHECK(IsValidPoint(point));
return CPoint(point.x(), point.y());
}
// Returns an absolute font height of the composition font.
// Note that this function does not take the DPI virtualization into
// consideration so the caller is responsible to multiply an appropriate
// scaling factor.
// If a composition font is not available, this function try to use
// the default GUI font on this session.
int GetAbsoluteFontHeight(const CandidateWindowLayoutParams &params) {
// A negative font height is also valid in the LONGFONT structure.
// Use |abs| for normalization.
// http://msdn.microsoft.com/en-us/library/dd145037.aspx
if (params.composition_font.has_value()) {
return abs(params.composition_font.value().lfHeight);
}
if (params.default_gui_font.has_value()) {
return abs(params.default_gui_font.value().lfHeight);
}
return 0;
}
// "base_pos" is an ideal position where the candidate window is placed.
// Basically the ideal position depends on historical reason for each
// country and language.
// As for Japanese IME, the bottom-left (for horizontal writing) and
// top-left (for vertical writing) corner of the target segment have been
// used for many years.
CPoint GetBasePositionFromExcludeRect(const CRect &exclude_rect,
bool is_vertical) {
if (is_vertical) {
// Vertical
return exclude_rect.TopLeft();
}
// Horizontal
return CPoint(exclude_rect.left, exclude_rect.bottom);
}
// Returns false if given |form| should be ignored for some compatibility
// reason. Otherwise, returns true.
bool IsCompatibleCompositionForm(const CompositionForm &form,
int compatibility_mode) {
// If IGNORE_DEFAULT_COMPOSITION_FORM flag is specified and all the fields
// in the CompositionForm is 0, returns false.
if ((compatibility_mode & IGNORE_DEFAULT_COMPOSITION_FORM) !=
IGNORE_DEFAULT_COMPOSITION_FORM) {
return true;
}
// TODO(yukawa): Stop supporting |deprecated_style()|.
const uint32 style_bits =
(form.has_deprecated_style() ? form.deprecated_style()
: form.style_bits());
// Note that CompositionForm::DEFAULT is defined as 0.
if (style_bits != CompositionForm::DEFAULT) {
return true;
}
if (!IsValidPoint(form.current_position())) {
return true;
}
const CPoint current_position = ToPoint(form.current_position());
if (current_position != CPoint(0, 0)) {
return true;
}
if (!IsValidRect(form.area())) {
return true;
}
const CRect area = ToRect(form.area());
if (!area.IsRectNull()) {
return true;
}
return false;
}
bool ExtractParams(
LayoutManager *layout,
int compatibility_mode,
const commands::RendererCommand::ApplicationInfo &app_info,
CandidateWindowLayoutParams *params) {
DCHECK_NE(NULL, layout);
DCHECK_NE(NULL, params);
params->window_handle.Clear();
params->char_pos.Clear();
params->composition_form_topleft.Clear();
params->candidate_form.Clear();
params->caret_rect.Clear();
params->composition_font.Clear();
params->default_gui_font.Clear();
params->client_rect.Clear();
params->vertical_writing.Clear();
if (!app_info.has_target_window_handle()) {
return false;
}
const HWND target_window = reinterpret_cast<HWND>(
app_info.target_window_handle());
*params->window_handle.mutable_value() = target_window;
if (app_info.has_composition_target()) {
const commands::RendererCommand::CharacterPosition &char_pos =
app_info.composition_target();
// Check the availability of optional fields.
if (char_pos.has_position() &&
char_pos.has_top_left() &&
IsValidPoint(char_pos.top_left()) &&
char_pos.has_line_height() &&
char_pos.line_height() > 0 &&
char_pos.has_document_area() &&
IsValidRect(char_pos.document_area())) {
// Positional fields are (logical) screen coordinate.
IMECHARPOSITION *dest = params->char_pos.mutable_value();
dest->dwCharPos = char_pos.position();
dest->pt = ToPoint(char_pos.top_left());
dest->cLineHeight = char_pos.line_height();
dest->rcDocument = ToRect(char_pos.document_area());
}
}
if (app_info.has_composition_form()) {
const commands::RendererCommand::CompositionForm &form =
app_info.composition_form();
// Check the availability of optional fields.
if (form.has_current_position() &&
IsValidPoint(form.current_position()) &&
IsCompatibleCompositionForm(form, compatibility_mode)) {
Optional<CPoint> screen_pos;
if (!layout->ClientPointToScreen(
target_window, ToPoint(form.current_position()),
params->composition_form_topleft.mutable_value())) {
params->composition_form_topleft.Clear();
}
}
}
if (app_info.has_candidate_form()) {
const commands::RendererCommand::CandidateForm &form =
app_info.candidate_form();
// TODO(yukawa): Stop supporting |deprecated_style()|.
const uint32 candidate_style_bits =
(form.has_deprecated_style() ? form.deprecated_style()
: form.style_bits());
const bool has_candidate_pos_style_bit =
((candidate_style_bits & CandidateForm::CANDIDATEPOS) ==
CandidateForm::CANDIDATEPOS);
const bool has_exclude_style_bit =
((candidate_style_bits & CandidateForm::EXCLUDE) ==
CandidateForm::EXCLUDE);
// Check the availability of optional fields.
if ((has_candidate_pos_style_bit || has_exclude_style_bit) &&
form.has_current_position() &&
IsValidPoint(form.current_position())) {
const bool use_local_coord =
(compatibility_mode & USE_LOCAL_COORD_FOR_CANDIDATE_FORM) ==
USE_LOCAL_COORD_FOR_CANDIDATE_FORM;
Optional<CPoint> screen_pos;
if (use_local_coord) {
if (!layout->LocalPointToScreen(
target_window, ToPoint(form.current_position()),
screen_pos.mutable_value())) {
screen_pos.Clear();
}
} else {
if (!layout->ClientPointToScreen(
target_window, ToPoint(form.current_position()),
screen_pos.mutable_value())) {
screen_pos.Clear();
}
}
if (screen_pos.has_value()) {
// Here, we got an appropriate position where the candidate window
// can be placed.
params->candidate_form.mutable_value()->
InitializeWithPosition(screen_pos.value());
// If |CandidateForm::EXCLUDE| is specified, try to use the |area|
// field as an exclude region.
if (has_exclude_style_bit &&
form.has_area() &&
IsValidRect(form.area())) {
Optional<CRect> screen_rect;
if (use_local_coord) {
if (!layout->LocalRectToScreen(
target_window, ToRect(form.area()),
screen_rect.mutable_value())) {
screen_rect.Clear();
}
} else {
if (!layout->ClientRectToScreen(
target_window, ToRect(form.area()),
screen_rect.mutable_value())) {
screen_rect.Clear();
}
}
if (screen_rect.has_value()) {
// Here we got an appropriate exclude region too.
// Update the |candidate_form| with them.
params->candidate_form.mutable_value()->
InitializeWithPositionAndExcludeRegion(
screen_pos.value(), screen_rect.value());
}
}
}
}
}
if (app_info.has_caret_info()) {
const commands::RendererCommand::CaretInfo &caret_info =
app_info.caret_info();
// Check the availability of optional fields.
if (caret_info.has_blinking() &&
caret_info.has_caret_rect() &&
IsValidRect(caret_info.caret_rect()) &&
caret_info.has_target_window_handle()) {
const HWND caret_window = reinterpret_cast<HWND>(
caret_info.target_window_handle());
const CRect caret_rect_in_client_coord(ToRect(caret_info.caret_rect()));
// It seems (0, 0, 0, 0) represents that the application does not have a
// valid caret now.
if (!caret_rect_in_client_coord.IsRectNull()) {
if (!layout->ClientRectToScreen(
caret_window, caret_rect_in_client_coord,
params->caret_rect.mutable_value())) {
params->caret_rect.Clear();
}
}
}
}
if (app_info.has_composition_font()) {
if (!mozc::win32::FontUtil::ToLOGFONT(
app_info.composition_font(),
params->composition_font.mutable_value())) {
params->composition_font.Clear();
}
}
if (!layout->GetDefaultGuiFont(params->default_gui_font.mutable_value())) {
params->default_gui_font.Clear();
}
{
CRect client_rect_in_local_coord;
CRect client_rect_in_logical_screen_coord;
if (layout->GetClientRect(target_window, &client_rect_in_local_coord) &&
layout->ClientRectToScreen(
target_window, client_rect_in_local_coord,
&client_rect_in_logical_screen_coord)) {
*params->client_rect.mutable_value() =
client_rect_in_logical_screen_coord;
}
}
{
const LayoutManager::WritingDirection direction =
layout->GetWritingDirection(app_info);
if (direction == LayoutManager::VERTICAL_WRITING) {
*params->vertical_writing.mutable_value() = true;
} else if (direction == LayoutManager::HORIZONTAL_WRITING) {
*params->vertical_writing.mutable_value() = false;
}
}
return true;
}
wstring ComposePreeditText(const commands::Preedit &preedit,
string *preedit_utf8,
vector<int> *segment_indices,
vector<CharacterRange> *segment_ranges) {
if (preedit_utf8 != NULL) {
preedit_utf8->clear();
}
if (segment_indices != NULL) {
segment_indices->clear();
}
if (segment_ranges != NULL) {
segment_ranges->clear();
}
wstring value;
int total_characters = 0;
for (size_t segment_index = 0; segment_index < preedit.segment_size();
++segment_index) {
const commands::Preedit::Segment &segment = preedit.segment(segment_index);
wstring segment_value;
mozc::Util::UTF8ToWide(segment.value(), &segment_value);
value.append(segment_value);
if (preedit_utf8 != NULL) {
preedit_utf8->append(segment.value());
}
const int text_length = segment_value.size();
if (segment_indices != NULL) {
for (size_t i = 0; i < text_length; ++i) {
segment_indices->push_back(segment_index);
}
}
if (segment_ranges != NULL) {
CharacterRange range;
range.begin = total_characters;
range.length = text_length;
segment_ranges->push_back(range);
}
total_characters += text_length;
}
return value;
}
bool CalcLayoutWithTextWrappingInternal(
CDCHandle dc,
const wstring &str,
const int maximum_line_length,
const int initial_offset,
vector<LineLayout> *line_layouts) {
DCHECK(line_layouts != NULL);
if (initial_offset < 0 || maximum_line_length <= 0 ||
maximum_line_length < initial_offset) {
LOG(ERROR) << "(initial_offset, maximum_line_length) = ("
<< initial_offset << ", " << maximum_line_length << ")";
return false;
}
TEXTMETRIC metrics = {0};
if (!dc.GetTextMetrics(&metrics)) {
const int error = ::GetLastError();
LOG(ERROR) << "GetTextMetrics failed. error = " << error;
return false;
}
int string_index = 0;
int current_offset = initial_offset;
while (true) {
const int remaining_chars = str.size() - string_index;
if (remaining_chars <= 0) {
return true;
}
// Here we can assume the following conditions are satisfied.
// 0 <= current_offset
// 0 <= maximum_line_length
// current_offset <= maximum_line_length
const int remaining_extent = maximum_line_length - current_offset;
DCHECK_GE(remaining_extent, 0)
<< "(remaining_extent, maximum_line_length) = ("
<< remaining_extent << ", " << maximum_line_length << ")";
DCHECK_GE(maximum_line_length, remaining_extent)
<< "(remaining_extent, maximum_line_length) = ("
<< remaining_extent << ", " << maximum_line_length << ")";
int allowable_chars = 0;
CSize dummy;
BOOL result = dc.GetTextExtentExPoint(
str.c_str() + string_index,
remaining_chars,
&dummy,
remaining_extent,
&allowable_chars,
NULL);
if (result == FALSE) {
const int error = ::GetLastError();
LOG(ERROR) << "GetTextExtentExPoint failed. error = " << error;
return false;
}
if (allowable_chars == 0 && current_offset == 0) {
// In this case, the region does not have enough space to display the
// next character.
return false;
}
// Just in case GetTextExtentExPoint returns true but the returned value
// is invalid. We have not seen any problem around this API though.
if (allowable_chars < 0 || remaining_chars < allowable_chars) {
// Something wrong.
LOG(ERROR) << "(allowable_chars, remaining_chars) = ("
<< allowable_chars << ", " << remaining_chars << ")";
return false;
}
{
LineLayout layout;
layout.text = str.substr(string_index, allowable_chars);
if (allowable_chars == 0) {
// This case occurs only when this line does not have enough space to
// render the next character from |current_offset|. We will try to
// render text in the next line. Note that an infinite loop should
// never occur because we have already checked the case above. This is
// why |current_offset| should be positive here.
DCHECK_GT(current_offset, 0);
layout.line_width = metrics.tmHeight;
} else {
CSize line_size;
int allowable_chars_for_confirmation = 0;
unique_ptr<int[]> size_buffer(new int[allowable_chars]);
result = dc.GetTextExtentExPoint(
layout.text.c_str(),
layout.text.size(),
&line_size,
remaining_extent,
&allowable_chars_for_confirmation,
size_buffer.get());
if (result == FALSE) {
const int error = ::GetLastError();
LOG(ERROR) << "GetTextExtentExPoint failed. error = " << error;
return false;
}
if (allowable_chars != allowable_chars_for_confirmation) {
LOG(ERROR)
<< "(allowable_chars, allowable_chars_for_confirmation) = ("
<< allowable_chars << ", "
<< allowable_chars_for_confirmation << ")";
return false;
}
layout.line_length = line_size.cx;
layout.line_width = line_size.cy;
layout.line_start_offset = current_offset;
int next_char_begin = 0;
for (size_t character_index = 0; character_index < allowable_chars;
++character_index) {
CharacterRange range;
range.begin = next_char_begin;
range.length = size_buffer[character_index] - next_char_begin;
layout.character_positions.push_back(range);
next_char_begin = size_buffer[character_index];
}
}
DCHECK_EQ(layout.text.size(), layout.character_positions.size());
line_layouts->push_back(layout);
}
string_index += allowable_chars;
current_offset = 0;
}
return false;
}
class NativeSystemPreferenceAPI : public SystemPreferenceInterface {
public:
virtual ~NativeSystemPreferenceAPI() {}
virtual bool GetDefaultGuiFont(LOGFONTW *log_font) {
if (log_font == NULL) {
return false;
}
CLogFont message_box_font;
// Use message box font as a default font to be consistent with
// the candidate window.
// TODO(yukawa): make a theme layer which is responsible for
// the look and feel of both composition window and candidate window.
// TODO(yukawa): verify the font can render U+005C as a yen sign.
// (http://b/1992773)
message_box_font.SetMessageBoxFont();
// Use factor "3" to be consistent with the candidate window.
message_box_font.MakeLarger(3);
message_box_font.lfWeight = FW_NORMAL;
*log_font = message_box_font;
return true;
}
};
class NativeWorkingAreaAPI : public WorkingAreaInterface {
public:
NativeWorkingAreaAPI() {}
private:
virtual bool GetWorkingAreaFromPoint(const POINT &point,
RECT *working_area) {
if (working_area == nullptr) {
return false;
}
::SetRect(working_area, 0, 0, 0, 0);
// Obtain the monitor's working area
const HMONITOR monitor = ::MonitorFromPoint(point,
MONITOR_DEFAULTTONEAREST);
if (monitor == nullptr) {
return false;
}
MONITORINFO monitor_info = {};
monitor_info.cbSize = CCSIZEOF_STRUCT(MONITORINFO, dwFlags);
if (!::GetMonitorInfo(monitor, &monitor_info)) {
const DWORD error = GetLastError();
LOG(ERROR) << "GetMonitorInfo failed. Error: " << error;
return false;
}
*working_area = monitor_info.rcWork;
return true;
}
};
class NativeWindowPositionAPI : public WindowPositionInterface {
public:
NativeWindowPositionAPI()
: logical_to_physical_point_(GetLogicalToPhysicalPoint()) {
}
virtual ~NativeWindowPositionAPI() {}
virtual bool LogicalToPhysicalPoint(
HWND window_handle, const POINT &logical_coordinate,
POINT *physical_coordinate) {
if (physical_coordinate == NULL) {
return false;
}
DCHECK_NE(NULL, physical_coordinate);
if (!::IsWindow(window_handle)) {
return false;
}
if (logical_to_physical_point_ == NULL) {
// In Windows XP, LogicalToPhysicalPoint API is not available.
// In this case, we simply returns the specified coordinate and returns
// true.
*physical_coordinate = logical_coordinate;
return true;
}
// The attached window is likely to be a child window but only root
// windows are fully supported by LogicalToPhysicalPoint API. Using
// root window handle instead of target window handle is likely to make
// this API happy.
const HWND root_window_handle = GetRootWindow(window_handle);
// The document of LogicalToPhysicalPoint API is somewhat ambiguous.
// http://msdn.microsoft.com/en-us/library/ms633533.aspx
// Both input coordinates and output coordinates of this API are so-called
// screen coordinates (offset from the upper-left corner of the screen).
// Note that the input coordinates are logical coordinates, which means you
// should pass screen coordinates obtained in a DPI-unaware process to
// this API. For example, coordinates returned by ClientToScreen API in a
// DPI-unaware process are logical coordinates. You can copy these
// coordinates to a DPI-aware process and convert them to physical screen
// coordinates by LogicalToPhysicalPoint API.
*physical_coordinate = logical_coordinate;
return logical_to_physical_point_(root_window_handle,
physical_coordinate) != FALSE;
}
// This method is not const to implement Win32WindowInterface.
virtual bool GetWindowRect(HWND window_handle, RECT *rect) {
return (::GetWindowRect(window_handle, rect) != FALSE);
}
// This method is not const to implement Win32WindowInterface.
virtual bool GetClientRect(HWND window_handle, RECT *rect) {
return (::GetClientRect(window_handle, rect) != FALSE);
}
// This method is not const to implement Win32WindowInterface.
virtual bool ClientToScreen(HWND window_handle, POINT *point) {
return (::ClientToScreen(window_handle, point) != FALSE);
}
// This method is not const to implement Win32WindowInterface.
virtual bool IsWindow(HWND window_handle) {
return (::IsWindow(window_handle) != FALSE);
}
// This method is not const to implement Win32WindowInterface.
virtual HWND GetRootWindow(HWND window_handle) {
// See the following document for Win32 window system.
// http://msdn.microsoft.com/en-us/library/ms997562.aspx
return ::GetAncestor(window_handle, GA_ROOT);
}
// This method is not const to implement Win32WindowInterface.
virtual bool GetWindowClassName(HWND window_handle, wstring *class_name) {
if (class_name == NULL) {
return false;
}
wchar_t class_name_buffer[1024] = {};
const size_t num_copied_without_null = ::GetClassNameW(
window_handle, class_name_buffer, ARRAYSIZE(class_name_buffer));
if (num_copied_without_null >= (ARRAYSIZE(class_name_buffer) - 1)) {
DLOG(ERROR) << "buffer length is insufficient.";
return false;
}
class_name_buffer[num_copied_without_null] = L'\0';
class_name->assign(class_name_buffer);
return (::IsWindow(window_handle) != FALSE);
}
private:
typedef BOOL (WINAPI *FPLogicalToPhysicalPoint)(HWND window_handle,
POINT *point);
static FPLogicalToPhysicalPoint GetLogicalToPhysicalPoint() {
// LogicalToPhysicalPoint API is available in Vista or later.
const HMODULE module = WinUtil::GetSystemModuleHandle(L"user32.dll");
if (module == nullptr) {
return nullptr;
}
// Despite its name, LogicalToPhysicalPoint API no longer converts
// coordinates on Windows 8.1 and later. We must use
// LogicalToPhysicalPointForPerMonitorDPI API instead when it is available.
// See http://go.microsoft.com/fwlink/?LinkID=307061
void *function = ::GetProcAddress(
module, "LogicalToPhysicalPointForPerMonitorDPI");
if (function == nullptr) {
// When LogicalToPhysicalPointForPerMonitorDPI API does not exist but
// LogicalToPhysicalPoint API exists, LogicalToPhysicalPoint works fine.
// This is the case on Windows Vista, Windows 7 and Windows 8.
function = ::GetProcAddress(module, "LogicalToPhysicalPoint");
if (function == nullptr) {
return nullptr;
}
}
return reinterpret_cast<FPLogicalToPhysicalPoint>(function);
}
FPLogicalToPhysicalPoint logical_to_physical_point_;
DISALLOW_COPY_AND_ASSIGN(NativeWindowPositionAPI);
};
struct WindowInfo {
wstring class_name;
CRect window_rect;
CPoint client_area_offset;
CSize client_area_size;
double scale_factor;
WindowInfo()
: scale_factor(1.0) {}
};
class SystemPreferenceEmulatorImpl : public SystemPreferenceInterface {
public:
explicit SystemPreferenceEmulatorImpl(const LOGFONTW &gui_font)
: default_gui_font_(gui_font) {}
virtual ~SystemPreferenceEmulatorImpl() {}
virtual bool GetDefaultGuiFont(LOGFONTW *log_font) {
if (log_font == NULL) {
return false;
}
*log_font = default_gui_font_;
return true;
}
private:
CLogFont default_gui_font_;
};
class WorkingAreaEmulatorImpl : public WorkingAreaInterface {
public:
explicit WorkingAreaEmulatorImpl(const CRect &area)
: area_(area) {}
private:
virtual bool GetWorkingAreaFromPoint(const POINT &point,
RECT *working_area) {
if (working_area == nullptr) {
return false;
}
*working_area = area_;
return true;
}
const CRect area_;
};
class WindowPositionEmulatorImpl : public WindowPositionEmulator {
public:
WindowPositionEmulatorImpl() {}
virtual ~WindowPositionEmulatorImpl() {}
// This method is not const to implement Win32WindowInterface.
virtual bool GetWindowRect(HWND window_handle, RECT *rect) {
if (rect == NULL) {
return false;
}
const WindowInfo *info = GetWindowInfomation(window_handle);
if (info == NULL) {
return false;
}
*rect = info->window_rect;
return true;
}
// This method is not const to implement Win32WindowInterface.
virtual bool GetClientRect(HWND window_handle, RECT *rect) {
if (rect == NULL) {
return false;
}
const WindowInfo *info = GetWindowInfomation(window_handle);
if (info == NULL) {
return false;
}
*rect = CRect(CPoint(0, 0), info->client_area_size);
return true;
}
// This method is not const to implement Win32WindowInterface.
virtual bool ClientToScreen(HWND window_handle, POINT *point) {
if (point == NULL) {
return false;
}
const WindowInfo *info = GetWindowInfomation(window_handle);
if (info == NULL) {
return false;
}
*point = (info->window_rect.TopLeft() + info->client_area_offset +
*point);
return true;
}
// This method is not const to implement Win32WindowInterface.
virtual bool IsWindow(HWND window_handle) {
const WindowInfo *info = GetWindowInfomation(window_handle);
if (info == NULL) {
return false;
}
return true;
}
// This method wraps API call of GetAncestor/GA_ROOT.
virtual HWND GetRootWindow(HWND window_handle) {
const map<HWND, HWND>::const_iterator it = root_map_.find(window_handle);
if (it == root_map_.end()) {
return window_handle;
}
return it->second;
}
// This method is not const to implement Win32WindowInterface.
virtual bool GetWindowClassName(HWND window_handle, wstring *class_name) {
if (class_name == NULL) {
return false;
}
const WindowInfo *info = GetWindowInfomation(window_handle);
if (info == NULL) {
return false;
}
*class_name = info->class_name;
return true;
}
// This method is not const to implement Win32WindowInterface.
virtual bool LogicalToPhysicalPoint(
HWND window_handle, const POINT &logical_coordinate,
POINT *physical_coordinate) {
if (physical_coordinate == NULL) {
return false;
}
DCHECK_NE(NULL, physical_coordinate);
const WindowInfo *root_info =
GetWindowInfomation(GetRootWindow(window_handle));
if (root_info == NULL) {
return false;
}
// BottomRight is treated inside of the rect in this scenario.
const CRect &bottom_right_inflated_rect = CRect(
root_info->window_rect.left,
root_info->window_rect.top,
root_info->window_rect.right + 1,
root_info->window_rect.bottom + 1);
if (bottom_right_inflated_rect.PtInRect(logical_coordinate) == FALSE) {
return false;
}
physical_coordinate->x = logical_coordinate.x * root_info->scale_factor;
physical_coordinate->y = logical_coordinate.y * root_info->scale_factor;
return true;
}
virtual HWND RegisterWindow(
const wstring &class_name, const RECT &window_rect,
const POINT &client_area_offset, const SIZE &client_area_size,
double scale_factor) {
const HWND hwnd = GetNextWindowHandle();
window_map_[hwnd].class_name = class_name;
window_map_[hwnd].window_rect = window_rect;
window_map_[hwnd].client_area_offset = client_area_offset;
window_map_[hwnd].client_area_size = client_area_size;
window_map_[hwnd].scale_factor = scale_factor;
return hwnd;
}
virtual void SetRoot(HWND child_window, HWND root_window) {
root_map_[child_window] = root_window;
}
private:
HWND GetNextWindowHandle() const {
if (window_map_.size() > 0) {
const HWND last_hwnd = window_map_.rbegin()->first;
return reinterpret_cast<HWND>(
reinterpret_cast<BYTE *>(last_hwnd) + sizeof(last_hwnd));
}
return reinterpret_cast<HWND>(0x12345678);
}
// This method is not const to implement Win32WindowInterface.
const WindowInfo *GetWindowInfomation(HWND hwnd) {
if (window_map_.find(hwnd) == window_map_.end()) {
return NULL;
}
return &(window_map_.find(hwnd)->second);
}
map<HWND, WindowInfo> window_map_;
map<HWND, HWND> root_map_;
DISALLOW_COPY_AND_ASSIGN(WindowPositionEmulatorImpl);
};
bool IsVerticalWriting(const CandidateWindowLayoutParams &params) {
return params.vertical_writing.has_value() &&
params.vertical_writing.value();
}
// This is a helper function for LayoutCandidateWindowByCandidateForm.
// Some applications give us only the base position of candidate window.
// However, the exclude region is definitely important in terms of UX around
// candidate/suggest window. As a workaround, we try to use font height to
// compose a virtual exclude region from the base position.
// Expected applications and controls are:
// - Candidate Window on Pidgin 2.6.1
// - Candidate Window on V2C 2.1.6 on JRE 1.6.0.21
// - Candidate Window on Fudemame 21
// See also relevant unit tests.
// Returns true if the |candidate_layout| is determined in successful.
bool UpdateCandidateWindowFromBasePosAndFontHeight(
const CandidateWindowLayoutParams &params,
int compatibility_mode,
const CPoint &base_pos_in_logical_coord,
LayoutManager *layout_manager,
CandidateWindowLayout *candidate_layout) {
DCHECK(candidate_layout);
candidate_layout->Clear();
if (!params.window_handle.has_value()) {
return false;
}
const HWND target_window = params.window_handle.value();
const int font_height = GetAbsoluteFontHeight(params);
if (font_height == 0) {
return false;
}
DCHECK_LT(0, font_height);
const bool is_vertical = IsVerticalWriting(params);
CRect exclude_region_in_logical_coord;
if (is_vertical) {
// Vertical
exclude_region_in_logical_coord.SetRect(
base_pos_in_logical_coord.x,
base_pos_in_logical_coord.y,
base_pos_in_logical_coord.x + font_height,
base_pos_in_logical_coord.y + 1);
} else {
// Horizontal
exclude_region_in_logical_coord.SetRect(
base_pos_in_logical_coord.x,
base_pos_in_logical_coord.y - font_height,
base_pos_in_logical_coord.x + 1,
base_pos_in_logical_coord.y);
}
CRect exclude_region_in_physical_coord;
layout_manager->GetRectInPhysicalCoords(
target_window, exclude_region_in_logical_coord,
&exclude_region_in_physical_coord);
const CPoint base_pos_in_physical_coord =
GetBasePositionFromExcludeRect(exclude_region_in_physical_coord,
is_vertical);
candidate_layout->InitializeWithPositionAndExcludeRegion(
base_pos_in_physical_coord, exclude_region_in_physical_coord);
return true;
}
// CANDIDATEFORM is most standard way to specify the position of a candidate
// window. There are two major cases; one has an exclude region and the other
// has no exclude region. The second case is virtually handled by
// UpdateCandidateWindowFromBasePosAndFontHeight function.
// Expected applications and controls (for the first case) are:
// - Suggest Window on apps with CAN_USE_CANDIDATE_FORM_FOR_SUGGEST
// -- Qt-related windows whose class name is "QWidget"
// -- Google Chrome-related windows whose class name is
// "Chrome_RenderWidgetHostHWND"
// - Candidate Window on windows which do not support IMECHARPOSITION
// -- Internet Explorer
// -- Open Office Writer
// -- Qt-based applications
// See also relevant unit tests.
// Returns true if the |candidate_layout| is determined in successful.
bool LayoutCandidateWindowByCandidateForm(
const CandidateWindowLayoutParams &params,
int compatibility_mode,
LayoutManager *layout_manager,
CandidateWindowLayout *candidate_layout) {
DCHECK(candidate_layout);
candidate_layout->Clear();
if (!params.window_handle.has_value()) {
return false;
}
if (!params.candidate_form.has_value()) {
return false;
}
const HWND target_window = params.window_handle.value();
const CandidateWindowLayout &form = params.candidate_form.value();
CPoint base_pos_in_physical_coord;
layout_manager->GetPointInPhysicalCoords(
target_window, form.position(), &base_pos_in_physical_coord);
if (!form.has_exclude_region()) {
// If the candidate form does not have the exclude region, try to compose
// supplemental exclude region by using font height information.
CandidateWindowLayout layout;
if (UpdateCandidateWindowFromBasePosAndFontHeight(
params, compatibility_mode, form.position(), layout_manager,
&layout)) {
// succeeded to compose exclude region.
DCHECK(layout.initialized());
*candidate_layout = layout;
return true;
}
candidate_layout->InitializeWithPosition(base_pos_in_physical_coord);
return true;
}
DCHECK(form.has_exclude_region());
CRect exclude_region_in_physical_coord;
layout_manager->GetRectInPhysicalCoords(
target_window, form.exclude_region(),
&exclude_region_in_physical_coord);
candidate_layout->InitializeWithPositionAndExcludeRegion(
base_pos_in_physical_coord, exclude_region_in_physical_coord);
return true;
}
// This function tries to use IMECHARPOSITION structure, which gives us
// sufficient information around the focused segment to use EXCLUDE-style
// positioning. However, relatively small number of applications support
// this structure.
// Expected applications and controls are:
// - Microsoft Word
// - Built-in RichEdit control
// -- Chrome's Omni-box
// - Built-in Edit control
// -- Internet Explorer's address bar
// - Firefox
// See also relevant unit tests.
// Returns true if the |candidate_layout| is determined in successful.
bool LayoutCandidateWindowByCompositionTarget(
const CandidateWindowLayoutParams &params,
int compatibility_mode,
LayoutManager *layout_manager,
CandidateWindowLayout *candidate_layout) {
DCHECK(candidate_layout);
candidate_layout->Clear();
if (!params.window_handle.has_value()) {
return false;
}
if (!params.char_pos.has_value()) {
return false;
}
const HWND target_window = params.window_handle.value();
const IMECHARPOSITION &char_pos = params.char_pos.value();
// From the behavior of MS Office, we assume that an application fills
// members in IMECHARPOSITION as follows, even though other interpretations
// might be possible from the document especially for the vertical writing.
// http://msdn.microsoft.com/en-us/library/dd318162.aspx
//
// [Horizontal Writing]
//
// (pt)
// v_____
// | |
// | | (cLineHeight)
// | |
// --+-----+----------> (Base Line)
//
// [Vertical Writing]
//
// |
// +-----< (pt)
// | |
// |-----+
// | (cLineHeight)
// |
// |
// v
// (Base Line)
const bool is_vertical = IsVerticalWriting(params);
CRect exclude_region_in_logical_coord;
if (is_vertical) {
// Vertical
exclude_region_in_logical_coord.left =
char_pos.pt.x - char_pos.cLineHeight;
exclude_region_in_logical_coord.top = char_pos.pt.y;
exclude_region_in_logical_coord.right = char_pos.pt.x;
exclude_region_in_logical_coord.bottom = char_pos.pt.y + 1;
} else {
// Horizontal
exclude_region_in_logical_coord.left = char_pos.pt.x;
exclude_region_in_logical_coord.top = char_pos.pt.y;
exclude_region_in_logical_coord.right = char_pos.pt.x + 1;
exclude_region_in_logical_coord.bottom =
char_pos.pt.y + char_pos.cLineHeight;
}
const CPoint base_pos_in_logical_coord =
GetBasePositionFromExcludeRect(exclude_region_in_logical_coord,
is_vertical);
CPoint base_pos_in_physical_coord;
layout_manager->GetPointInPhysicalCoords(
target_window, base_pos_in_logical_coord,
&base_pos_in_physical_coord);
CRect exclude_region_in_physical_coord;
layout_manager->GetRectInPhysicalCoords(
target_window, exclude_region_in_logical_coord,
&exclude_region_in_physical_coord);
candidate_layout->InitializeWithPositionAndExcludeRegion(
base_pos_in_physical_coord, exclude_region_in_physical_coord);
return true;
}
// COMPOSITIONFORM contains the expected position of top-left corner of the
// composition window, which might be used to determine the position of the
// candidate window if no other relevant information is available.
// Actually the top left corner might be good enough for vertical writing.
// As for horizontal writing, the base position can be calculated if the height
// of the composition string is available. This function supposes that the
// font height approximates to the height of the composition string.
// In both cases, this function also tries to compose an exclude region to
// improve the UX around suggest/candidate window if font height is available.
// Expected applications and controls are:
// - Suggest window on Pidgin 2.6.1
// See also relevant unit tests.
// Returns true if the |candidate_layout| is determined in successful.
bool LayoutCandidateWindowByCompositionForm(
const CandidateWindowLayoutParams &params,
int compatibility_mode,
LayoutManager *layout_manager,
CandidateWindowLayout *candidate_layout) {
DCHECK(candidate_layout);
candidate_layout->Clear();
if (!params.window_handle.has_value()) {
return false;
}
if (!params.composition_form_topleft.has_value()) {
return false;
}
const HWND target_window = params.window_handle.value();
const CPoint &topleft_in_logical_coord =
params.composition_form_topleft.value();
const bool is_vertical = IsVerticalWriting(params);
const int font_height = GetAbsoluteFontHeight(params);
if (!is_vertical) {
// For horizontal writing, a valid |font_height| is necessary to calculate
// an appropriate position of suggest/candidate window.
if (font_height == 0) {
return false;
}
DCHECK_LT(0, font_height);
const CRect rect_in_logical_coord(
topleft_in_logical_coord.x,
topleft_in_logical_coord.y,
topleft_in_logical_coord.x + 1,
topleft_in_logical_coord.y + font_height);
CRect rect_in_physical_coord;
layout_manager->GetRectInPhysicalCoords(
target_window, rect_in_logical_coord, &rect_in_physical_coord);
const CPoint bottom_left_in_physical_coord(
rect_in_physical_coord.left, rect_in_physical_coord.bottom);
candidate_layout->InitializeWithPositionAndExcludeRegion(
bottom_left_in_physical_coord, rect_in_physical_coord);
return true;
}
// Vertical
DCHECK(is_vertical);
CPoint topleft_in_physical_coord;
layout_manager->GetPointInPhysicalCoords(
target_window, topleft_in_logical_coord,
&topleft_in_physical_coord);
if (font_height == 0) {
// For vertical writing, top-left cornier is acceptable.
// Use CANDIDATEPOS-style by compromise.
candidate_layout->InitializeWithPosition(topleft_in_physical_coord);
return true;
}
DCHECK_LT(0, font_height);
const CRect rect_in_logical_coord(
topleft_in_logical_coord.x,
topleft_in_logical_coord.y,
topleft_in_logical_coord.x + font_height,
topleft_in_logical_coord.y + 1);
CRect rect_in_physical_coord;
layout_manager->GetRectInPhysicalCoords(
target_window, rect_in_logical_coord, &rect_in_physical_coord);
candidate_layout->InitializeWithPositionAndExcludeRegion(
topleft_in_physical_coord, rect_in_physical_coord);
return true;
}
// This function calculates the candidate window position by using caret
// information, which is generally unreliable but sometimes becomes a good
// alternative even when no other positional information is available.
// In fact, the position of suggest window sometimes relies on the caret
// position because it is not guaranteed that the CANDIDATEFORM is valid before
// the application receives IMN_OPENCANDIDATE message.
// Another important consideration is how to calculate the exclude region.
// One may consider that the caret rect seems to be used but very small number
// of applications always use 1x1 rect regardless of the actual caret size.
// To improve the positional accuracy of the exclude region, this function
// adopt larger one between the caret height and font height when the exclude
// region is calculated.
// Relevant applications and controls are:
// - Workaround against Google Chrome (b/3104035)
// - Suggest window on Hidemaru
// - Suggest window on Open Office Writer
// - Suggest window on Internet Explorer 8
// See also relevant unit tests.
// Returns true if the |candidate_layout| is determined in successful.
bool LayoutCandidateWindowByCaretInfo(
const CandidateWindowLayoutParams &params,
int compatibility_mode,
LayoutManager *layout_manager,
CandidateWindowLayout *candidate_layout) {
DCHECK(candidate_layout);
candidate_layout->Clear();
if (!params.window_handle.has_value()) {
return false;
}
if (!params.caret_rect.has_value()) {
return false;
}
const HWND target_window = params.window_handle.value();
CRect exclude_region_in_logical_coord = params.caret_rect.value();
// Use font height if available to improve the accuracy of exclude region.
const int font_height = GetAbsoluteFontHeight(params);
const bool is_vertical = IsVerticalWriting(params);
if (font_height > 0) {
if (is_vertical &&
(exclude_region_in_logical_coord.Width() < font_height)) {
// Vertical
exclude_region_in_logical_coord.right =
exclude_region_in_logical_coord.left + font_height;
} else if (!is_vertical &&
(exclude_region_in_logical_coord.Height() < font_height)) {
// Horizontal
exclude_region_in_logical_coord.bottom =
exclude_region_in_logical_coord.top + font_height;
}
}
CRect exclude_region_in_physical_coord;
layout_manager->GetRectInPhysicalCoords(
target_window, exclude_region_in_logical_coord,
&exclude_region_in_physical_coord);
const CPoint base_pos_in_physical_coord =
GetBasePositionFromExcludeRect(exclude_region_in_physical_coord,
is_vertical);
candidate_layout->InitializeWithPositionAndExcludeRegion(
base_pos_in_physical_coord, exclude_region_in_physical_coord);
return true;
}
// On some applications, no positional information is available especially when
// the client want to show the suggest window. In this case, we might want to
// show the (suggest) window next to the target window so that the candidate
// window will not cover the target window.
// Expected applications and controls are:
// - Suggest window on Fudemame 21
// See also relevant unit tests.
// Returns true if the |candidate_layout| is determined in successful.
bool LayoutCandidateWindowByClientRect(
const CandidateWindowLayoutParams &params,
int compatibility_mode,
LayoutManager *layout_manager,
CandidateWindowLayout *candidate_layout) {
DCHECK(candidate_layout);
candidate_layout->Clear();
if (!params.window_handle.has_value()) {
return false;
}
if (!params.client_rect.has_value()) {
return false;
}
const HWND target_window = params.window_handle.value();
const CRect &client_rect_in_logical_coord =
params.client_rect.value();
const bool is_vertical = IsVerticalWriting(params);
CRect client_rect_in_physical_coord;
layout_manager->GetRectInPhysicalCoords(
target_window, client_rect_in_logical_coord,
&client_rect_in_physical_coord);
if (is_vertical) {
// Vertical
// Current candidate window has not fully supported vertical writing yet so
// it would be rather better to show the candidate window at the right side
// of the target window.
// This is why we do not use GetBasePositionFromExcludeRect here.
// TODO(yukawa): use GetBasePositionFromExcludeRect once the vertical
// writing is fully supported by the candidate window.
candidate_layout->InitializeWithPosition(CPoint(
client_rect_in_physical_coord.right,
client_rect_in_physical_coord.top));
} else {
// Horizontal
candidate_layout->InitializeWithPosition(CPoint(
client_rect_in_physical_coord.left,
client_rect_in_physical_coord.bottom));
}
return true;
}
bool LayoutIndicatorWindowByCompositionTarget(
const CandidateWindowLayoutParams &params,
const LayoutManager &layout_manager,
CRect *target_rect) {
DCHECK(target_rect);
*target_rect = CRect();
if (!params.window_handle.has_value()) {
return false;
}
if (!params.char_pos.has_value()) {
return false;
}
const HWND target_window = params.window_handle.value();
const IMECHARPOSITION &char_pos = params.char_pos.value();
// From the behavior of MS Office, we assume that an application fills
// members in IMECHARPOSITION as follows, even though other interpretations
// might be possible from the document especially for the vertical writing.
// http://msdn.microsoft.com/en-us/library/dd318162.aspx
const bool is_vertical = IsVerticalWriting(params);
CRect rect_in_logical_coord;
if (is_vertical) {
// [Vertical Writing]
//
// |
// +-----< (pt)
// | |
// |-----+
// | (cLineHeight)
// |
// |
// v
// (Base Line)
rect_in_logical_coord = CRect(
char_pos.pt.x - char_pos.cLineHeight,
char_pos.pt.y,
char_pos.pt.x,
char_pos.pt.y + 1);
} else {
// [Horizontal Writing]
//
// (pt)
// v_____
// | |
// | | (cLineHeight)
// | |
// --+-----+----------> (Base Line)
rect_in_logical_coord = CRect(
char_pos.pt.x,
char_pos.pt.y,
char_pos.pt.x + 1,
char_pos.pt.y + char_pos.cLineHeight);
}
layout_manager.GetRectInPhysicalCoords(
target_window, rect_in_logical_coord, target_rect);
return true;
}
bool LayoutIndicatorWindowByCompositionForm(
const CandidateWindowLayoutParams &params,
const LayoutManager &layout_manager,
CRect *target_rect) {
DCHECK(target_rect);
*target_rect = CRect();
if (!params.window_handle.has_value()) {
return false;
}
if (!params.composition_form_topleft.has_value()) {
return false;
}
const HWND target_window = params.window_handle.value();
const CPoint &topleft_in_logical_coord =
params.composition_form_topleft.value();
const bool is_vertical = IsVerticalWriting(params);
const int font_height = GetAbsoluteFontHeight(params);
if (font_height <= 0) {
return false;
}
const CRect rect_in_logical_coord(
topleft_in_logical_coord,
is_vertical ? CSize(font_height, 1) : CSize(1, font_height));
layout_manager.GetRectInPhysicalCoords(
target_window, rect_in_logical_coord, target_rect);
return true;
}
bool LayoutIndicatorWindowByCaretInfo(
const CandidateWindowLayoutParams &params,
const LayoutManager &layout_manager,
CRect *target_rect) {
DCHECK(target_rect);
*target_rect = CRect();
if (!params.window_handle.has_value()) {
return false;
}
if (!params.caret_rect.has_value()) {
return false;
}
const HWND target_window = params.window_handle.value();
CRect rect_in_logical_coord = params.caret_rect.value();
// Use font height if available to improve the accuracy of exlude region.
const int font_height = GetAbsoluteFontHeight(params);
const bool is_vertical = IsVerticalWriting(params);
if (font_height > 0) {
if (is_vertical &&
(rect_in_logical_coord.Width() < font_height)) {
// Vertical
rect_in_logical_coord.right =
rect_in_logical_coord.left + font_height;
} else if (!is_vertical &&
(rect_in_logical_coord.Height() < font_height)) {
// Horizontal
rect_in_logical_coord.bottom =
rect_in_logical_coord.top + font_height;
}
}
layout_manager.GetRectInPhysicalCoords(
target_window, rect_in_logical_coord, target_rect);
return true;
}
bool GetTargetRectForIndicator(
const CandidateWindowLayoutParams &params,
const LayoutManager &layout_manager,
CRect *focus_rect) {
if (focus_rect == nullptr) {
return false;
}
if (LayoutIndicatorWindowByCompositionTarget(params, layout_manager,
focus_rect)) {
return true;
}
if (LayoutIndicatorWindowByCompositionForm(params,
layout_manager,
focus_rect)) {
return true;
}
if (LayoutIndicatorWindowByCaretInfo(params,
layout_manager,
focus_rect)) {
return true;
}
// Clear the data just in case.
*focus_rect = CRect();
return false;
}
} // namespace
SystemPreferenceInterface *SystemPreferenceFactory::CreateMock(
const LOGFONTW &gui_font) {
return new SystemPreferenceEmulatorImpl(gui_font);
}
WorkingAreaInterface *WorkingAreaFactory::Create() {
return new NativeWorkingAreaAPI();
}
WorkingAreaInterface *WorkingAreaFactory::CreateMock(
const RECT &working_area) {
return new WorkingAreaEmulatorImpl(working_area);
}
WindowPositionEmulator *WindowPositionEmulator::Create() {
return new WindowPositionEmulatorImpl();
}
CharacterRange::CharacterRange()
: begin(0),
length(0) {}
LineLayout::LineLayout()
: line_length(0),
line_width(0),
line_start_offset(0) {}
CandidateWindowLayout::CandidateWindowLayout()
: position_(CPoint()),
exclude_region_(CRect()),
has_exclude_region_(false),
initialized_(false) {}
CandidateWindowLayout::~CandidateWindowLayout() {}
void CandidateWindowLayout::Clear() {
position_ = CPoint();
exclude_region_ = CRect();
has_exclude_region_ = false;
initialized_ = false;
}
void CandidateWindowLayout::InitializeWithPosition(const POINT &position) {
position_ = position;
exclude_region_ = CRect();
has_exclude_region_ = false;
initialized_ = true;
}
void CandidateWindowLayout::InitializeWithPositionAndExcludeRegion(
const POINT &position, const RECT &exclude_region) {
position_ = position;
exclude_region_ = exclude_region;
has_exclude_region_ = true;
initialized_ = true;
}
const POINT &CandidateWindowLayout::position() const {
return position_;
}
const RECT &CandidateWindowLayout::exclude_region() const {
DCHECK(has_exclude_region_);
return exclude_region_;
}
bool CandidateWindowLayout::has_exclude_region() const {
return has_exclude_region_;
}
bool CandidateWindowLayout::initialized() const {
return initialized_;
}
IndicatorWindowLayout::IndicatorWindowLayout()
: is_vertical(false) {
::SetRect(&window_rect, 0, 0, 0, 0);
}
void IndicatorWindowLayout::Clear() {
is_vertical = false;
::SetRect(&window_rect, 0, 0, 0, 0);
}
bool LayoutManager::CalcLayoutWithTextWrapping(
const LOGFONTW &font,
const wstring &text,
int maximum_line_length,
int initial_offset,
vector<LineLayout> *line_layouts) {
if (line_layouts == NULL) {
return false;
}
line_layouts->clear();
CFont new_font(CLogFont(font).CreateFontIndirectW());
if (new_font.IsNull()) {
LOG(ERROR) << "CreateFont failed.";
return false;
}
CDC dc;
// Create a memory DC compatible with desktop DC.
dc.CreateCompatibleDC(CDC(::GetDC(NULL)));
CFontHandle old_font = dc.SelectFont(new_font);
const bool result = CalcLayoutWithTextWrappingInternal(
dc.m_hDC, text, maximum_line_length, initial_offset,
line_layouts);
dc.SelectFont(old_font);
return result;
}
void LayoutManager::GetPointInPhysicalCoords(
HWND window_handle, const POINT &point, POINT *result) const {
if (result == NULL) {
return;
}
DCHECK_NE(nullptr, window_position_.get());
if (window_position_->LogicalToPhysicalPoint(
window_handle, point, result)) {
return;
}
// LogicalToPhysicalPoint API failed for some reason.
// Emulate the result based on the scaling factor.
const HWND root_window_handle =
window_position_->GetRootWindow(window_handle);
const double scale_factor = GetScalingFactor(root_window_handle);
result->x = point.x * scale_factor;
result->y = point.y * scale_factor;
return;
}
void LayoutManager::GetRectInPhysicalCoords(
HWND window_handle, const RECT &rect, RECT *result) const {
if (result == NULL) {
return;
}
DCHECK_NE(nullptr, window_position_.get());
CPoint top_left;
GetPointInPhysicalCoords(
window_handle, CPoint(rect.left, rect.top), &top_left);
CPoint bottom_right;
GetPointInPhysicalCoords(
window_handle, CPoint(rect.right, rect.bottom), &bottom_right);
*result = CRect(top_left, bottom_right);
return;
}
SegmentMarkerLayout::SegmentMarkerLayout()
: from(CPoint()),
to(CPoint()),
highlighted(false) {}
CompositionWindowLayout::CompositionWindowLayout()
: window_position_in_screen_coordinate(CRect()),
caret_rect(CRect()),
text_area(CRect()),
base_position(CPoint()),
log_font(CLogFont()) {}
LayoutManager::LayoutManager()
: system_preference_(new NativeSystemPreferenceAPI),
window_position_(new NativeWindowPositionAPI) {}
LayoutManager::LayoutManager(SystemPreferenceInterface *mock_system_preference,
WindowPositionInterface *mock_window_position)
: system_preference_(mock_system_preference),
window_position_(mock_window_position) {}
LayoutManager::~LayoutManager() {}
// TODO(yukawa): Refactor this function into smaller functions as soon as
// possible so that you can update the functionality and add new unit tests
// more easily.
bool LayoutManager::LayoutCompositionWindow(
const commands::RendererCommand &command,
vector<CompositionWindowLayout> *composition_window_layouts,
CandidateWindowLayout *candidate_layout) const {
if (composition_window_layouts != NULL) {
composition_window_layouts->clear();
}
if (candidate_layout != NULL) {
candidate_layout->Clear();
}
if (!command.has_output() ||
!command.output().has_preedit() ||
command.output().preedit().segment_size() <= 0 ||
!command.output().preedit().has_cursor() ||
!command.has_application_info() ||
!command.application_info().has_target_window_handle()) {
LOG(INFO) << "do nothing because of the lack of parameter(s)";
return true;
}
const mozc::commands::Output &output = command.output();
const HWND target_window_handle = reinterpret_cast<HWND>(
command.application_info().target_window_handle());
const mozc::commands::RendererCommand::ApplicationInfo &app =
command.application_info();
CLogFont logfont;
if (!app.has_composition_font() ||
!mozc::win32::FontUtil::ToLOGFONT(app.composition_font(), &logfont)) {
// If the composition font is not available, use default GUI font as a
// fall back.
if (!system_preference_->GetDefaultGuiFont(&logfont)) {
LOG(ERROR) << "GetDefaultGuiFont failed.";
return false;
}
}
// Remove underline attribute. See b/2935480 for details.
logfont.lfUnderline = 0;
// We only support lfEscapement == 0 or 2700.
if (logfont.lfEscapement != 0 && logfont.lfEscapement != 2700) {
LOG(ERROR) << "Unsupported escapement: " << logfont.lfEscapement;
return false;
}
const mozc::commands::Preedit &preedit = output.preedit();
const bool is_vertical = (GetWritingDirection(app) == VERTICAL_WRITING);
CompositionForm composition_form;
if (command.application_info().has_composition_form()) {
composition_form.CopyFrom(command.application_info().composition_form());
} else {
// No composition form is available. Use client rect instead.
CRect client_rect;
if (!window_position_->GetClientRect(target_window_handle, &client_rect)) {
return false;
}
// We need not to use CompositionForm::RECT. The client area will be used
// for character wrapping anyway.
composition_form.set_style_bits(CompositionForm::POINT);
if (is_vertical) {
composition_form.mutable_current_position()->set_x(client_rect.left);
composition_form.mutable_current_position()->set_y(client_rect.top);
} else {
composition_form.mutable_current_position()->set_x(client_rect.left);
composition_form.mutable_current_position()->set_y(client_rect.bottom);
}
}
// TODO(yukawa): Stop supporting |deprecated_style()|.
const uint32 style_bits =
(composition_form.has_deprecated_style()
? composition_form.deprecated_style()
: composition_form.style_bits());
// Check the availability of optional fields.
// Note that currently we always use |current_position| field even when
// |style_bits| does not contain CompositionForm::POINT bit.
if (!composition_form.has_current_position() ||
!composition_form.current_position().has_x() ||
!composition_form.current_position().has_y()) {
return false;
}
const HWND root_window_handle =
window_position_->GetRootWindow(target_window_handle);
if (root_window_handle == NULL) {
LOG(ERROR) << "GetRootWindow failed.";
return false;
}
const double scale = GetScalingFactor(root_window_handle);
const bool no_dpi_virtualization = (scale == 1.0);
const CPoint current_pos_in_client_coord(
composition_form.current_position().x(),
composition_form.current_position().y());
CPoint current_pos_in_logical_coord;
if (!ClientPointToScreen(target_window_handle, current_pos_in_client_coord,
&current_pos_in_logical_coord)) {
LOG(ERROR) << "ClientPointToScreen failed.";
return false;
}
CPoint current_pos;
GetPointInPhysicalCoords(target_window_handle,
current_pos_in_logical_coord, &current_pos);
// Check the availability of optional fields.
// Note that some applications may set |CompositionForm::RECT| and other
// style bits like |CompositionForm::POINT| at the same time.
// See b/3200425 for details.
bool use_area_in_composition_form = false;
if (((style_bits & CompositionForm::RECT) == CompositionForm::RECT) &&
composition_form.has_area() &&
composition_form.area().has_left() &&
composition_form.area().has_top() &&
composition_form.area().has_right() &&
composition_form.area().has_bottom()) {
use_area_in_composition_form = true;
}
CRect area_in_client_coord;
if (use_area_in_composition_form) {
area_in_client_coord.SetRect(
composition_form.area().left(),
composition_form.area().top(),
composition_form.area().right(),
composition_form.area().bottom());
} else {
if (window_position_->GetClientRect(
target_window_handle, &area_in_client_coord) == FALSE) {
const int error = ::GetLastError();
DLOG(ERROR) << "GetClientRect failed. error = " << error;
return false;
}
}
CRect area_in_logical_coord;
if (!ClientRectToScreen(target_window_handle, area_in_client_coord,
&area_in_logical_coord)) {
return false;
}
CPoint current_pos_in_physical_coord;
GetPointInPhysicalCoords(
target_window_handle, current_pos_in_logical_coord,
&current_pos_in_physical_coord);
CRect area_in_physical_coord;
GetRectInPhysicalCoords(
target_window_handle, area_in_logical_coord,
&area_in_physical_coord);
// Adjust the font size to be equal to that in the target process with
// taking DPI virtualization into account.
if (!no_dpi_virtualization) {
logfont.lfHeight = static_cast<int>(logfont.lfHeight * scale);
}
// Ensure the escapement and orientation are consistent with writing
// direction. Note that some applications always set 0 to |lfOrientation|.
if (is_vertical) {
logfont.lfEscapement = 2700;
logfont.lfOrientation = 2700;
} else {
logfont.lfEscapement = 0;
logfont.lfOrientation = 0;
}
string preedit_utf8;
vector<int> segment_indices;
vector<CharacterRange> segment_lengths;
const wstring composition_text = ComposePreeditText(
preedit, &preedit_utf8, &segment_indices, &segment_lengths);
DCHECK_EQ(composition_text.size(), segment_indices.size());
DCHECK_EQ(preedit.segment_size(), segment_lengths.size());
vector<mozc::renderer::win32::LineLayout> layouts;
bool result = false;
{
const int offset = is_vertical
? current_pos_in_physical_coord.y - area_in_physical_coord.top
: current_pos_in_physical_coord.x - area_in_physical_coord.left;
const int limit = is_vertical ? area_in_physical_coord.Height()
: area_in_physical_coord.Width();
result = CalcLayoutWithTextWrapping(
logfont, composition_text, limit, offset, &layouts);
}
if (!result) {
LOG(ERROR) << "CalcLayoutWithTextWrapping failed.";
return false;
}
if (composition_window_layouts != NULL) {
composition_window_layouts->clear();
}
int cursor_index = -1;
if (output.has_candidates() && output.candidates().has_position()) {
// |cursor_index| is supposed to be wide character index but
// |output.candidates().position()| is the number of Unicode characters.
// In case surrogate pair appears, use Util::WideCharsLen to calculate the
// cursor position as wide character index. See b/4163234 for details.
cursor_index = Util::WideCharsLen(
Util::SubString(preedit_utf8, 0, output.candidates().position()));
}
const bool is_suggest =
output.candidates().has_category() &&
(output.candidates().category() == commands::SUGGESTION);
// When this flag is true, suggest window must not hide preedit text.
// TODO(yukawa): remove |!is_vertical| when vertical candidate window is
// implemented.
const bool suggest_window_never_hides_preedit = (!is_vertical && is_suggest);
int total_line_offset = 0;
int total_characters = 0;
for (size_t layout_index = 0; layout_index < layouts.size();
++layout_index) {
const mozc::renderer::win32::LineLayout &layout = layouts[layout_index];
if (layout.text.size() < 0 ||
layout.line_length < 0 ||
layout.character_positions.size() < 0) {
// unexpected values found.
return false;
}
if (layout.text.size() == 0 ||
layout.line_length == 0 ||
layout.character_positions.size() == 0) {
// This line is full. Go to next line.
total_line_offset += layout.line_width;
total_characters += layout.text.size();
continue;
}
DCHECK_GT(layout.text.size(), 0);
DCHECK_GT(layout.line_length, 0);
DCHECK_GT(layout.character_positions.size(), 0);
CompositionWindowLayout window_layout;
window_layout.text = layout.text;
window_layout.log_font = logfont;
CRect window_rect;
CRect text_rect;
CPoint base_point;
if (is_vertical) {
window_rect.top = area_in_physical_coord.top + layout.line_start_offset;
window_rect.right = current_pos.x - total_line_offset;
window_rect.left =
window_rect.right - layout.line_width;
window_rect.bottom =
window_rect.top + layout.line_length;
text_rect.SetRect(0, 0, layout.line_width, layout.line_length);
base_point.SetPoint(layout.line_width, 0);
} else {
window_rect.left =
area_in_physical_coord.left + layout.line_start_offset;
window_rect.top = current_pos.y + total_line_offset;
window_rect.right = window_rect.left + layout.line_length;
window_rect.bottom = window_rect.top + layout.line_width;
text_rect.SetRect(0, 0, layout.line_length, layout.line_width);
base_point.SetPoint(0, 0);
}
window_layout.window_position_in_screen_coordinate = window_rect;
window_layout.text_area = text_rect;
window_layout.base_position = base_point;
const int next_total_characters = total_characters + layout.text.size();
// Calculate caret rect assuming its width is 1 pixel.
// Note that |caret_index| is supposed to be wide character index but
// |output.preedit().cursor()| is the number of Unicode characters.
// In case surrogate pair appears, use Util::WideCharsLen to calculate the
// cursor position as wide character index. See b/4163234 for details.
// TODO(yukawa): We should use the actual caret size, which can be
// obtained by GetGUIThreadInfo API.
const int caret_index = Util::WideCharsLen(
Util::SubString(preedit_utf8, 0, output.preedit().cursor()));
if (total_characters <= caret_index &&
caret_index < next_total_characters) {
// In this case, caret points existing character. We use the left edge
// of the pointed character.
const int local_caret_index = caret_index - total_characters;
const int caret_begin =
layout.character_positions[local_caret_index].begin;
// Add 1 because Win32 RECTs are endpoint-exclusive.
// http://weblogs.asp.net/oldnewthing/archive/2004/02/18/75652.aspx
// http://www.radiumsoftware.com/0402.html#040222
const int caret_end = caret_begin + 1;
if (is_vertical) {
window_layout.caret_rect =
CRect(0, caret_begin, layout.line_width, caret_end);
} else {
window_layout.caret_rect =
CRect(caret_begin, 0, caret_end, layout.line_width);
}
} else if (((layout_index + 1) == layouts.size()) &&
(caret_index == next_total_characters)) {
// In this case, caret points the next to the last character.
// The composition window should have an extra space to draw the caret if
// the window can be extended.
CRect extended_rect(window_layout.window_position_in_screen_coordinate);
if (is_vertical) {
if (extended_rect.bottom < area_in_physical_coord.bottom) {
// Still inside of the |area| if we extend the window.
extended_rect.InflateRect(0, 0, 0, 1);
}
const int caret_begin = extended_rect.Height() - 1;
// Add 1 because Win32 RECTs are endpoint-exclusive.
// http://weblogs.asp.net/oldnewthing/archive/2004/02/18/75652.aspx
// http://www.radiumsoftware.com/0402.html#040222
const int caret_end = caret_begin + 1;
window_layout.caret_rect =
CRect(0, caret_begin, layout.line_width, caret_end);
} else {
if (extended_rect.right < area_in_physical_coord.right) {
// Still inside of the |area| if we extend the window.
extended_rect.InflateRect(0, 0, 1, 0);
}
const int caret_begin = extended_rect.Width() - 1;
// Add 1 because Win32 RECTs are endpoint-exclusive.
// http://weblogs.asp.net/oldnewthing/archive/2004/02/18/75652.aspx
// http://www.radiumsoftware.com/0402.html#040222
const int caret_end = caret_begin + 1;
window_layout.caret_rect =
CRect(caret_begin, 0, caret_end, layout.line_width);
}
window_layout.window_position_in_screen_coordinate =
extended_rect;
}
if (total_characters <= cursor_index &&
cursor_index < next_total_characters &&
candidate_layout != NULL &&
!suggest_window_never_hides_preedit) {
const int local_cursor_index = cursor_index - total_characters;
CPoint cursor_pos;
CRect exclusion_area;
if (is_vertical) {
cursor_pos.SetPoint(
0, layout.character_positions[local_cursor_index].begin);
exclusion_area.SetRect(
cursor_pos, CPoint(window_rect.Width(), window_rect.Height()));
} else {
cursor_pos.SetPoint(
layout.character_positions[local_cursor_index].begin,
layout.line_width);
exclusion_area.SetRect(
CPoint(layout.character_positions[local_cursor_index].begin, 0),
CPoint(window_rect.Width(), window_rect.Height()));
}
cursor_pos.Offset(window_rect.left, window_rect.top);
exclusion_area.OffsetRect(window_rect.left, window_rect.top);
candidate_layout->InitializeWithPositionAndExcludeRegion(
cursor_pos, exclusion_area);
}
const size_t min_segment_index = segment_indices[total_characters];
const size_t max_segment_index =
segment_indices[next_total_characters - 1];
for (size_t segment_index = min_segment_index;
segment_index <= max_segment_index; ++segment_index) {
const commands::Preedit::Segment &segment =
preedit.segment(segment_index);
if ((segment.annotation() & commands::Preedit::Segment::UNDERLINE) !=
commands::Preedit::Segment::UNDERLINE &&
(segment.annotation() & commands::Preedit::Segment::HIGHLIGHT) !=
commands::Preedit::Segment::HIGHLIGHT) {
continue;
}
const int segment_begin =
max(segment_lengths[segment_index].begin, total_characters) -
total_characters;
const int segment_end =
min(segment_lengths[segment_index].begin +
segment_lengths[segment_index].length,
next_total_characters) - total_characters;
if (segment_begin >= segment_end) {
continue;
}
DCHECK_GT(segment_end, segment_begin);
bool show_segment_gap = true;
if ((segment_index + 1) >= preedit.segment_size()) {
// If this segment is the last segment, we do not show the gap.
show_segment_gap = false;
} else if (segment_lengths[segment_index].begin +
segment_lengths[segment_index].length !=
segment_end + total_characters) {
// If this segment continues to the next line, we do not show the
// gap. This behavior is different from the composition window
// drawn by CUAS.
show_segment_gap = false;
}
// As CUAS does, we make a gap in underline between segments.
// The length of underline will be shortened 20% of the width of
// the last character.
const int begin_pos =
layout.character_positions[segment_begin].begin;
const int end_pos =
layout.character_positions[segment_end - 1].begin +
80 * layout.character_positions[segment_end - 1].length /
(show_segment_gap ? 100 : 80);
SegmentMarkerLayout marker;
if ((segment.annotation() & commands::Preedit::Segment::HIGHLIGHT) ==
commands::Preedit::Segment::HIGHLIGHT) {
marker.highlighted = true;
}
if (is_vertical) {
// |CPoint(layout.line_width, begin_pos)| is outside of the
// window.
marker.from = CPoint(layout.line_width - 1, begin_pos);
marker.to = CPoint(layout.line_width - 1, end_pos);
} else {
// |CPoint(begin_pos, layout.line_width)| is outside of the
// window.
marker.from = CPoint(begin_pos, layout.line_width - 1);
marker.to = CPoint(end_pos, layout.line_width - 1);
}
window_layout.marker_layouts.push_back(marker);
}
if (composition_window_layouts != NULL) {
composition_window_layouts->push_back(window_layout);
}
total_line_offset += layout.line_width;
total_characters += layout.text.size();
}
// In this case, suggest window moves to the next line of the preedit text
// so that suggest window never hides the preedit.
if (suggest_window_never_hides_preedit &&
(composition_window_layouts->size() > 0) &&
(candidate_layout != NULL)) {
// Initialize the |exclusion_area| with invalid data. These values will
// be updated to be valid at the first turn of the next for-loop.
// For example, |exclusion_area.left| will be updated as follows.
// exclusion_area.left = min(exclusion_area.left,
// numeric_limits<int>::max());
CRect exclusion_area(numeric_limits<int>::max(),
numeric_limits<int>::max(),
numeric_limits<int>::min(),
numeric_limits<int>::min());
for (size_t i = 0; i < composition_window_layouts->size(); ++i) {
const CompositionWindowLayout &layout = composition_window_layouts->at(i);
CRect text_area_in_screen_coord = layout.text_area;
text_area_in_screen_coord.OffsetRect(
layout.window_position_in_screen_coordinate.left,
layout.window_position_in_screen_coordinate.top);
exclusion_area.left = min(exclusion_area.left,
text_area_in_screen_coord.left);
exclusion_area.top = min(exclusion_area.top,
text_area_in_screen_coord.top);
exclusion_area.right = max(exclusion_area.right,
text_area_in_screen_coord.right);
exclusion_area.bottom = max(exclusion_area.bottom,
text_area_in_screen_coord.bottom);
}
CPoint cursor_pos;
if (is_vertical) {
cursor_pos.SetPoint(exclusion_area.left, exclusion_area.top);
} else {
cursor_pos.SetPoint(exclusion_area.left, exclusion_area.bottom);
}
candidate_layout->InitializeWithPositionAndExcludeRegion(
cursor_pos, exclusion_area);
}
return true;
}
bool LayoutManager::ClientPointToScreen(
HWND src_window_handle,
const POINT &src_point,
POINT *dest_point) const {
if (dest_point == NULL) {
return false;
}
if (!window_position_->IsWindow(src_window_handle)) {
DLOG(ERROR) << "Invalid window handle.";
return false;
}
CPoint converted = src_point;
if (window_position_->ClientToScreen(src_window_handle, &converted) ==
FALSE) {
DLOG(ERROR) << "ClientToScreen failed.";
return false;
}
*dest_point = converted;
return true;
}
bool LayoutManager::ClientRectToScreen(
HWND src_window_handle,
const RECT &src_rect,
RECT *dest_rect) const {
if (dest_rect == NULL) {
return false;
}
if (!window_position_->IsWindow(src_window_handle)) {
DLOG(ERROR) << "Invalid window handle.";
return false;
}
CPoint top_left(src_rect.left, src_rect.top);
if (window_position_->ClientToScreen(src_window_handle, &top_left) == FALSE) {
DLOG(ERROR) << "ClientToScreen failed.";
return false;
}
CPoint bottom_right(src_rect.right, src_rect.bottom);
if (window_position_->ClientToScreen(src_window_handle, &bottom_right) ==
FALSE) {
DLOG(ERROR) << "ClientToScreen failed.";
return false;
}
dest_rect->left = top_left.x;
dest_rect->top = top_left.y;
dest_rect->right = bottom_right.x;
dest_rect->bottom = bottom_right.y;
return true;
}
bool LayoutManager::LocalPointToScreen(
HWND src_window_handle,
const POINT &src_point,
POINT *dest_point) const {
if (dest_point == NULL) {
return false;
}
if (!window_position_->IsWindow(src_window_handle)) {
DLOG(ERROR) << "Invalid window handle.";
return false;
}
CRect window_rect;
if (window_position_->GetWindowRect(src_window_handle, &window_rect) ==
FALSE) {
return false;
}
const CPoint offset(window_rect.TopLeft());
dest_point->x = src_point.x + offset.x;
dest_point->y = src_point.y + offset.y;
return true;
}
bool LayoutManager::LocalRectToScreen(
HWND src_window_handle,
const RECT &src_rect,
RECT *dest_rect) const {
if (dest_rect == NULL) {
return false;
}
if (!window_position_->IsWindow(src_window_handle)) {
DLOG(ERROR) << "Invalid window handle.";
return false;
}
CRect window_rect;
if (window_position_->GetWindowRect(src_window_handle, &window_rect) ==
FALSE) {
return false;
}
const CPoint offset(window_rect.TopLeft());
dest_rect->left = src_rect.left + offset.x;
dest_rect->top = src_rect.top + offset.y;
dest_rect->right = src_rect.right + offset.x;
dest_rect->bottom = src_rect.bottom + offset.y;
return true;
}
bool LayoutManager::GetClientRect(
HWND window_handle, RECT *client_rect) const {
return window_position_->GetClientRect(window_handle, client_rect);
}
double LayoutManager::GetScalingFactor(HWND window_handle) const {
const double kDefaultValue = 1.0;
CRect window_rect_in_logical_coord;
if (!window_position_->GetWindowRect(window_handle,
&window_rect_in_logical_coord)) {
return kDefaultValue;
}
CPoint top_left_in_physical_coord;
if (!window_position_->LogicalToPhysicalPoint(
window_handle, window_rect_in_logical_coord.TopLeft(),
&top_left_in_physical_coord)) {
return kDefaultValue;
}
CPoint bottom_right_in_physical_coord;
if (!window_position_->LogicalToPhysicalPoint(
window_handle, window_rect_in_logical_coord.BottomRight(),
&bottom_right_in_physical_coord)) {
return kDefaultValue;
}
const CRect window_rect_in_physical_coord(
top_left_in_physical_coord, bottom_right_in_physical_coord);
if (window_rect_in_physical_coord == window_rect_in_logical_coord) {
// No scaling.
return 1.0;
}
// use larger edge to calculate the scaling factor more accurately.
if (window_rect_in_logical_coord.Width() >
window_rect_in_logical_coord.Height()) {
// Use width.
if (window_rect_in_physical_coord.Width() <= 0 ||
window_rect_in_logical_coord.Width() <= 0) {
return kDefaultValue;
}
DCHECK_NE(0, window_rect_in_logical_coord.Width()) << "divided-by-zero";
return (static_cast<double>(window_rect_in_physical_coord.Width()) /
window_rect_in_logical_coord.Width());
} else {
// Use Height.
if (window_rect_in_physical_coord.Height() <= 0 ||
window_rect_in_logical_coord.Height() <= 0) {
return kDefaultValue;
}
DCHECK_NE(0, window_rect_in_logical_coord.Height()) << "divided-by-zero";
return (static_cast<double>(window_rect_in_physical_coord.Height()) /
window_rect_in_logical_coord.Height());
}
}
bool LayoutManager::GetDefaultGuiFont(LOGFONTW *logfont) const {
return system_preference_->GetDefaultGuiFont(logfont);
}
LayoutManager::WritingDirection LayoutManager::GetWritingDirection(
const commands::RendererCommand_ApplicationInfo &app_info) {
// |escapement| is the angle between the escapement vector and the x-axis
// of the device, in tenths of degrees. In Windows, (Japanese) vertical
// writing is usually implemented by setting 2700 to the |escapement|,
// which means the escapement vector is parallel to |Rot(270 deg) * (1, 0)|
// in the display coordinate. Note that |escapement| and |orientation| are
// different concept. But we only check |escapement| here for application
// compatibility.
// Any |escapement| except for 2700 is treated as horizontal, on the
// strength of IMEINFO::fdwUICaps has only UI_CAP_2700 as for Mozc.
// See the document of LOGFONT structure and ImmGetProperty API for
// details.
// http://msdn.microsoft.com/en-us/library/dd145037.aspx
// http://msdn.microsoft.com/en-us/library/dd318567.aspx
// TODO(yukawa): Support arbitrary angle.
if (!app_info.has_composition_font() ||
!app_info.composition_font().has_escapement()) {
return WRITING_DIRECTION_UNSPECIFIED;
}
if (app_info.composition_font().escapement() == 2700) {
return VERTICAL_WRITING;
}
return HORIZONTAL_WRITING;
}
bool LayoutManager::LayoutCandidateWindowForSuggestion(
const commands::RendererCommand::ApplicationInfo &app_info,
CandidateWindowLayout *candidate_layout) {
const int compatibility_mode = GetCompatibilityMode(app_info);
CandidateWindowLayoutParams params;
if (!ExtractParams(this, compatibility_mode, app_info, &params)) {
return false;
}
if (LayoutCandidateWindowByCompositionTarget(
params, compatibility_mode, this, candidate_layout)) {
DCHECK(candidate_layout->initialized());
return true;
}
if ((compatibility_mode & CAN_USE_CANDIDATE_FORM_FOR_SUGGEST) ==
CAN_USE_CANDIDATE_FORM_FOR_SUGGEST) {
if (LayoutCandidateWindowByCandidateForm(
params, compatibility_mode, this, candidate_layout)) {
DCHECK(candidate_layout->initialized());
return true;
}
}
if (LayoutCandidateWindowByCaretInfo(
params, compatibility_mode, this, candidate_layout)) {
DCHECK(candidate_layout->initialized());
return true;
}
if (LayoutCandidateWindowByCompositionForm(
params, compatibility_mode, this, candidate_layout)) {
DCHECK(candidate_layout->initialized());
return true;
}
if (LayoutCandidateWindowByClientRect(
params, compatibility_mode, this, candidate_layout)) {
DCHECK(candidate_layout->initialized());
return true;
}
return false;
}
bool LayoutManager::LayoutCandidateWindowForConversion(
const commands::RendererCommand::ApplicationInfo &app_info,
CandidateWindowLayout *candidate_layout) {
const int compatibility_mode = GetCompatibilityMode(app_info);
CandidateWindowLayoutParams params;
if (!ExtractParams(this, compatibility_mode, app_info, &params)) {
return false;
}
if (LayoutCandidateWindowByCompositionTarget(
params, compatibility_mode, this, candidate_layout)) {
DCHECK(candidate_layout->initialized());
return true;
}
if (LayoutCandidateWindowByCandidateForm(
params, compatibility_mode, this, candidate_layout)) {
DCHECK(candidate_layout->initialized());
return true;
}
if (LayoutCandidateWindowByCaretInfo(
params, compatibility_mode, this, candidate_layout)) {
DCHECK(candidate_layout->initialized());
return true;
}
if (LayoutCandidateWindowByCompositionForm(
params, compatibility_mode, this, candidate_layout)) {
DCHECK(candidate_layout->initialized());
return true;
}
if (LayoutCandidateWindowByClientRect(
params, compatibility_mode, this, candidate_layout)) {
DCHECK(candidate_layout->initialized());
return true;
}
return false;
}
int LayoutManager::GetCompatibilityMode(
const commands::RendererCommand_ApplicationInfo &app_info) {
if (!app_info.has_target_window_handle()) {
return COMPATIBILITY_MODE_NONE;
}
const HWND target_window = reinterpret_cast<HWND>(
app_info.target_window_handle());
if (!window_position_->IsWindow(target_window)) {
return COMPATIBILITY_MODE_NONE;
}
wstring class_name;
if (!window_position_->GetWindowClassName(target_window, &class_name)) {
return COMPATIBILITY_MODE_NONE;
}
int mode = COMPATIBILITY_MODE_NONE;
{
{
const wchar_t *kUseCandidateFormForSuggest[] = {
L"Chrome_RenderWidgetHostHWND",
L"JsTaroCtrl",
L"OperaWindowClass",
L"QWidget",
};
for (size_t i = 0; i < ARRAYSIZE(kUseCandidateFormForSuggest); ++i) {
if (kUseCandidateFormForSuggest[i] == class_name) {
mode |= CAN_USE_CANDIDATE_FORM_FOR_SUGGEST;
break;
}
}
}
}
{
const wchar_t *kUseLocalCoord[] = {
L"gdkWindowToplevel",
L"SunAwtDialog",
L"SunAwtFrame",
};
for (size_t i = 0; i < ARRAYSIZE(kUseLocalCoord); ++i) {
if (kUseLocalCoord[i] == class_name) {
mode |= USE_LOCAL_COORD_FOR_CANDIDATE_FORM;
break;
}
}
}
{
const wchar_t *kIgnoreDefaultCompositionForm[] = {
L"SunAwtDialog",
L"SunAwtFrame",
};
for (size_t i = 0; i < ARRAYSIZE(kIgnoreDefaultCompositionForm); ++i) {
if (kIgnoreDefaultCompositionForm[i] == class_name) {
mode |= IGNORE_DEFAULT_COMPOSITION_FORM;
break;
}
}
}
{
const wchar_t *kShowInfolistImmediately[] = {
L"Emacs",
L"MEADOW",
};
for (size_t i = 0; i < ARRAYSIZE(kShowInfolistImmediately); ++i) {
if (kShowInfolistImmediately[i] == class_name) {
mode |= SHOW_INFOLIST_IMMEDIATELY;
break;
}
}
}
return mode;
}
bool LayoutManager::LayoutIndicatorWindow(
const commands::RendererCommand_ApplicationInfo &app_info,
IndicatorWindowLayout *indicator_layout) {
if (indicator_layout == nullptr) {
return false;
}
indicator_layout->Clear();
CandidateWindowLayoutParams params;
if (!ExtractParams(this,
GetCompatibilityMode(app_info),
app_info,
&params)) {
return false;
}
CRect target_rect;
if (!GetTargetRectForIndicator(params, *this, &target_rect)) {
return false;
}
indicator_layout->is_vertical = IsVerticalWriting(params);
indicator_layout->window_rect = target_rect;
return true;
}
} // namespace win32
} // namespace renderer
} // namespace mozc