OLD | NEW |
| (Empty) |
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: | |
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 } | |
38 ~CandidateWindowBorder() override {} | |
39 | |
40 void set_offset(int offset) { offset_ = offset; } | |
41 | |
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())); | |
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 | |
65 gfx::Insets GetInsets() const override { return gfx::Insets(); } | |
66 | |
67 gfx::NativeView parent_; | |
68 int offset_; | |
69 | |
70 DISALLOW_COPY_AND_ASSIGN(CandidateWindowBorder); | |
71 }; | |
72 | |
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; | |
80 } | |
81 | |
82 } // namespace | |
83 | |
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 | |
91 }; | |
92 | |
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)); | |
99 | |
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))); | |
107 } | |
108 | |
109 // Sets the text alignment. | |
110 void SetAlignment(gfx::HorizontalAlignment alignment) { | |
111 label_->SetHorizontalAlignment(alignment); | |
112 } | |
113 | |
114 // Sets the displayed text. | |
115 void SetText(const base::string16& text) { | |
116 label_->SetText(text); | |
117 } | |
118 | |
119 // Sets the border thickness for top/bottom. | |
120 void SetBorderFromPosition(BorderPosition position) { | |
121 SetBorder(views::Border::CreateSolidSidedBorder( | |
122 (position == TOP) ? 1 : 0, | |
123 0, | |
124 (position == BOTTOM) ? 1 : 0, | |
125 0, | |
126 GetNativeTheme()->GetSystemColor( | |
127 ui::NativeTheme::kColorId_MenuBorderColor))); | |
128 } | |
129 | |
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; | |
135 } | |
136 | |
137 private: | |
138 views::Label* label_; | |
139 int min_width_; | |
140 | |
141 DISALLOW_COPY_AND_ASSIGN(InformationTextArea); | |
142 }; | |
143 | |
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()); | |
152 | |
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))); | |
160 | |
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)); | |
184 } | |
185 } | |
186 | |
187 CandidateWindowView::~CandidateWindowView() { | |
188 } | |
189 | |
190 views::Widget* CandidateWindowView::InitWidget() { | |
191 views::Widget* widget = BubbleDelegateView::CreateBubble(this); | |
192 | |
193 wm::SetWindowVisibilityAnimationType( | |
194 widget->GetNativeView(), | |
195 wm::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE); | |
196 | |
197 GetBubbleFrameView()->SetBubbleBorder(scoped_ptr<views::BubbleBorder>( | |
198 new CandidateWindowBorder(parent_window()))); | |
199 return widget; | |
200 } | |
201 | |
202 void CandidateWindowView::UpdateVisibility() { | |
203 if (candidate_area_->visible() || auxiliary_text_->visible() || | |
204 preedit_->visible()) { | |
205 SizeToContents(); | |
206 } else { | |
207 GetWidget()->Close(); | |
208 } | |
209 } | |
210 | |
211 void CandidateWindowView::HideLookupTable() { | |
212 candidate_area_->SetVisible(false); | |
213 auxiliary_text_->SetVisible(false); | |
214 UpdateVisibility(); | |
215 } | |
216 | |
217 void CandidateWindowView::HidePreeditText() { | |
218 preedit_->SetVisible(false); | |
219 UpdateVisibility(); | |
220 } | |
221 | |
222 void CandidateWindowView::ShowPreeditText() { | |
223 preedit_->SetVisible(true); | |
224 UpdateVisibility(); | |
225 } | |
226 | |
227 void CandidateWindowView::UpdatePreeditText(const base::string16& text) { | |
228 preedit_->SetText(text); | |
229 } | |
230 | |
231 void CandidateWindowView::ShowLookupTable() { | |
232 candidate_area_->SetVisible(true); | |
233 auxiliary_text_->SetVisible(candidate_window_.is_auxiliary_text_visible()); | |
234 UpdateVisibility(); | |
235 } | |
236 | |
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)); | |
256 } | |
257 } | |
258 | |
259 // Initialize candidate views if necessary. | |
260 MaybeInitializeCandidateViews(new_candidate_window); | |
261 | |
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; | |
268 | |
269 // Update the candidates in the current page. | |
270 const size_t start_from = | |
271 current_page_index * new_candidate_window.page_size(); | |
272 | |
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); | |
291 } | |
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); | |
299 } | |
300 } | |
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); | |
304 } | |
305 | |
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); | |
312 } | |
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); | |
316 | |
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); | |
323 } | |
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; | |
332 } | |
333 } | |
334 | |
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())); | |
339 } | |
340 | |
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); | |
347 } | |
348 | |
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(); | |
354 | |
355 // Reset all candidate_views_ when orientation changes. | |
356 if (orientation != candidate_window_.orientation()) | |
357 STLDeleteElements(&candidate_views_); | |
358 | |
359 while (page_size < candidate_views_.size()) { | |
360 delete candidate_views_.back(); | |
361 candidate_views_.pop_back(); | |
362 } | |
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); | |
367 } | |
368 } | |
369 | |
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; | |
374 } | |
375 | |
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; | |
383 } | |
384 | |
385 // Remember the currently selected candidate index in the current page. | |
386 selected_candidate_index_in_page_ = index_in_page; | |
387 | |
388 // Select the candidate specified by index_in_page. | |
389 candidate_views_[index_in_page]->SetHighlighted(true); | |
390 | |
391 // Update the cursor indexes in the model. | |
392 candidate_window_.set_cursor_position(cursor_absolute_index); | |
393 } | |
394 | |
395 void CandidateWindowView::ButtonPressed(views::Button* sender, | |
396 const ui::Event& event) { | |
397 for (size_t i = 0; i < candidate_views_.size(); ++i) { | |
398 if (sender == candidate_views_[i]) { | |
399 FOR_EACH_OBSERVER(Observer, observers_, OnCandidateCommitted(i)); | |
400 return; | |
401 } | |
402 } | |
403 } | |
404 | |
405 } // namespace ime | |
406 } // namespace ash | |
OLD | NEW |