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 virtual ~CandidateWindowBorder() {} 39 40 void set_offset(int offset) { offset_ = offset; } 41 42 private: 43 // Overridden from views::BubbleBorder: 44 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 65 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. 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. 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. 112 void SetAlignment(gfx::HorizontalAlignment alignment) { 113 label_->SetHorizontalAlignment(alignment); 114 } 115 116 // Sets the displayed text. 117 void SetText(const base::string16& text) { 118 label_->SetText(text); 119 } 120 121 // Sets the border thickness for top/bottom. 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: 133 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 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_can_activate(false); 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 189 CandidateWindowView::~CandidateWindowView() { 190 } 191 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 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 213 void CandidateWindowView::HideLookupTable() { 214 candidate_area_->SetVisible(false); 215 auxiliary_text_->SetVisible(false); 216 UpdateVisibility(); 217 } 218 219 void CandidateWindowView::HidePreeditText() { 220 preedit_->SetVisible(false); 221 UpdateVisibility(); 222 } 223 224 void CandidateWindowView::ShowPreeditText() { 225 preedit_->SetVisible(true); 226 UpdateVisibility(); 227 } 228 229 void CandidateWindowView::UpdatePreeditText(const base::string16& text) { 230 preedit_->SetText(text); 231 } 232 233 void CandidateWindowView::ShowLookupTable() { 234 candidate_area_->SetVisible(true); 235 auxiliary_text_->SetVisible(candidate_window_.is_auxiliary_text_visible()); 236 UpdateVisibility(); 237 } 238 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 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 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 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 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