1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ash/ime/candidate_window_view.h"
9 #include "ash/ime/candidate_view.h"
10 #include "ash/ime/candidate_window_constants.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "ui/gfx/color_utils.h"
13 #include "ui/gfx/screen.h"
14 #include "ui/native_theme/native_theme.h"
15 #include "ui/views/background.h"
16 #include "ui/views/border.h"
17 #include "ui/views/bubble/bubble_frame_view.h"
18 #include "ui/views/controls/label.h"
19 #include "ui/views/layout/box_layout.h"
20 #include "ui/views/layout/fill_layout.h"
21 #include "ui/wm/core/window_animations.h"
28 class CandidateWindowBorder
: public views::BubbleBorder
{
30 explicit CandidateWindowBorder(gfx::NativeView parent
)
31 : views::BubbleBorder(views::BubbleBorder::TOP_CENTER
,
32 views::BubbleBorder::NO_SHADOW
,
36 set_paint_arrow(views::BubbleBorder::PAINT_NONE
);
38 virtual ~CandidateWindowBorder() {}
40 void set_offset(int offset
) { offset_
= offset
; }
43 // Overridden from views::BubbleBorder:
44 virtual gfx::Rect
GetBounds(const gfx::Rect
& anchor_rect
,
45 const gfx::Size
& content_size
) const OVERRIDE
{
46 gfx::Rect
bounds(content_size
);
47 bounds
.set_origin(gfx::Point(
48 anchor_rect
.x() - offset_
,
49 is_arrow_on_top(arrow()) ?
50 anchor_rect
.bottom() : anchor_rect
.y() - content_size
.height()));
52 // It cannot use the normal logic of arrow offset for horizontal offscreen,
53 // because the arrow must be in the content's edge. But CandidateWindow has
54 // to be visible even when |anchor_rect| is out of the screen.
55 gfx::Rect work_area
= gfx::Screen::GetNativeScreen()->
56 GetDisplayNearestWindow(parent_
).work_area();
57 if (bounds
.right() > work_area
.right())
58 bounds
.set_x(work_area
.right() - bounds
.width());
59 if (bounds
.x() < work_area
.x())
60 bounds
.set_x(work_area
.x());
65 virtual gfx::Insets
GetInsets() const OVERRIDE
{
69 gfx::NativeView parent_
;
72 DISALLOW_COPY_AND_ASSIGN(CandidateWindowBorder
);
75 // Computes the page index. For instance, if the page size is 9, and the
76 // cursor is pointing to 13th candidate, the page index will be 1 (2nd
77 // page, as the index is zero-origin). Returns -1 on error.
78 int ComputePageIndex(const ui::CandidateWindow
& candidate_window
) {
79 if (candidate_window
.page_size() > 0)
80 return candidate_window
.cursor_position() / candidate_window
.page_size();
86 class InformationTextArea
: public views::View
{
88 // InformationTextArea's border is drawn as a separator, it should appear
89 // at either top or bottom.
95 // Specify the alignment and initialize the control.
96 InformationTextArea(gfx::HorizontalAlignment align
, int min_width
)
97 : min_width_(min_width
) {
98 label_
= new views::Label
;
99 label_
->SetHorizontalAlignment(align
);
100 label_
->SetBorder(views::Border::CreateEmptyBorder(2, 2, 2, 4));
102 SetLayoutManager(new views::FillLayout());
103 AddChildView(label_
);
104 set_background(views::Background::CreateSolidBackground(
105 color_utils::AlphaBlend(SK_ColorBLACK
,
106 GetNativeTheme()->GetSystemColor(
107 ui::NativeTheme::kColorId_WindowBackground
),
111 // Sets the text alignment.
112 void SetAlignment(gfx::HorizontalAlignment alignment
) {
113 label_
->SetHorizontalAlignment(alignment
);
116 // Sets the displayed text.
117 void SetText(const base::string16
& text
) {
118 label_
->SetText(text
);
121 // Sets the border thickness for top/bottom.
122 void SetBorderFromPosition(BorderPosition position
) {
123 SetBorder(views::Border::CreateSolidSidedBorder(
124 (position
== TOP
) ? 1 : 0,
126 (position
== BOTTOM
) ? 1 : 0,
128 GetNativeTheme()->GetSystemColor(
129 ui::NativeTheme::kColorId_MenuBorderColor
)));
133 virtual gfx::Size
GetPreferredSize() const OVERRIDE
{
134 gfx::Size size
= views::View::GetPreferredSize();
135 size
.SetToMax(gfx::Size(min_width_
, 0));
140 views::Label
* label_
;
143 DISALLOW_COPY_AND_ASSIGN(InformationTextArea
);
146 CandidateWindowView::CandidateWindowView(gfx::NativeView parent
)
147 : selected_candidate_index_in_page_(-1),
148 should_show_at_composition_head_(false),
149 should_show_upper_side_(false),
150 was_candidate_window_open_(false) {
151 set_can_activate(false);
152 set_parent_window(parent
);
153 set_margins(gfx::Insets());
155 // Set the background and the border of the view.
156 ui::NativeTheme
* theme
= GetNativeTheme();
158 views::Background::CreateSolidBackground(theme
->GetSystemColor(
159 ui::NativeTheme::kColorId_WindowBackground
)));
160 SetBorder(views::Border::CreateSolidBorder(
161 1, theme
->GetSystemColor(ui::NativeTheme::kColorId_MenuBorderColor
)));
163 SetLayoutManager(new views::BoxLayout(views::BoxLayout::kVertical
, 0, 0, 0));
164 auxiliary_text_
= new InformationTextArea(gfx::ALIGN_RIGHT
, 0);
165 preedit_
= new InformationTextArea(gfx::ALIGN_LEFT
, kMinPreeditAreaWidth
);
166 candidate_area_
= new views::View
;
167 auxiliary_text_
->SetVisible(false);
168 preedit_
->SetVisible(false);
169 candidate_area_
->SetVisible(false);
170 preedit_
->SetBorderFromPosition(InformationTextArea::BOTTOM
);
171 if (candidate_window_
.orientation() == ui::CandidateWindow::VERTICAL
) {
172 AddChildView(preedit_
);
173 AddChildView(candidate_area_
);
174 AddChildView(auxiliary_text_
);
175 auxiliary_text_
->SetBorderFromPosition(InformationTextArea::TOP
);
176 candidate_area_
->SetLayoutManager(new views::BoxLayout(
177 views::BoxLayout::kVertical
, 0, 0, 0));
179 AddChildView(preedit_
);
180 AddChildView(auxiliary_text_
);
181 AddChildView(candidate_area_
);
182 auxiliary_text_
->SetAlignment(gfx::ALIGN_LEFT
);
183 auxiliary_text_
->SetBorderFromPosition(InformationTextArea::BOTTOM
);
184 candidate_area_
->SetLayoutManager(new views::BoxLayout(
185 views::BoxLayout::kHorizontal
, 0, 0, 0));
189 CandidateWindowView::~CandidateWindowView() {
192 views::Widget
* CandidateWindowView::InitWidget() {
193 views::Widget
* widget
= BubbleDelegateView::CreateBubble(this);
195 wm::SetWindowVisibilityAnimationType(
196 widget
->GetNativeView(),
197 wm::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE
);
199 GetBubbleFrameView()->SetBubbleBorder(scoped_ptr
<views::BubbleBorder
>(
200 new CandidateWindowBorder(parent_window())));
204 void CandidateWindowView::UpdateVisibility() {
205 if (candidate_area_
->visible() || auxiliary_text_
->visible() ||
206 preedit_
->visible()) {
209 GetWidget()->Close();
213 void CandidateWindowView::HideLookupTable() {
214 candidate_area_
->SetVisible(false);
215 auxiliary_text_
->SetVisible(false);
219 void CandidateWindowView::HidePreeditText() {
220 preedit_
->SetVisible(false);
224 void CandidateWindowView::ShowPreeditText() {
225 preedit_
->SetVisible(true);
229 void CandidateWindowView::UpdatePreeditText(const base::string16
& text
) {
230 preedit_
->SetText(text
);
233 void CandidateWindowView::ShowLookupTable() {
234 candidate_area_
->SetVisible(true);
235 auxiliary_text_
->SetVisible(candidate_window_
.is_auxiliary_text_visible());
239 void CandidateWindowView::UpdateCandidates(
240 const ui::CandidateWindow
& new_candidate_window
) {
241 // Updating the candidate views is expensive. We'll skip this if possible.
242 if (!candidate_window_
.IsEqual(new_candidate_window
)) {
243 if (candidate_window_
.orientation() != new_candidate_window
.orientation()) {
244 // If the new layout is vertical, the aux text should appear at the
245 // bottom. If horizontal, it should appear between preedit and candidates.
246 if (new_candidate_window
.orientation() == ui::CandidateWindow::VERTICAL
) {
247 ReorderChildView(auxiliary_text_
, -1);
248 auxiliary_text_
->SetAlignment(gfx::ALIGN_RIGHT
);
249 auxiliary_text_
->SetBorderFromPosition(InformationTextArea::TOP
);
250 candidate_area_
->SetLayoutManager(new views::BoxLayout(
251 views::BoxLayout::kVertical
, 0, 0, 0));
253 ReorderChildView(auxiliary_text_
, 1);
254 auxiliary_text_
->SetAlignment(gfx::ALIGN_LEFT
);
255 auxiliary_text_
->SetBorderFromPosition(InformationTextArea::BOTTOM
);
256 candidate_area_
->SetLayoutManager(new views::BoxLayout(
257 views::BoxLayout::kHorizontal
, 0, 0, 0));
261 // Initialize candidate views if necessary.
262 MaybeInitializeCandidateViews(new_candidate_window
);
264 should_show_at_composition_head_
265 = new_candidate_window
.show_window_at_composition();
266 // Compute the index of the current page.
267 const int current_page_index
= ComputePageIndex(new_candidate_window
);
268 if (current_page_index
< 0)
271 // Update the candidates in the current page.
272 const size_t start_from
=
273 current_page_index
* new_candidate_window
.page_size();
275 int max_shortcut_width
= 0;
276 int max_candidate_width
= 0;
277 for (size_t i
= 0; i
< candidate_views_
.size(); ++i
) {
278 const size_t index_in_page
= i
;
279 const size_t candidate_index
= start_from
+ index_in_page
;
280 CandidateView
* candidate_view
= candidate_views_
[index_in_page
];
281 // Set the candidate text.
282 if (candidate_index
< new_candidate_window
.candidates().size()) {
283 const ui::CandidateWindow::Entry
& entry
=
284 new_candidate_window
.candidates()[candidate_index
];
285 candidate_view
->SetEntry(entry
);
286 candidate_view
->SetEnabled(true);
287 candidate_view
->SetInfolistIcon(!entry
.description_title
.empty());
289 // Disable the empty row.
290 candidate_view
->SetEntry(ui::CandidateWindow::Entry());
291 candidate_view
->SetEnabled(false);
292 candidate_view
->SetInfolistIcon(false);
294 if (new_candidate_window
.orientation() == ui::CandidateWindow::VERTICAL
) {
295 int shortcut_width
= 0;
296 int candidate_width
= 0;
297 candidate_views_
[i
]->GetPreferredWidths(
298 &shortcut_width
, &candidate_width
);
299 max_shortcut_width
= std::max(max_shortcut_width
, shortcut_width
);
300 max_candidate_width
= std::max(max_candidate_width
, candidate_width
);
303 if (new_candidate_window
.orientation() == ui::CandidateWindow::VERTICAL
) {
304 for (size_t i
= 0; i
< candidate_views_
.size(); ++i
)
305 candidate_views_
[i
]->SetWidths(max_shortcut_width
, max_candidate_width
);
308 CandidateWindowBorder
* border
= static_cast<CandidateWindowBorder
*>(
309 GetBubbleFrameView()->bubble_border());
310 if (new_candidate_window
.orientation() == ui::CandidateWindow::VERTICAL
)
311 border
->set_offset(max_shortcut_width
);
313 border
->set_offset(0);
315 // Update the current candidate window. We'll use candidate_window_ from here.
316 // Note that SelectCandidateAt() uses candidate_window_.
317 candidate_window_
.CopyFrom(new_candidate_window
);
319 // Select the current candidate in the page.
320 if (candidate_window_
.is_cursor_visible()) {
321 if (candidate_window_
.page_size()) {
322 const int current_candidate_in_page
=
323 candidate_window_
.cursor_position() % candidate_window_
.page_size();
324 SelectCandidateAt(current_candidate_in_page
);
327 // Unselect the currently selected candidate.
328 if (0 <= selected_candidate_index_in_page_
&&
329 static_cast<size_t>(selected_candidate_index_in_page_
) <
330 candidate_views_
.size()) {
331 candidate_views_
[selected_candidate_index_in_page_
]->SetHighlighted(
333 selected_candidate_index_in_page_
= -1;
337 // Updates auxiliary text
338 auxiliary_text_
->SetVisible(candidate_window_
.is_auxiliary_text_visible());
339 auxiliary_text_
->SetText(base::UTF8ToUTF16(
340 candidate_window_
.auxiliary_text()));
343 void CandidateWindowView::SetCursorBounds(const gfx::Rect
& cursor_bounds
,
344 const gfx::Rect
& composition_head
) {
345 if (candidate_window_
.show_window_at_composition())
346 SetAnchorRect(composition_head
);
348 SetAnchorRect(cursor_bounds
);
351 void CandidateWindowView::MaybeInitializeCandidateViews(
352 const ui::CandidateWindow
& candidate_window
) {
353 const ui::CandidateWindow::Orientation orientation
=
354 candidate_window
.orientation();
355 const size_t page_size
= candidate_window
.page_size();
357 // Reset all candidate_views_ when orientation changes.
358 if (orientation
!= candidate_window_
.orientation())
359 STLDeleteElements(&candidate_views_
);
361 while (page_size
< candidate_views_
.size()) {
362 delete candidate_views_
.back();
363 candidate_views_
.pop_back();
365 while (page_size
> candidate_views_
.size()) {
366 CandidateView
* new_candidate
= new CandidateView(this, orientation
);
367 candidate_area_
->AddChildView(new_candidate
);
368 candidate_views_
.push_back(new_candidate
);
372 void CandidateWindowView::SelectCandidateAt(int index_in_page
) {
373 const int current_page_index
= ComputePageIndex(candidate_window_
);
374 if (current_page_index
< 0) {
378 const int cursor_absolute_index
=
379 candidate_window_
.page_size() * current_page_index
+ index_in_page
;
380 // Ignore click on out of range views.
381 if (cursor_absolute_index
< 0 ||
382 candidate_window_
.candidates().size() <=
383 static_cast<size_t>(cursor_absolute_index
)) {
387 // Remember the currently selected candidate index in the current page.
388 selected_candidate_index_in_page_
= index_in_page
;
390 // Select the candidate specified by index_in_page.
391 candidate_views_
[index_in_page
]->SetHighlighted(true);
393 // Update the cursor indexes in the model.
394 candidate_window_
.set_cursor_position(cursor_absolute_index
);
397 void CandidateWindowView::ButtonPressed(views::Button
* sender
,
398 const ui::Event
& event
) {
399 for (size_t i
= 0; i
< candidate_views_
.size(); ++i
) {
400 if (sender
== candidate_views_
[i
]) {
401 FOR_EACH_OBSERVER(Observer
, observers_
, OnCandidateCommitted(i
));