• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4 
5 #include "ash/ime/candidate_window_view.h"
6 
7 #include <string>
8 
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"
22 
23 namespace ash {
24 namespace ime {
25 
26 namespace {
27 
28 class CandidateWindowBorder : public views::BubbleBorder {
29  public:
CandidateWindowBorder(gfx::NativeView parent)30   explicit CandidateWindowBorder(gfx::NativeView parent)
31       : views::BubbleBorder(views::BubbleBorder::TOP_CENTER,
32                             views::BubbleBorder::NO_SHADOW,
33                             SK_ColorTRANSPARENT),
34         parent_(parent),
35         offset_(0) {
36     set_paint_arrow(views::BubbleBorder::PAINT_NONE);
37   }
~CandidateWindowBorder()38   virtual ~CandidateWindowBorder() {}
39 
set_offset(int offset)40   void set_offset(int offset) { offset_ = offset; }
41 
42  private:
43   // Overridden from views::BubbleBorder:
GetBounds(const gfx::Rect & anchor_rect,const gfx::Size & content_size) const44   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()));
51 
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());
61 
62     return bounds;
63   }
64 
GetInsets() const65   virtual gfx::Insets GetInsets() const OVERRIDE {
66     return gfx::Insets();
67   }
68 
69   gfx::NativeView parent_;
70   int offset_;
71 
72   DISALLOW_COPY_AND_ASSIGN(CandidateWindowBorder);
73 };
74 
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.
ComputePageIndex(const ui::CandidateWindow & candidate_window)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();
81   return -1;
82 }
83 
84 }  // namespace
85 
86 class InformationTextArea : public views::View {
87  public:
88   // InformationTextArea's border is drawn as a separator, it should appear
89   // at either top or bottom.
90   enum BorderPosition {
91     TOP,
92     BOTTOM
93   };
94 
95   // Specify the alignment and initialize the control.
InformationTextArea(gfx::HorizontalAlignment align,int min_width)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));
101 
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),
108                                 0x10)));
109   }
110 
111   // Sets the text alignment.
SetAlignment(gfx::HorizontalAlignment alignment)112   void SetAlignment(gfx::HorizontalAlignment alignment) {
113     label_->SetHorizontalAlignment(alignment);
114   }
115 
116   // Sets the displayed text.
SetText(const base::string16 & text)117   void SetText(const base::string16& text) {
118     label_->SetText(text);
119   }
120 
121   // Sets the border thickness for top/bottom.
SetBorderFromPosition(BorderPosition position)122   void SetBorderFromPosition(BorderPosition position) {
123     SetBorder(views::Border::CreateSolidSidedBorder(
124         (position == TOP) ? 1 : 0,
125         0,
126         (position == BOTTOM) ? 1 : 0,
127         0,
128         GetNativeTheme()->GetSystemColor(
129             ui::NativeTheme::kColorId_MenuBorderColor)));
130   }
131 
132  protected:
GetPreferredSize() const133   virtual gfx::Size GetPreferredSize() const OVERRIDE {
134     gfx::Size size = views::View::GetPreferredSize();
135     size.SetToMax(gfx::Size(min_width_, 0));
136     return size;
137   }
138 
139  private:
140   views::Label* label_;
141   int min_width_;
142 
143   DISALLOW_COPY_AND_ASSIGN(InformationTextArea);
144 };
145 
CandidateWindowView(gfx::NativeView parent)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_use_focusless(true);
152   set_parent_window(parent);
153   set_margins(gfx::Insets());
154 
155   // Set the background and the border of the view.
156   ui::NativeTheme* theme = GetNativeTheme();
157   set_background(
158       views::Background::CreateSolidBackground(theme->GetSystemColor(
159           ui::NativeTheme::kColorId_WindowBackground)));
160   SetBorder(views::Border::CreateSolidBorder(
161       1, theme->GetSystemColor(ui::NativeTheme::kColorId_MenuBorderColor)));
162 
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));
178   } else {
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));
186   }
187 }
188 
~CandidateWindowView()189 CandidateWindowView::~CandidateWindowView() {
190 }
191 
InitWidget()192 views::Widget* CandidateWindowView::InitWidget() {
193   views::Widget* widget = BubbleDelegateView::CreateBubble(this);
194 
195   wm::SetWindowVisibilityAnimationType(
196       widget->GetNativeView(),
197       wm::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE);
198 
199   GetBubbleFrameView()->SetBubbleBorder(scoped_ptr<views::BubbleBorder>(
200       new CandidateWindowBorder(parent_window())));
201   return widget;
202 }
203 
UpdateVisibility()204 void CandidateWindowView::UpdateVisibility() {
205   if (candidate_area_->visible() || auxiliary_text_->visible() ||
206       preedit_->visible()) {
207     SizeToContents();
208   } else {
209     GetWidget()->Close();
210   }
211 }
212 
HideLookupTable()213 void CandidateWindowView::HideLookupTable() {
214   candidate_area_->SetVisible(false);
215   auxiliary_text_->SetVisible(false);
216   UpdateVisibility();
217 }
218 
HidePreeditText()219 void CandidateWindowView::HidePreeditText() {
220   preedit_->SetVisible(false);
221   UpdateVisibility();
222 }
223 
ShowPreeditText()224 void CandidateWindowView::ShowPreeditText() {
225   preedit_->SetVisible(true);
226   UpdateVisibility();
227 }
228 
UpdatePreeditText(const base::string16 & text)229 void CandidateWindowView::UpdatePreeditText(const base::string16& text) {
230   preedit_->SetText(text);
231 }
232 
ShowLookupTable()233 void CandidateWindowView::ShowLookupTable() {
234   candidate_area_->SetVisible(true);
235   auxiliary_text_->SetVisible(candidate_window_.is_auxiliary_text_visible());
236   UpdateVisibility();
237 }
238 
UpdateCandidates(const ui::CandidateWindow & new_candidate_window)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));
252       } else {
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));
258       }
259     }
260 
261     // Initialize candidate views if necessary.
262     MaybeInitializeCandidateViews(new_candidate_window);
263 
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)
269       return;
270 
271     // Update the candidates in the current page.
272     const size_t start_from =
273         current_page_index * new_candidate_window.page_size();
274 
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());
288       } else {
289         // Disable the empty row.
290         candidate_view->SetEntry(ui::CandidateWindow::Entry());
291         candidate_view->SetEnabled(false);
292         candidate_view->SetInfolistIcon(false);
293       }
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);
301       }
302     }
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);
306     }
307 
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);
312     else
313       border->set_offset(0);
314   }
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);
318 
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);
325     }
326   } else {
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(
332           false);
333       selected_candidate_index_in_page_ = -1;
334     }
335   }
336 
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()));
341 }
342 
SetCursorBounds(const gfx::Rect & cursor_bounds,const gfx::Rect & composition_head)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);
347   else
348     SetAnchorRect(cursor_bounds);
349 }
350 
MaybeInitializeCandidateViews(const ui::CandidateWindow & candidate_window)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();
356 
357   // Reset all candidate_views_ when orientation changes.
358   if (orientation != candidate_window_.orientation())
359     STLDeleteElements(&candidate_views_);
360 
361   while (page_size < candidate_views_.size()) {
362     delete candidate_views_.back();
363     candidate_views_.pop_back();
364   }
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);
369   }
370 }
371 
SelectCandidateAt(int index_in_page)372 void CandidateWindowView::SelectCandidateAt(int index_in_page) {
373   const int current_page_index = ComputePageIndex(candidate_window_);
374   if (current_page_index < 0) {
375     return;
376   }
377 
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)) {
384     return;
385   }
386 
387   // Remember the currently selected candidate index in the current page.
388   selected_candidate_index_in_page_ = index_in_page;
389 
390   // Select the candidate specified by index_in_page.
391   candidate_views_[index_in_page]->SetHighlighted(true);
392 
393   // Update the cursor indexes in the model.
394   candidate_window_.set_cursor_position(cursor_absolute_index);
395 }
396 
ButtonPressed(views::Button * sender,const ui::Event & event)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));
402       return;
403     }
404   }
405 }
406 
407 }  // namespace ime
408 }  // namespace ash
409