Roll src/third_party/WebKit 06cb9e9:a978ee5 (svn 202558:202559)
[chromium-blink-merge.git] / ui / chromeos / ime / candidate_window_view.cc
blob7532703567784617ecd535017dcfa19b38952a1c
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ui/chromeos/ime/candidate_window_view.h"
7 #include <string>
9 #include "base/strings/utf_string_conversions.h"
10 #include "ui/chromeos/ime/candidate_view.h"
11 #include "ui/chromeos/ime/candidate_window_constants.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"
23 namespace ui {
24 namespace ime {
26 namespace {
28 class CandidateWindowBorder : public views::BubbleBorder {
29 public:
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);
38 ~CandidateWindowBorder() override {}
40 void set_offset(int offset) { offset_ = offset; }
42 private:
43 // Overridden from views::BubbleBorder:
44 gfx::Rect GetBounds(const gfx::Rect& anchor_rect,
45 const gfx::Size& content_size) const override {
46 gfx::Rect bounds(content_size);
47 bounds.set_origin(gfx::Point(
48 anchor_rect.x() - offset_,
49 is_arrow_on_top(arrow()) ?
50 anchor_rect.bottom() : anchor_rect.y() - content_size.height()));
52 // It cannot use the normal logic of arrow offset for horizontal offscreen,
53 // because the arrow must be in the content's edge. But CandidateWindow has
54 // to be visible even when |anchor_rect| is out of the screen.
55 gfx::Rect work_area = gfx::Screen::GetNativeScreen()->
56 GetDisplayNearestWindow(parent_).work_area();
57 if (bounds.right() > work_area.right())
58 bounds.set_x(work_area.right() - bounds.width());
59 if (bounds.x() < work_area.x())
60 bounds.set_x(work_area.x());
62 return bounds;
65 gfx::Insets GetInsets() const override { return gfx::Insets(); }
67 gfx::NativeView parent_;
68 int offset_;
70 DISALLOW_COPY_AND_ASSIGN(CandidateWindowBorder);
73 // Computes the page index. For instance, if the page size is 9, and the
74 // cursor is pointing to 13th candidate, the page index will be 1 (2nd
75 // page, as the index is zero-origin). Returns -1 on error.
76 int ComputePageIndex(const ui::CandidateWindow& candidate_window) {
77 if (candidate_window.page_size() > 0)
78 return candidate_window.cursor_position() / candidate_window.page_size();
79 return -1;
82 } // namespace
84 class InformationTextArea : public views::View {
85 public:
86 // InformationTextArea's border is drawn as a separator, it should appear
87 // at either top or bottom.
88 enum BorderPosition {
89 TOP,
90 BOTTOM
93 // Specify the alignment and initialize the control.
94 InformationTextArea(gfx::HorizontalAlignment align, int min_width)
95 : min_width_(min_width) {
96 label_ = new views::Label;
97 label_->SetHorizontalAlignment(align);
98 label_->SetBorder(views::Border::CreateEmptyBorder(2, 2, 2, 4));
100 SetLayoutManager(new views::FillLayout());
101 AddChildView(label_);
102 set_background(views::Background::CreateSolidBackground(
103 color_utils::AlphaBlend(SK_ColorBLACK,
104 GetNativeTheme()->GetSystemColor(
105 ui::NativeTheme::kColorId_WindowBackground),
106 0x10)));
109 // Sets the text alignment.
110 void SetAlignment(gfx::HorizontalAlignment alignment) {
111 label_->SetHorizontalAlignment(alignment);
114 // Sets the displayed text.
115 void SetText(const base::string16& text) {
116 label_->SetText(text);
119 // Sets the border thickness for top/bottom.
120 void SetBorderFromPosition(BorderPosition position) {
121 SetBorder(views::Border::CreateSolidSidedBorder(
122 (position == TOP) ? 1 : 0,
124 (position == BOTTOM) ? 1 : 0,
126 GetNativeTheme()->GetSystemColor(
127 ui::NativeTheme::kColorId_MenuBorderColor)));
130 protected:
131 gfx::Size GetPreferredSize() const override {
132 gfx::Size size = views::View::GetPreferredSize();
133 size.SetToMax(gfx::Size(min_width_, 0));
134 return size;
137 private:
138 views::Label* label_;
139 int min_width_;
141 DISALLOW_COPY_AND_ASSIGN(InformationTextArea);
144 CandidateWindowView::CandidateWindowView(gfx::NativeView parent)
145 : selected_candidate_index_in_page_(-1),
146 should_show_at_composition_head_(false),
147 should_show_upper_side_(false),
148 was_candidate_window_open_(false) {
149 set_can_activate(false);
150 set_parent_window(parent);
151 set_margins(gfx::Insets());
153 // Set the background and the border of the view.
154 ui::NativeTheme* theme = GetNativeTheme();
155 set_background(
156 views::Background::CreateSolidBackground(theme->GetSystemColor(
157 ui::NativeTheme::kColorId_WindowBackground)));
158 SetBorder(views::Border::CreateSolidBorder(
159 1, theme->GetSystemColor(ui::NativeTheme::kColorId_MenuBorderColor)));
161 SetLayoutManager(new views::BoxLayout(views::BoxLayout::kVertical, 0, 0, 0));
162 auxiliary_text_ = new InformationTextArea(gfx::ALIGN_RIGHT, 0);
163 preedit_ = new InformationTextArea(gfx::ALIGN_LEFT, kMinPreeditAreaWidth);
164 candidate_area_ = new views::View;
165 auxiliary_text_->SetVisible(false);
166 preedit_->SetVisible(false);
167 candidate_area_->SetVisible(false);
168 preedit_->SetBorderFromPosition(InformationTextArea::BOTTOM);
169 if (candidate_window_.orientation() == ui::CandidateWindow::VERTICAL) {
170 AddChildView(preedit_);
171 AddChildView(candidate_area_);
172 AddChildView(auxiliary_text_);
173 auxiliary_text_->SetBorderFromPosition(InformationTextArea::TOP);
174 candidate_area_->SetLayoutManager(new views::BoxLayout(
175 views::BoxLayout::kVertical, 0, 0, 0));
176 } else {
177 AddChildView(preedit_);
178 AddChildView(auxiliary_text_);
179 AddChildView(candidate_area_);
180 auxiliary_text_->SetAlignment(gfx::ALIGN_LEFT);
181 auxiliary_text_->SetBorderFromPosition(InformationTextArea::BOTTOM);
182 candidate_area_->SetLayoutManager(new views::BoxLayout(
183 views::BoxLayout::kHorizontal, 0, 0, 0));
187 CandidateWindowView::~CandidateWindowView() {
190 views::Widget* CandidateWindowView::InitWidget() {
191 views::Widget* widget = BubbleDelegateView::CreateBubble(this);
193 wm::SetWindowVisibilityAnimationType(
194 widget->GetNativeView(),
195 wm::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE);
197 GetBubbleFrameView()->SetBubbleBorder(scoped_ptr<views::BubbleBorder>(
198 new CandidateWindowBorder(parent_window())));
199 return widget;
202 void CandidateWindowView::UpdateVisibility() {
203 if (candidate_area_->visible() || auxiliary_text_->visible() ||
204 preedit_->visible()) {
205 SizeToContents();
206 } else {
207 GetWidget()->Close();
211 void CandidateWindowView::HideLookupTable() {
212 candidate_area_->SetVisible(false);
213 auxiliary_text_->SetVisible(false);
214 UpdateVisibility();
217 void CandidateWindowView::HidePreeditText() {
218 preedit_->SetVisible(false);
219 UpdateVisibility();
222 void CandidateWindowView::ShowPreeditText() {
223 preedit_->SetVisible(true);
224 UpdateVisibility();
227 void CandidateWindowView::UpdatePreeditText(const base::string16& text) {
228 preedit_->SetText(text);
231 void CandidateWindowView::ShowLookupTable() {
232 candidate_area_->SetVisible(true);
233 auxiliary_text_->SetVisible(candidate_window_.is_auxiliary_text_visible());
234 UpdateVisibility();
237 void CandidateWindowView::UpdateCandidates(
238 const ui::CandidateWindow& new_candidate_window) {
239 // Updating the candidate views is expensive. We'll skip this if possible.
240 if (!candidate_window_.IsEqual(new_candidate_window)) {
241 if (candidate_window_.orientation() != new_candidate_window.orientation()) {
242 // If the new layout is vertical, the aux text should appear at the
243 // bottom. If horizontal, it should appear between preedit and candidates.
244 if (new_candidate_window.orientation() == ui::CandidateWindow::VERTICAL) {
245 ReorderChildView(auxiliary_text_, -1);
246 auxiliary_text_->SetAlignment(gfx::ALIGN_RIGHT);
247 auxiliary_text_->SetBorderFromPosition(InformationTextArea::TOP);
248 candidate_area_->SetLayoutManager(new views::BoxLayout(
249 views::BoxLayout::kVertical, 0, 0, 0));
250 } else {
251 ReorderChildView(auxiliary_text_, 1);
252 auxiliary_text_->SetAlignment(gfx::ALIGN_LEFT);
253 auxiliary_text_->SetBorderFromPosition(InformationTextArea::BOTTOM);
254 candidate_area_->SetLayoutManager(new views::BoxLayout(
255 views::BoxLayout::kHorizontal, 0, 0, 0));
259 // Initialize candidate views if necessary.
260 MaybeInitializeCandidateViews(new_candidate_window);
262 should_show_at_composition_head_
263 = new_candidate_window.show_window_at_composition();
264 // Compute the index of the current page.
265 const int current_page_index = ComputePageIndex(new_candidate_window);
266 if (current_page_index < 0)
267 return;
269 // Update the candidates in the current page.
270 const size_t start_from =
271 current_page_index * new_candidate_window.page_size();
273 int max_shortcut_width = 0;
274 int max_candidate_width = 0;
275 for (size_t i = 0; i < candidate_views_.size(); ++i) {
276 const size_t index_in_page = i;
277 const size_t candidate_index = start_from + index_in_page;
278 CandidateView* candidate_view = candidate_views_[index_in_page];
279 // Set the candidate text.
280 if (candidate_index < new_candidate_window.candidates().size()) {
281 const ui::CandidateWindow::Entry& entry =
282 new_candidate_window.candidates()[candidate_index];
283 candidate_view->SetEntry(entry);
284 candidate_view->SetEnabled(true);
285 candidate_view->SetInfolistIcon(!entry.description_title.empty());
286 } else {
287 // Disable the empty row.
288 candidate_view->SetEntry(ui::CandidateWindow::Entry());
289 candidate_view->SetEnabled(false);
290 candidate_view->SetInfolistIcon(false);
292 if (new_candidate_window.orientation() == ui::CandidateWindow::VERTICAL) {
293 int shortcut_width = 0;
294 int candidate_width = 0;
295 candidate_views_[i]->GetPreferredWidths(
296 &shortcut_width, &candidate_width);
297 max_shortcut_width = std::max(max_shortcut_width, shortcut_width);
298 max_candidate_width = std::max(max_candidate_width, candidate_width);
301 if (new_candidate_window.orientation() == ui::CandidateWindow::VERTICAL) {
302 for (size_t i = 0; i < candidate_views_.size(); ++i)
303 candidate_views_[i]->SetWidths(max_shortcut_width, max_candidate_width);
306 CandidateWindowBorder* border = static_cast<CandidateWindowBorder*>(
307 GetBubbleFrameView()->bubble_border());
308 if (new_candidate_window.orientation() == ui::CandidateWindow::VERTICAL)
309 border->set_offset(max_shortcut_width);
310 else
311 border->set_offset(0);
313 // Update the current candidate window. We'll use candidate_window_ from here.
314 // Note that SelectCandidateAt() uses candidate_window_.
315 candidate_window_.CopyFrom(new_candidate_window);
317 // Select the current candidate in the page.
318 if (candidate_window_.is_cursor_visible()) {
319 if (candidate_window_.page_size()) {
320 const int current_candidate_in_page =
321 candidate_window_.cursor_position() % candidate_window_.page_size();
322 SelectCandidateAt(current_candidate_in_page);
324 } else {
325 // Unselect the currently selected candidate.
326 if (0 <= selected_candidate_index_in_page_ &&
327 static_cast<size_t>(selected_candidate_index_in_page_) <
328 candidate_views_.size()) {
329 candidate_views_[selected_candidate_index_in_page_]->SetHighlighted(
330 false);
331 selected_candidate_index_in_page_ = -1;
335 // Updates auxiliary text
336 auxiliary_text_->SetVisible(candidate_window_.is_auxiliary_text_visible());
337 auxiliary_text_->SetText(base::UTF8ToUTF16(
338 candidate_window_.auxiliary_text()));
341 void CandidateWindowView::SetCursorBounds(const gfx::Rect& cursor_bounds,
342 const gfx::Rect& composition_head) {
343 if (candidate_window_.show_window_at_composition())
344 SetAnchorRect(composition_head);
345 else
346 SetAnchorRect(cursor_bounds);
349 void CandidateWindowView::MaybeInitializeCandidateViews(
350 const ui::CandidateWindow& candidate_window) {
351 const ui::CandidateWindow::Orientation orientation =
352 candidate_window.orientation();
353 const size_t page_size = candidate_window.page_size();
355 // Reset all candidate_views_ when orientation changes.
356 if (orientation != candidate_window_.orientation())
357 STLDeleteElements(&candidate_views_);
359 while (page_size < candidate_views_.size()) {
360 delete candidate_views_.back();
361 candidate_views_.pop_back();
363 while (page_size > candidate_views_.size()) {
364 CandidateView* new_candidate = new CandidateView(this, orientation);
365 candidate_area_->AddChildView(new_candidate);
366 candidate_views_.push_back(new_candidate);
370 void CandidateWindowView::SelectCandidateAt(int index_in_page) {
371 const int current_page_index = ComputePageIndex(candidate_window_);
372 if (current_page_index < 0) {
373 return;
376 const int cursor_absolute_index =
377 candidate_window_.page_size() * current_page_index + index_in_page;
378 // Ignore click on out of range views.
379 if (cursor_absolute_index < 0 ||
380 candidate_window_.candidates().size() <=
381 static_cast<size_t>(cursor_absolute_index)) {
382 return;
385 // Remember the currently selected candidate index in the current page.
386 selected_candidate_index_in_page_ = index_in_page;
388 // Select the candidate specified by index_in_page.
389 candidate_views_[index_in_page]->SetHighlighted(true);
391 // Update the cursor indexes in the model.
392 candidate_window_.set_cursor_position(cursor_absolute_index);
395 const char* CandidateWindowView::GetClassName() const {
396 return "CandidateWindowView";
399 void CandidateWindowView::ButtonPressed(views::Button* sender,
400 const ui::Event& event) {
401 for (size_t i = 0; i < candidate_views_.size(); ++i) {
402 if (sender == candidate_views_[i]) {
403 FOR_EACH_OBSERVER(Observer, observers_, OnCandidateCommitted(i));
404 return;
409 } // namespace ime
410 } // namespace ui