| 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 |