1 // Copyright (c) 2012 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.
4 #include "chrome/browser/chromeos/input_method/candidate_window_view.h"
5
6 #include <string>
7
8 #include "ash/shell.h"
9 #include "base/strings/utf_string_conversions.h"
10 #include "chrome/browser/chromeos/input_method/candidate_view.h"
11 #include "chrome/browser/chromeos/input_method/candidate_window_constants.h"
12 #include "chrome/browser/chromeos/input_method/hidable_area.h"
13 #include "chromeos/ime/candidate_window.h"
14 #include "ui/gfx/color_utils.h"
15 #include "ui/native_theme/native_theme.h"
16 #include "ui/views/background.h"
17 #include "ui/views/border.h"
18 #include "ui/views/controls/label.h"
19 #include "ui/views/layout/grid_layout.h"
20 #include "ui/views/widget/widget.h"
21
22 namespace chromeos {
23 namespace input_method {
24
25 namespace {
26 // VerticalCandidateLabel is used for rendering candidate text in
27 // the vertical candidate window.
28 class VerticalCandidateLabel : public views::Label {
29 public:
VerticalCandidateLabel()30 VerticalCandidateLabel() {}
31
32 private:
~VerticalCandidateLabel()33 virtual ~VerticalCandidateLabel() {}
34
35 // Returns the preferred size, but guarantees that the width has at
36 // least kMinCandidateLabelWidth pixels.
GetPreferredSize()37 virtual gfx::Size GetPreferredSize() OVERRIDE {
38 gfx::Size size = Label::GetPreferredSize();
39 // Hack. +2 is needed to prevent labels from getting elided like
40 // "abc..." in some cases. TODO(satorux): Figure out why it's
41 // necessary.
42 size.set_width(size.width() + 2);
43 if (size.width() < kMinCandidateLabelWidth) {
44 size.set_width(kMinCandidateLabelWidth);
45 }
46 if (size.width() > kMaxCandidateLabelWidth) {
47 size.set_width(kMaxCandidateLabelWidth);
48 }
49 return size;
50 }
51
52 DISALLOW_COPY_AND_ASSIGN(VerticalCandidateLabel);
53 };
54
55 // Wraps the given view with some padding, and returns it.
WrapWithPadding(views::View * view,const gfx::Insets & insets)56 views::View* WrapWithPadding(views::View* view, const gfx::Insets& insets) {
57 views::View* wrapper = new views::View;
58 // Use GridLayout to give some insets inside.
59 views::GridLayout* layout = new views::GridLayout(wrapper);
60 wrapper->SetLayoutManager(layout); // |wrapper| owns |layout|.
61 layout->SetInsets(insets);
62
63 views::ColumnSet* column_set = layout->AddColumnSet(0);
64 column_set->AddColumn(
65 views::GridLayout::FILL, views::GridLayout::FILL,
66 1, views::GridLayout::USE_PREF, 0, 0);
67 layout->StartRow(0, 0);
68
69 // Add the view contents.
70 layout->AddView(view); // |view| is owned by |wraper|, not |layout|.
71 return wrapper;
72 }
73
74 // Creates shortcut text from the given index and the orientation.
CreateShortcutText(size_t index,const CandidateWindow & candidate_window)75 base::string16 CreateShortcutText(size_t index,
76 const CandidateWindow& candidate_window) {
77 if (index >= candidate_window.candidates().size())
78 return UTF8ToUTF16("");
79 std::string shortcut_text = candidate_window.candidates()[index].label;
80 if (!shortcut_text.empty() &&
81 candidate_window.orientation() != CandidateWindow::VERTICAL)
82 shortcut_text += '.';
83 return UTF8ToUTF16(shortcut_text);
84 }
85
86 // Creates the shortcut label, and returns it (never returns NULL).
87 // The label text is not set in this function.
CreateShortcutLabel(CandidateWindow::Orientation orientation,const ui::NativeTheme & theme)88 views::Label* CreateShortcutLabel(
89 CandidateWindow::Orientation orientation, const ui::NativeTheme& theme) {
90 // Create the shortcut label. The label will be owned by
91 // |wrapped_shortcut_label|, hence it's deleted when
92 // |wrapped_shortcut_label| is deleted.
93 views::Label* shortcut_label = new views::Label;
94
95 if (orientation == CandidateWindow::VERTICAL) {
96 shortcut_label->SetFont(
97 shortcut_label->font().DeriveFont(kFontSizeDelta, gfx::Font::BOLD));
98 } else {
99 shortcut_label->SetFont(
100 shortcut_label->font().DeriveFont(kFontSizeDelta));
101 }
102 // TODO(satorux): Maybe we need to use language specific fonts for
103 // candidate_label, like Chinese font for Chinese input method?
104 shortcut_label->SetEnabledColor(theme.GetSystemColor(
105 ui::NativeTheme::kColorId_LabelEnabledColor));
106 shortcut_label->SetDisabledColor(theme.GetSystemColor(
107 ui::NativeTheme::kColorId_LabelDisabledColor));
108
109 return shortcut_label;
110 }
111
112 // Wraps the shortcut label, then decorates wrapped shortcut label
113 // and returns it (never returns NULL).
114 // The label text is not set in this function.
CreateWrappedShortcutLabel(views::Label * shortcut_label,CandidateWindow::Orientation orientation,const ui::NativeTheme & theme)115 views::View* CreateWrappedShortcutLabel(
116 views::Label* shortcut_label,
117 CandidateWindow::Orientation orientation,
118 const ui::NativeTheme& theme) {
119 // Wrap it with padding.
120 const gfx::Insets kVerticalShortcutLabelInsets(1, 6, 1, 6);
121 const gfx::Insets kHorizontalShortcutLabelInsets(1, 3, 1, 0);
122 const gfx::Insets insets =
123 (orientation == CandidateWindow::VERTICAL ?
124 kVerticalShortcutLabelInsets :
125 kHorizontalShortcutLabelInsets);
126 views::View* wrapped_shortcut_label =
127 WrapWithPadding(shortcut_label, insets);
128
129 // Add decoration based on the orientation.
130 if (orientation == CandidateWindow::VERTICAL) {
131 // Set the background color.
132 SkColor blackish = color_utils::AlphaBlend(
133 SK_ColorBLACK,
134 theme.GetSystemColor(ui::NativeTheme::kColorId_WindowBackground),
135 0x40);
136 SkColor transparent_blakish = color_utils::AlphaBlend(
137 SK_ColorTRANSPARENT, blackish, 0xE0);
138 wrapped_shortcut_label->set_background(
139 views::Background::CreateSolidBackground(transparent_blakish));
140 shortcut_label->SetBackgroundColor(
141 wrapped_shortcut_label->background()->get_color());
142 }
143
144 return wrapped_shortcut_label;
145 }
146
147 // Creates the candidate label, and returns it (never returns NULL).
148 // The label text is not set in this function.
CreateCandidateLabel(CandidateWindow::Orientation orientation)149 views::Label* CreateCandidateLabel(
150 CandidateWindow::Orientation orientation) {
151 views::Label* candidate_label = NULL;
152
153 // Create the candidate label. The label will be added to |this| as a
154 // child view, hence it's deleted when |this| is deleted.
155 if (orientation == CandidateWindow::VERTICAL) {
156 candidate_label = new VerticalCandidateLabel;
157 } else {
158 candidate_label = new views::Label;
159 }
160
161 // Change the font size.
162 candidate_label->SetFont(
163 candidate_label->font().DeriveFont(kFontSizeDelta));
164 candidate_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
165
166 return candidate_label;
167 }
168
169 // Creates the annotation label, and return it (never returns NULL).
170 // The label text is not set in this function.
CreateAnnotationLabel(CandidateWindow::Orientation orientation,const ui::NativeTheme & theme)171 views::Label* CreateAnnotationLabel(
172 CandidateWindow::Orientation orientation, const ui::NativeTheme& theme) {
173 // Create the annotation label.
174 views::Label* annotation_label = new views::Label;
175
176 // Change the font size and color.
177 annotation_label->SetFont(
178 annotation_label->font().DeriveFont(kFontSizeDelta));
179 annotation_label->SetEnabledColor(theme.GetSystemColor(
180 ui::NativeTheme::kColorId_LabelDisabledColor));
181 annotation_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
182
183 return annotation_label;
184 }
185
186 // Computes shortcut column size.
ComputeShortcutColumnSize(const CandidateWindow & candidate_window,const ui::NativeTheme & theme)187 gfx::Size ComputeShortcutColumnSize(
188 const CandidateWindow& candidate_window,
189 const ui::NativeTheme& theme) {
190 int shortcut_column_width = 0;
191 int shortcut_column_height = 0;
192 // Create the shortcut label. The label will be owned by
193 // |wrapped_shortcut_label|, hence it's deleted when
194 // |wrapped_shortcut_label| is deleted.
195 views::Label* shortcut_label = CreateShortcutLabel(
196 candidate_window.orientation(), theme);
197 scoped_ptr<views::View> wrapped_shortcut_label(
198 CreateWrappedShortcutLabel(shortcut_label,
199 candidate_window.orientation(),
200 theme));
201
202 // Compute the max width and height in shortcut labels.
203 // We'll create temporary shortcut labels, and choose the largest width and
204 // height.
205 for (size_t i = 0; i < candidate_window.page_size(); ++i) {
206 shortcut_label->SetText(CreateShortcutText(i, candidate_window));
207 gfx::Size text_size = wrapped_shortcut_label->GetPreferredSize();
208 shortcut_column_width = std::max(shortcut_column_width, text_size.width());
209 shortcut_column_height = std::max(shortcut_column_height,
210 text_size.height());
211 }
212
213 return gfx::Size(shortcut_column_width, shortcut_column_height);
214 }
215
216 // Computes the page index. For instance, if the page size is 9, and the
217 // cursor is pointing to 13th candidate, the page index will be 1 (2nd
218 // page, as the index is zero-origin). Returns -1 on error.
ComputePageIndex(const CandidateWindow & candidate_window)219 int ComputePageIndex(const CandidateWindow& candidate_window) {
220 if (candidate_window.page_size() > 0)
221 return candidate_window.cursor_position() / candidate_window.page_size();
222 return -1;
223 }
224
225 // Computes candidate column size.
ComputeCandidateColumnSize(const CandidateWindow & candidate_window)226 gfx::Size ComputeCandidateColumnSize(
227 const CandidateWindow& candidate_window) {
228 int candidate_column_width = 0;
229 int candidate_column_height = 0;
230 scoped_ptr<views::Label> candidate_label(
231 CreateCandidateLabel(candidate_window.orientation()));
232
233 // Compute the start index of |candidate_window_|.
234 const int current_page_index = ComputePageIndex(candidate_window);
235 if (current_page_index < 0)
236 return gfx::Size(0, 0);
237 const size_t start_from = current_page_index * candidate_window.page_size();
238
239 // Compute the max width and height in candidate labels.
240 // We'll create temporary candidate labels, and choose the largest width and
241 // height.
242 for (size_t i = 0;
243 i + start_from < candidate_window.candidates().size();
244 ++i) {
245 const size_t index = start_from + i;
246
247 candidate_label->SetText(
248 UTF8ToUTF16(candidate_window.candidates()[index].value));
249 gfx::Size text_size = candidate_label->GetPreferredSize();
250 candidate_column_width = std::max(candidate_column_width,
251 text_size.width());
252 candidate_column_height = std::max(candidate_column_height,
253 text_size.height());
254 }
255
256 return gfx::Size(candidate_column_width, candidate_column_height);
257 }
258
259 // Computes annotation column size.
ComputeAnnotationColumnSize(const CandidateWindow & candidate_window,const ui::NativeTheme & theme)260 gfx::Size ComputeAnnotationColumnSize(
261 const CandidateWindow& candidate_window, const ui::NativeTheme& theme) {
262 int annotation_column_width = 0;
263 int annotation_column_height = 0;
264 scoped_ptr<views::Label> annotation_label(
265 CreateAnnotationLabel(candidate_window.orientation(), theme));
266
267 // Compute the start index of |candidate_window_|.
268 const int current_page_index = ComputePageIndex(candidate_window);
269 if (current_page_index < 0)
270 return gfx::Size(0, 0);
271 const size_t start_from = current_page_index * candidate_window.page_size();
272
273 // Compute max width and height in annotation labels.
274 // We'll create temporary annotation labels, and choose the largest width and
275 // height.
276 for (size_t i = 0;
277 i + start_from < candidate_window.candidates().size();
278 ++i) {
279 const size_t index = start_from + i;
280
281 annotation_label->SetText(
282 UTF8ToUTF16(candidate_window.candidates()[index].annotation));
283 gfx::Size text_size = annotation_label->GetPreferredSize();
284 annotation_column_width = std::max(annotation_column_width,
285 text_size.width());
286 annotation_column_height = std::max(annotation_column_height,
287 text_size.height());
288 }
289
290 return gfx::Size(annotation_column_width, annotation_column_height);
291 }
292
293 } // namespace
294
295 // InformationTextArea is a HidableArea having a single Label in it.
296 class InformationTextArea : public HidableArea {
297 public:
298 // Specify the alignment and initialize the control.
InformationTextArea(gfx::HorizontalAlignment align,int minWidth)299 InformationTextArea(gfx::HorizontalAlignment align, int minWidth)
300 : minWidth_(minWidth) {
301 label_ = new views::Label;
302 label_->SetHorizontalAlignment(align);
303
304 const gfx::Insets kInsets(2, 2, 2, 4);
305 views::View* contents = WrapWithPadding(label_, kInsets);
306 SetContents(contents);
307 contents->set_border(views::Border::CreateSolidBorder(
308 1,
309 GetNativeTheme()->GetSystemColor(
310 ui::NativeTheme::kColorId_MenuBorderColor)));
311 contents->set_background(views::Background::CreateSolidBackground(
312 color_utils::AlphaBlend(SK_ColorBLACK,
313 GetNativeTheme()->GetSystemColor(
314 ui::NativeTheme::kColorId_WindowBackground),
315 0x10)));
316 label_->SetBackgroundColor(contents->background()->get_color());
317 }
318
319 // Set the displayed text.
SetText(const std::string & utf8_text)320 void SetText(const std::string& utf8_text) {
321 label_->SetText(UTF8ToUTF16(utf8_text));
322 }
323
324 protected:
GetPreferredSize()325 virtual gfx::Size GetPreferredSize() OVERRIDE {
326 gfx::Size size = HidableArea::GetPreferredSize();
327 // Hack. +2 is needed as the same reason as in VerticalCandidateLabel
328 size.set_width(size.width() + 2);
329 if (size.width() < minWidth_) {
330 size.set_width(minWidth_);
331 }
332 return size;
333 }
334
335 private:
336 views::Label* label_;
337 int minWidth_;
338
339 DISALLOW_COPY_AND_ASSIGN(InformationTextArea);
340 };
341
CandidateView(CandidateWindowView * parent_candidate_window,int index_in_page,CandidateWindow::Orientation orientation)342 CandidateView::CandidateView(
343 CandidateWindowView* parent_candidate_window,
344 int index_in_page,
345 CandidateWindow::Orientation orientation)
346 : index_in_page_(index_in_page),
347 orientation_(orientation),
348 parent_candidate_window_(parent_candidate_window),
349 shortcut_label_(NULL),
350 candidate_label_(NULL),
351 annotation_label_(NULL),
352 infolist_icon_(NULL),
353 infolist_icon_enabled_(false) {
354 }
355
Init(int shortcut_column_width,int candidate_column_width,int annotation_column_width,int column_height)356 void CandidateView::Init(int shortcut_column_width,
357 int candidate_column_width,
358 int annotation_column_width,
359 int column_height) {
360 views::GridLayout* layout = new views::GridLayout(this);
361 SetLayoutManager(layout); // |this| owns |layout|.
362
363 // Create Labels.
364 const ui::NativeTheme& theme = *GetNativeTheme();
365 shortcut_label_ = CreateShortcutLabel(orientation_, theme);
366 views::View* wrapped_shortcut_label =
367 CreateWrappedShortcutLabel(shortcut_label_, orientation_, theme);
368 candidate_label_ = CreateCandidateLabel(orientation_);
369 annotation_label_ = CreateAnnotationLabel(orientation_, theme);
370
371 // Initialize the column set with three columns.
372 views::ColumnSet* column_set = layout->AddColumnSet(0);
373
374 // If orientation is vertical, each column width is fixed.
375 // Otherwise the width is resizable.
376 const views::GridLayout::SizeType column_type =
377 orientation_ == CandidateWindow::VERTICAL ?
378 views::GridLayout::FIXED : views::GridLayout::USE_PREF;
379
380 const int padding_column_width =
381 orientation_ == CandidateWindow::VERTICAL ? 4 : 6;
382
383 // Set shortcut column type and width.
384 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
385 0, column_type, shortcut_column_width, 0);
386 column_set->AddPaddingColumn(0, padding_column_width);
387
388 // Set candidate column type and width.
389 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
390 1, views::GridLayout::USE_PREF, 0,
391 orientation_ == CandidateWindow::VERTICAL ?
392 candidate_column_width : 0);
393 column_set->AddPaddingColumn(0, padding_column_width);
394
395 // Set annotation column type and width.
396 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
397 0, column_type, annotation_column_width, 0);
398
399 if (orientation_ == CandidateWindow::VERTICAL) {
400 column_set->AddPaddingColumn(0, 1);
401 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 0,
402 views::GridLayout::FIXED, kInfolistIndicatorIconWidth,
403 0);
404 column_set->AddPaddingColumn(0, 2);
405 } else {
406 column_set->AddPaddingColumn(0, padding_column_width);
407 }
408
409 // Add the shortcut label, the candidate label, and annotation label.
410 layout->StartRow(0, 0);
411 // |wrapped_shortcut_label|, |candidate_label_|, and |annotation_label_|
412 // will be owned by |this|.
413 layout->AddView(wrapped_shortcut_label,
414 1, // Column span.
415 1, // Row span.
416 views::GridLayout::FILL, // Horizontal alignment.
417 views::GridLayout::FILL, // Vertical alignment.
418 -1, // Preferred width, not specified.
419 column_height); // Preferred height.
420 layout->AddView(candidate_label_,
421 1, // Column span.
422 1, // Row span.
423 views::GridLayout::FILL, // Horizontal alignment.
424 views::GridLayout::FILL, // Vertical alignment.
425 -1, // Preferred width, not specified.
426 column_height); // Preferred height.
427 layout->AddView(annotation_label_,
428 1, // Column span.
429 1, // Row span.
430 views::GridLayout::FILL, // Horizontal alignment.
431 views::GridLayout::FILL, // Vertical alignemnt.
432 -1, // Preferred width, not specified.
433 column_height); // Preferred height.
434 if (orientation_ == CandidateWindow::VERTICAL) {
435 infolist_icon_ = new views::View;
436 views::View* infolist_icon_wrapper = new views::View;
437 views::GridLayout* infolist_icon_layout =
438 new views::GridLayout(infolist_icon_wrapper);
439 // |infolist_icon_layout| is owned by |infolist_icon_wrapper|.
440 infolist_icon_wrapper->SetLayoutManager(infolist_icon_layout);
441 infolist_icon_layout->AddColumnSet(0)->AddColumn(
442 views::GridLayout::FILL, views::GridLayout::FILL,
443 0, views::GridLayout::FIXED, kInfolistIndicatorIconWidth, 0);
444 infolist_icon_layout->AddPaddingRow(0, kInfolistIndicatorIconPadding);
445 infolist_icon_layout->StartRow(1.0, 0); // infolist_icon_ is resizable.
446 // |infolist_icon_| is owned by |infolist_icon_wrapper|.
447 infolist_icon_layout->AddView(infolist_icon_);
448 infolist_icon_layout->AddPaddingRow(0, kInfolistIndicatorIconPadding);
449 // |infolist_icon_wrapper| is owned by |this|.
450 layout->AddView(infolist_icon_wrapper);
451 }
452 UpdateLabelBackgroundColors();
453 }
454
SetCandidateText(const base::string16 & text)455 void CandidateView::SetCandidateText(const base::string16& text) {
456 candidate_label_->SetText(text);
457 }
458
SetShortcutText(const base::string16 & text)459 void CandidateView::SetShortcutText(const base::string16& text) {
460 shortcut_label_->SetText(text);
461 }
462
SetAnnotationText(const base::string16 & text)463 void CandidateView::SetAnnotationText(const base::string16& text) {
464 annotation_label_->SetText(text);
465 }
466
SetInfolistIcon(bool enable)467 void CandidateView::SetInfolistIcon(bool enable) {
468 if (!infolist_icon_ || (infolist_icon_enabled_ == enable))
469 return;
470 infolist_icon_enabled_ = enable;
471 infolist_icon_->set_background(
472 enable ?
473 views::Background::CreateSolidBackground(GetNativeTheme()->GetSystemColor(
474 ui::NativeTheme::kColorId_FocusedBorderColor)) :
475 NULL);
476 UpdateLabelBackgroundColors();
477 SchedulePaint();
478 }
479
Select()480 void CandidateView::Select() {
481 set_background(
482 views::Background::CreateSolidBackground(GetNativeTheme()->GetSystemColor(
483 ui::NativeTheme::kColorId_TextfieldSelectionBackgroundFocused)));
484 set_border(views::Border::CreateSolidBorder(
485 1, GetNativeTheme()->GetSystemColor(
486 ui::NativeTheme::kColorId_FocusedBorderColor)));
487 UpdateLabelBackgroundColors();
488 // Need to call SchedulePaint() for background and border color changes.
489 SchedulePaint();
490 }
491
Unselect()492 void CandidateView::Unselect() {
493 set_background(NULL);
494 set_border(NULL);
495 UpdateLabelBackgroundColors();
496 SchedulePaint(); // See comments at Select().
497 }
498
SetRowEnabled(bool enabled)499 void CandidateView::SetRowEnabled(bool enabled) {
500 shortcut_label_->SetEnabled(enabled);
501 }
502
GetCandidateLabelPosition() const503 gfx::Point CandidateView::GetCandidateLabelPosition() const {
504 return candidate_label_->GetMirroredPosition();
505 }
506
OnMousePressed(const ui::MouseEvent & event)507 bool CandidateView::OnMousePressed(const ui::MouseEvent& event) {
508 // TODO(kinaba): On Windows and MacOS, candidate windows typically commits a
509 // candidate at OnMouseReleased event. We have chosen OnMousePressed here for
510 // working around several obstacle rising from views implementation over GTK.
511 // See: http://crosbug.com/11423#c11. Since we have moved from GTK to Aura,
512 // the reasoning should have became obsolete. We might want to reconsider
513 // implementing mouse-up selection.
514 SelectCandidateAt(event.location());
515 return false;
516 }
517
OnGestureEvent(ui::GestureEvent * event)518 void CandidateView::OnGestureEvent(ui::GestureEvent* event) {
519 if (event->type() == ui::ET_GESTURE_TAP) {
520 SelectCandidateAt(event->location());
521 event->SetHandled();
522 return;
523 }
524 View::OnGestureEvent(event);
525 }
526
SelectCandidateAt(const gfx::Point & location)527 void CandidateView::SelectCandidateAt(const gfx::Point& location) {
528 gfx::Point location_in_candidate_window = location;
529 views::View::ConvertPointToTarget(this, parent_candidate_window_,
530 &location_in_candidate_window);
531 parent_candidate_window_->OnCandidatePressed(location_in_candidate_window);
532 parent_candidate_window_->CommitCandidate();
533 }
534
UpdateLabelBackgroundColors()535 void CandidateView::UpdateLabelBackgroundColors() {
536 SkColor color = background() ?
537 background()->get_color() :
538 GetNativeTheme()->GetSystemColor(
539 ui::NativeTheme::kColorId_WindowBackground);
540 if (orientation_ != CandidateWindow::VERTICAL)
541 shortcut_label_->SetBackgroundColor(color);
542 candidate_label_->SetBackgroundColor(color);
543 annotation_label_->SetBackgroundColor(color);
544 }
545
CandidateWindowView(views::Widget * parent_frame)546 CandidateWindowView::CandidateWindowView(views::Widget* parent_frame)
547 : selected_candidate_index_in_page_(-1),
548 parent_frame_(parent_frame),
549 preedit_area_(NULL),
550 header_area_(NULL),
551 candidate_area_(NULL),
552 footer_area_(NULL),
553 previous_shortcut_column_size_(0, 0),
554 previous_candidate_column_size_(0, 0),
555 previous_annotation_column_size_(0, 0),
556 should_show_at_composition_head_(false),
557 should_show_upper_side_(false),
558 was_candidate_window_open_(false) {
559 }
560
~CandidateWindowView()561 CandidateWindowView::~CandidateWindowView() {
562 }
563
Init()564 void CandidateWindowView::Init() {
565 // Set the background and the border of the view.
566 set_background(
567 views::Background::CreateSolidBackground(GetNativeTheme()->GetSystemColor(
568 ui::NativeTheme::kColorId_WindowBackground)));
569 set_border(views::Border::CreateSolidBorder(
570 1, GetNativeTheme()->GetSystemColor(
571 ui::NativeTheme::kColorId_MenuBorderColor)));
572
573 // Create areas.
574 preedit_area_ = new InformationTextArea(gfx::ALIGN_LEFT,
575 kMinPreeditAreaWidth);
576 header_area_ = new InformationTextArea(gfx::ALIGN_LEFT, 0);
577 candidate_area_ = new HidableArea;
578 candidate_area_->SetContents(new views::View);
579 footer_area_ = new InformationTextArea(gfx::ALIGN_RIGHT, 0);
580
581 // Set the window layout of the view
582 views::GridLayout* layout = new views::GridLayout(this);
583 SetLayoutManager(layout); // |this| owns |layout|.
584 views::ColumnSet* column_set = layout->AddColumnSet(0);
585 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
586 0, views::GridLayout::USE_PREF, 0, 0);
587
588 // Add the preedit area
589 layout->StartRow(0, 0);
590 layout->AddView(preedit_area_); // |preedit_area_| is owned by |this|.
591
592 // Add the header area.
593 layout->StartRow(0, 0);
594 layout->AddView(header_area_); // |header_area_| is owned by |this|.
595
596 // Add the candidate area.
597 layout->StartRow(0, 0);
598 layout->AddView(candidate_area_); // |candidate_area_| is owned by |this|.
599
600 // Add the footer area.
601 layout->StartRow(0, 0);
602 layout->AddView(footer_area_); // |footer_area_| is owned by |this|.
603 }
604
HideAll()605 void CandidateWindowView::HideAll() {
606 parent_frame_->Hide();
607 NotifyIfCandidateWindowOpenedOrClosed();
608 }
609
UpdateParentArea()610 void CandidateWindowView::UpdateParentArea() {
611 if (candidate_area_->IsShown() ||
612 header_area_->IsShown() ||
613 footer_area_->IsShown() ||
614 preedit_area_->IsShown()) {
615 ResizeAndMoveParentFrame();
616 parent_frame_->Show();
617 } else {
618 parent_frame_->Hide();
619 }
620 NotifyIfCandidateWindowOpenedOrClosed();
621 }
622
HideLookupTable()623 void CandidateWindowView::HideLookupTable() {
624 candidate_area_->Hide();
625 UpdateParentArea();
626 }
627
HideAuxiliaryText()628 void CandidateWindowView::HideAuxiliaryText() {
629 header_area_->Hide();
630 footer_area_->Hide();
631 UpdateParentArea();
632 }
633
ShowAuxiliaryText()634 void CandidateWindowView::ShowAuxiliaryText() {
635 // If candidate_area is not shown, shows auxiliary text at header_area.
636 // We expect both header_area_ and footer_area_ contain same value.
637 if (!candidate_area_->IsShown()) {
638 header_area_->Show();
639 footer_area_->Hide();
640 } else {
641 // If candidate_area is shown, shows auxiliary text with orientation.
642 if (candidate_window_.orientation() == CandidateWindow::HORIZONTAL) {
643 header_area_->Show();
644 footer_area_->Hide();
645 } else {
646 footer_area_->Show();
647 header_area_->Hide();
648 }
649 }
650 UpdateParentArea();
651 }
652
UpdateAuxiliaryText(const std::string & utf8_text)653 void CandidateWindowView::UpdateAuxiliaryText(const std::string& utf8_text) {
654 header_area_->SetText(utf8_text);
655 footer_area_->SetText(utf8_text);
656 ShowAuxiliaryText();
657 }
658
HidePreeditText()659 void CandidateWindowView::HidePreeditText() {
660 preedit_area_->Hide();
661 UpdateParentArea();
662 }
663
ShowPreeditText()664 void CandidateWindowView::ShowPreeditText() {
665 preedit_area_->Show();
666 UpdateParentArea();
667 }
668
UpdatePreeditText(const std::string & utf8_text)669 void CandidateWindowView::UpdatePreeditText(const std::string& utf8_text) {
670 preedit_area_->SetText(utf8_text);
671 }
672
ShowLookupTable()673 void CandidateWindowView::ShowLookupTable() {
674 if (!candidate_area_->IsShown())
675 should_show_upper_side_ = false;
676 candidate_area_->Show();
677 UpdateParentArea();
678 }
679
NotifyIfCandidateWindowOpenedOrClosed()680 void CandidateWindowView::NotifyIfCandidateWindowOpenedOrClosed() {
681 bool is_open = IsCandidateWindowOpen();
682 if (!was_candidate_window_open_ && is_open) {
683 FOR_EACH_OBSERVER(Observer, observers_, OnCandidateWindowOpened());
684 } else if (was_candidate_window_open_ && !is_open) {
685 FOR_EACH_OBSERVER(Observer, observers_, OnCandidateWindowClosed());
686 }
687 was_candidate_window_open_ = is_open;
688 }
689
ShouldUpdateCandidateViews(const CandidateWindow & old_candidate_window,const CandidateWindow & new_candidate_window)690 bool CandidateWindowView::ShouldUpdateCandidateViews(
691 const CandidateWindow& old_candidate_window,
692 const CandidateWindow& new_candidate_window) {
693 return !old_candidate_window.IsEqual(new_candidate_window);
694 }
695
UpdateCandidates(const CandidateWindow & new_candidate_window)696 void CandidateWindowView::UpdateCandidates(
697 const CandidateWindow& new_candidate_window) {
698 const bool should_update = ShouldUpdateCandidateViews(candidate_window_,
699 new_candidate_window);
700 // Updating the candidate views is expensive. We'll skip this if possible.
701 if (should_update) {
702 // Initialize candidate views if necessary.
703 MaybeInitializeCandidateViews(new_candidate_window);
704
705 should_show_at_composition_head_
706 = new_candidate_window.show_window_at_composition();
707 // Compute the index of the current page.
708 const int current_page_index = ComputePageIndex(new_candidate_window);
709 if (current_page_index < 0) {
710 return;
711 }
712
713 // Update the candidates in the current page.
714 const size_t start_from =
715 current_page_index * new_candidate_window.page_size();
716
717 // In some cases, engines send empty shortcut labels. For instance,
718 // ibus-mozc sends empty labels when they show suggestions. In this
719 // case, we should not show shortcut labels.
720 bool no_shortcut_mode = true;
721 for (size_t i = 0; i < new_candidate_window.candidates().size(); ++i) {
722 if (!new_candidate_window.candidates()[i].label.empty()) {
723 no_shortcut_mode = false;
724 break;
725 }
726 }
727
728 for (size_t i = 0; i < candidate_views_.size(); ++i) {
729 const size_t index_in_page = i;
730 const size_t candidate_index = start_from + index_in_page;
731 CandidateView* candidate_view = candidate_views_[index_in_page];
732 // Set the shortcut text.
733 if (no_shortcut_mode) {
734 candidate_view->SetShortcutText(base::string16());
735 } else {
736 // At this moment, we don't use labels sent from engines for UX
737 // reasons. First, we want to show shortcut labels in empty rows
738 // (ex. show 6, 7, 8, ... in empty rows when the number of
739 // candidates is 5). Second, we want to add a period after each
740 // shortcut label when the candidate window is horizontal.
741 candidate_view->SetShortcutText(
742 CreateShortcutText(i, new_candidate_window));
743 }
744 // Set the candidate text.
745 if (candidate_index < new_candidate_window.candidates().size()) {
746 const CandidateWindow::Entry& entry =
747 new_candidate_window.candidates()[candidate_index];
748 candidate_view->SetCandidateText(UTF8ToUTF16(entry.value));
749 candidate_view->SetAnnotationText(UTF8ToUTF16(entry.annotation));
750 candidate_view->SetRowEnabled(true);
751 candidate_view->SetInfolistIcon(!entry.description_title.empty());
752 } else {
753 // Disable the empty row.
754 candidate_view->SetCandidateText(base::string16());
755 candidate_view->SetAnnotationText(base::string16());
756 candidate_view->SetRowEnabled(false);
757 candidate_view->SetInfolistIcon(false);
758 }
759 }
760 }
761 // Update the current candidate window. We'll use candidate_window_ from here.
762 // Note that SelectCandidateAt() uses candidate_window_.
763 candidate_window_.CopyFrom(new_candidate_window);
764
765 // Select the current candidate in the page.
766 if (candidate_window_.is_cursor_visible()) {
767 if (candidate_window_.page_size()) {
768 const int current_candidate_in_page =
769 candidate_window_.cursor_position() % candidate_window_.page_size();
770 SelectCandidateAt(current_candidate_in_page);
771 }
772 } else {
773 // Unselect the currently selected candidate.
774 if (0 <= selected_candidate_index_in_page_ &&
775 static_cast<size_t>(selected_candidate_index_in_page_) <
776 candidate_views_.size()) {
777 candidate_views_[selected_candidate_index_in_page_]->Unselect();
778 selected_candidate_index_in_page_ = -1;
779 }
780 }
781 }
782
MaybeInitializeCandidateViews(const CandidateWindow & candidate_window)783 void CandidateWindowView::MaybeInitializeCandidateViews(
784 const CandidateWindow& candidate_window) {
785 const CandidateWindow::Orientation orientation =
786 candidate_window.orientation();
787 const int page_size = candidate_window.page_size();
788 views::View* candidate_area_contents = candidate_area_->contents();
789
790 // Current column width.
791 gfx::Size shortcut_column_size(0, 0);
792 gfx::Size candidate_column_size(0,0);
793 gfx::Size annotation_column_size(0, 0);
794
795 // If orientation is horizontal, don't need to compute width,
796 // because each label is left aligned.
797 if (orientation == CandidateWindow::VERTICAL) {
798 const ui::NativeTheme& theme = *GetNativeTheme();
799 shortcut_column_size = ComputeShortcutColumnSize(candidate_window, theme);
800 candidate_column_size = ComputeCandidateColumnSize(candidate_window);
801 annotation_column_size = ComputeAnnotationColumnSize(candidate_window,
802 theme);
803 }
804
805 // If the requested number of views matches the number of current views, and
806 // previous and current column width are same, just reuse these.
807 //
808 // Note that the early exit logic is not only useful for improving
809 // performance, but also necessary for the horizontal candidate window
810 // to be redrawn properly. If we get rid of the logic, the horizontal
811 // candidate window won't get redrawn properly for some reason when
812 // there is no size change. You can test this by removing "return" here
813 // and type "ni" with Pinyin input method.
814 if (static_cast<int>(candidate_views_.size()) == page_size &&
815 candidate_window_.orientation() == orientation &&
816 previous_shortcut_column_size_ == shortcut_column_size &&
817 previous_candidate_column_size_ == candidate_column_size &&
818 previous_annotation_column_size_ == annotation_column_size) {
819 return;
820 }
821
822 // Update the previous column widths.
823 previous_shortcut_column_size_ = shortcut_column_size;
824 previous_candidate_column_size_ = candidate_column_size;
825 previous_annotation_column_size_ = annotation_column_size;
826
827 // Clear the existing candidate_views if any.
828 for (size_t i = 0; i < candidate_views_.size(); ++i) {
829 candidate_area_contents->RemoveChildView(candidate_views_[i]);
830 // Delete the view after getting out the current message loop iteration.
831 base::MessageLoop::current()->DeleteSoon(FROM_HERE, candidate_views_[i]);
832 }
833 candidate_views_.clear();
834 selected_candidate_index_in_page_ = -1; // Invalidates the index.
835
836 views::GridLayout* layout = new views::GridLayout(candidate_area_contents);
837 // |candidate_area_contents| owns |layout|.
838 candidate_area_contents->SetLayoutManager(layout);
839 // Initialize the column set.
840 views::ColumnSet* column_set = layout->AddColumnSet(0);
841 if (orientation == CandidateWindow::VERTICAL) {
842 column_set->AddColumn(views::GridLayout::FILL,
843 views::GridLayout::FILL,
844 1, views::GridLayout::USE_PREF, 0, 0);
845 } else {
846 for (int i = 0; i < page_size; ++i) {
847 column_set->AddColumn(views::GridLayout::FILL,
848 views::GridLayout::FILL,
849 0, views::GridLayout::USE_PREF, 0, 0);
850 }
851 }
852
853 // Set insets so the border of the selected candidate is drawn inside of
854 // the border of the main candidate window, but we don't have the inset
855 // at the top and the bottom as we have the borders of the header and
856 // footer areas.
857 const gfx::Insets kCandidateAreaInsets(0, 1, 0, 1);
858 layout->SetInsets(kCandidateAreaInsets.top(),
859 kCandidateAreaInsets.left(),
860 kCandidateAreaInsets.bottom(),
861 kCandidateAreaInsets.right());
862
863 // Use maximum height for all rows in candidate area.
864 const int kColumnHeight = std::max(shortcut_column_size.height(),
865 std::max(candidate_column_size.height(),
866 annotation_column_size.height()));
867
868 // Add views to the candidate area.
869 if (orientation == CandidateWindow::HORIZONTAL) {
870 layout->StartRow(0, 0);
871 }
872
873 for (int i = 0; i < page_size; ++i) {
874 CandidateView* candidate_row = new CandidateView(this, i, orientation);
875 candidate_row->Init(shortcut_column_size.width(),
876 candidate_column_size.width(),
877 annotation_column_size.width(),
878 kColumnHeight);
879 candidate_views_.push_back(candidate_row);
880 if (orientation == CandidateWindow::VERTICAL) {
881 layout->StartRow(0, 0);
882 }
883 // |candidate_row| will be owned by |candidate_area_contents|.
884 layout->AddView(candidate_row,
885 1, // Column span.
886 1, // Row span.
887 // Horizontal alignment.
888 orientation == CandidateWindow::VERTICAL ?
889 views::GridLayout::FILL : views::GridLayout::CENTER,
890 views::GridLayout::CENTER, // Vertical alignment.
891 -1, // Preferred width, not specified.
892 kColumnHeight); // Preferred height.
893 }
894
895 // Compute views size in |layout|.
896 // If we don't call this function, GetHorizontalOffset() often
897 // returns invalid value (returns 0), then candidate window
898 // moves right from the correct position in ResizeAndMoveParentFrame().
899 // TODO(nhiroki): Figure out why it returns invalid value.
900 // It seems that the x-position of the candidate labels is not set.
901 layout->Layout(candidate_area_contents);
902 }
903
IsCandidateWindowOpen() const904 bool CandidateWindowView::IsCandidateWindowOpen() const {
905 return !should_show_at_composition_head_ &&
906 candidate_area_->visible() && candidate_area_->IsShown();
907 }
908
SelectCandidateAt(int index_in_page)909 void CandidateWindowView::SelectCandidateAt(int index_in_page) {
910 const int current_page_index = ComputePageIndex(candidate_window_);
911 if (current_page_index < 0) {
912 return;
913 }
914
915 const int cursor_absolute_index =
916 candidate_window_.page_size() * current_page_index + index_in_page;
917 // Ignore click on out of range views.
918 if (cursor_absolute_index < 0 ||
919 candidate_window_.candidates().size() <=
920 static_cast<size_t>(cursor_absolute_index)) {
921 return;
922 }
923
924 // Unselect the currently selected candidate.
925 if (0 <= selected_candidate_index_in_page_ &&
926 static_cast<size_t>(selected_candidate_index_in_page_) <
927 candidate_views_.size()) {
928 candidate_views_[selected_candidate_index_in_page_]->Unselect();
929 }
930 // Remember the currently selected candidate index in the current page.
931 selected_candidate_index_in_page_ = index_in_page;
932
933 // Select the candidate specified by index_in_page.
934 candidate_views_[index_in_page]->Select();
935
936 // Update the cursor indexes in the model.
937 candidate_window_.set_cursor_position(cursor_absolute_index);
938 }
939
OnCandidatePressed(const gfx::Point & location)940 void CandidateWindowView::OnCandidatePressed(
941 const gfx::Point& location) {
942 for (size_t i = 0; i < candidate_views_.size(); ++i) {
943 gfx::Point converted_location = location;
944 views::View::ConvertPointToTarget(this, candidate_views_[i],
945 &converted_location);
946 if (candidate_views_[i]->HitTestPoint(converted_location)) {
947 SelectCandidateAt(i);
948 break;
949 }
950 }
951 }
952
CommitCandidate()953 void CandidateWindowView::CommitCandidate() {
954 if (!(0 <= selected_candidate_index_in_page_ &&
955 static_cast<size_t>(selected_candidate_index_in_page_) <
956 candidate_views_.size())) {
957 return; // Out of range, do nothing.
958 }
959
960 FOR_EACH_OBSERVER(Observer, observers_,
961 OnCandidateCommitted(selected_candidate_index_in_page_));
962 }
963
ResizeAndMoveParentFrame()964 void CandidateWindowView::ResizeAndMoveParentFrame() {
965 // If rendering operation comes from mozc-engine, uses mozc specific bounds,
966 // otherwise candidate window is shown under the cursor.
967 const int x = should_show_at_composition_head_?
968 composition_head_bounds_.x() : cursor_bounds_.x();
969 // To avoid candidate-window overlapping, uses maximum y-position of mozc
970 // specific bounds and cursor bounds, because mozc-engine does not
971 // consider about multi-line composition.
972 const int y = should_show_at_composition_head_?
973 std::max(composition_head_bounds_.y(), cursor_bounds_.y()) :
974 cursor_bounds_.y();
975 const int height = cursor_bounds_.height();
976 const int horizontal_offset = GetHorizontalOffset();
977
978 gfx::Rect old_bounds = parent_frame_->GetClientAreaBoundsInScreen();
979 gfx::Rect screen_bounds = ash::Shell::GetScreen()->GetDisplayMatching(
980 cursor_bounds_).work_area();
981 // The size.
982 gfx::Rect frame_bounds = old_bounds;
983 frame_bounds.set_size(GetPreferredSize());
984
985 // The default position.
986 frame_bounds.set_x(x + horizontal_offset);
987 frame_bounds.set_y(y + height);
988
989 // Handle overflow at the left and the top.
990 frame_bounds.set_x(std::max(frame_bounds.x(), screen_bounds.x()));
991 frame_bounds.set_y(std::max(frame_bounds.y(), screen_bounds.y()));
992
993 // Handle overflow at the right.
994 const int right_overflow = frame_bounds.right() - screen_bounds.right();
995 if (right_overflow > 0) {
996 frame_bounds.set_x(frame_bounds.x() - right_overflow);
997 }
998
999 // Handle overflow at the bottom.
1000 const int bottom_overflow = frame_bounds.bottom() - screen_bounds.bottom();
1001
1002 // To avoid flickering window position, the candidate window should be shown
1003 // on upper side of composition string if it was shown there.
1004 if (should_show_upper_side_ || bottom_overflow > 0) {
1005 frame_bounds.set_y(frame_bounds.y() - height - frame_bounds.height());
1006 should_show_upper_side_ = true;
1007 }
1008
1009 // TODO(nona): check top_overflow here.
1010
1011 // Move the window per the cursor bounds.
1012 // SetBounds() is not cheap. Only call this when it is really changed.
1013 if (frame_bounds != old_bounds)
1014 parent_frame_->SetBounds(frame_bounds);
1015 }
1016
GetHorizontalOffset()1017 int CandidateWindowView::GetHorizontalOffset() {
1018 // Compute the horizontal offset if the candidate window is vertical.
1019 if (!candidate_views_.empty() &&
1020 candidate_window_.orientation() == CandidateWindow::VERTICAL) {
1021 return - candidate_views_[0]->GetCandidateLabelPosition().x();
1022 }
1023 return 0;
1024 }
1025
VisibilityChanged(View * starting_from,bool is_visible)1026 void CandidateWindowView::VisibilityChanged(View* starting_from,
1027 bool is_visible) {
1028 if (is_visible) {
1029 // If the visibility of candidate window is changed,
1030 // we should move the frame to the right position.
1031 ResizeAndMoveParentFrame();
1032 }
1033 }
1034
OnBoundsChanged(const gfx::Rect & previous_bounds)1035 void CandidateWindowView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
1036 // If the bounds(size) of candidate window is changed,
1037 // we should move the frame to the right position.
1038 View::OnBoundsChanged(previous_bounds);
1039 ResizeAndMoveParentFrame();
1040 }
1041
1042 } // namespace input_method
1043 } // namespace chromeos
1044