• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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