1 // Copyright (c) 2012 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 #include "chrome/browser/chromeos/input_method/candidate_window_view.h" 5 6 #include <string> 7 8 #include "ash/shell.h" 9 #include "base/strings/utf_string_conversions.h" 10 #include "chrome/browser/chromeos/input_method/candidate_view.h" 11 #include "chrome/browser/chromeos/input_method/candidate_window_constants.h" 12 #include "chrome/browser/chromeos/input_method/hidable_area.h" 13 #include "chromeos/dbus/ibus/ibus_lookup_table.h" 14 #include "ui/gfx/color_utils.h" 15 #include "ui/native_theme/native_theme.h" 16 #include "ui/views/controls/label.h" 17 #include "ui/views/layout/grid_layout.h" 18 #include "ui/views/widget/widget.h" 19 20 namespace chromeos { 21 namespace input_method { 22 23 namespace { 24 // VerticalCandidateLabel is used for rendering candidate text in 25 // the vertical candidate window. 26 class VerticalCandidateLabel : public views::Label { 27 public: 28 VerticalCandidateLabel() {} 29 30 private: 31 virtual ~VerticalCandidateLabel() {} 32 33 // Returns the preferred size, but guarantees that the width has at 34 // least kMinCandidateLabelWidth pixels. 35 virtual gfx::Size GetPreferredSize() OVERRIDE { 36 gfx::Size size = Label::GetPreferredSize(); 37 // Hack. +2 is needed to prevent labels from getting elided like 38 // "abc..." in some cases. TODO(satorux): Figure out why it's 39 // necessary. 40 size.set_width(size.width() + 2); 41 if (size.width() < kMinCandidateLabelWidth) { 42 size.set_width(kMinCandidateLabelWidth); 43 } 44 if (size.width() > kMaxCandidateLabelWidth) { 45 size.set_width(kMaxCandidateLabelWidth); 46 } 47 return size; 48 } 49 50 DISALLOW_COPY_AND_ASSIGN(VerticalCandidateLabel); 51 }; 52 53 // Wraps the given view with some padding, and returns it. 54 views::View* WrapWithPadding(views::View* view, const gfx::Insets& insets) { 55 views::View* wrapper = new views::View; 56 // Use GridLayout to give some insets inside. 57 views::GridLayout* layout = new views::GridLayout(wrapper); 58 wrapper->SetLayoutManager(layout); // |wrapper| owns |layout|. 59 layout->SetInsets(insets); 60 61 views::ColumnSet* column_set = layout->AddColumnSet(0); 62 column_set->AddColumn( 63 views::GridLayout::FILL, views::GridLayout::FILL, 64 1, views::GridLayout::USE_PREF, 0, 0); 65 layout->StartRow(0, 0); 66 67 // Add the view contents. 68 layout->AddView(view); // |view| is owned by |wraper|, not |layout|. 69 return wrapper; 70 } 71 72 // Creates shortcut text from the given index and the orientation. 73 string16 CreateShortcutText(size_t index, const IBusLookupTable& table) { 74 if (index >= table.candidates().size()) 75 return UTF8ToUTF16(""); 76 std::string shortcut_text = table.candidates()[index].label; 77 if (!shortcut_text.empty() && 78 table.orientation() != IBusLookupTable::VERTICAL) 79 shortcut_text += '.'; 80 return UTF8ToUTF16(shortcut_text); 81 } 82 83 // Creates the shortcut label, and returns it (never returns NULL). 84 // The label text is not set in this function. 85 views::Label* CreateShortcutLabel( 86 IBusLookupTable::Orientation orientation, const ui::NativeTheme& theme) { 87 // Create the shortcut label. The label will be owned by 88 // |wrapped_shortcut_label|, hence it's deleted when 89 // |wrapped_shortcut_label| is deleted. 90 views::Label* shortcut_label = new views::Label; 91 92 if (orientation == IBusLookupTable::VERTICAL) { 93 shortcut_label->SetFont( 94 shortcut_label->font().DeriveFont(kFontSizeDelta, gfx::Font::BOLD)); 95 } else { 96 shortcut_label->SetFont( 97 shortcut_label->font().DeriveFont(kFontSizeDelta)); 98 } 99 // TODO(satorux): Maybe we need to use language specific fonts for 100 // candidate_label, like Chinese font for Chinese input method? 101 shortcut_label->SetEnabledColor(theme.GetSystemColor( 102 ui::NativeTheme::kColorId_LabelEnabledColor)); 103 shortcut_label->SetDisabledColor(theme.GetSystemColor( 104 ui::NativeTheme::kColorId_LabelDisabledColor)); 105 106 return shortcut_label; 107 } 108 109 // Wraps the shortcut label, then decorates wrapped shortcut label 110 // and returns it (never returns NULL). 111 // The label text is not set in this function. 112 views::View* CreateWrappedShortcutLabel( 113 views::Label* shortcut_label, 114 IBusLookupTable::Orientation orientation, 115 const ui::NativeTheme& theme) { 116 // Wrap it with padding. 117 const gfx::Insets kVerticalShortcutLabelInsets(1, 6, 1, 6); 118 const gfx::Insets kHorizontalShortcutLabelInsets(1, 3, 1, 0); 119 const gfx::Insets insets = 120 (orientation == IBusLookupTable::VERTICAL ? 121 kVerticalShortcutLabelInsets : 122 kHorizontalShortcutLabelInsets); 123 views::View* wrapped_shortcut_label = 124 WrapWithPadding(shortcut_label, insets); 125 126 // Add decoration based on the orientation. 127 if (orientation == IBusLookupTable::VERTICAL) { 128 // Set the background color. 129 SkColor blackish = color_utils::AlphaBlend( 130 SK_ColorBLACK, 131 theme.GetSystemColor(ui::NativeTheme::kColorId_WindowBackground), 132 0x40); 133 SkColor transparent_blakish = color_utils::AlphaBlend( 134 SK_ColorTRANSPARENT, blackish, 0xE0); 135 wrapped_shortcut_label->set_background( 136 views::Background::CreateSolidBackground(transparent_blakish)); 137 shortcut_label->SetBackgroundColor( 138 wrapped_shortcut_label->background()->get_color()); 139 } 140 141 return wrapped_shortcut_label; 142 } 143 144 // Creates the candidate label, and returns it (never returns NULL). 145 // The label text is not set in this function. 146 views::Label* CreateCandidateLabel( 147 IBusLookupTable::Orientation orientation) { 148 views::Label* candidate_label = NULL; 149 150 // Create the candidate label. The label will be added to |this| as a 151 // child view, hence it's deleted when |this| is deleted. 152 if (orientation == IBusLookupTable::VERTICAL) { 153 candidate_label = new VerticalCandidateLabel; 154 } else { 155 candidate_label = new views::Label; 156 } 157 158 // Change the font size. 159 candidate_label->SetFont( 160 candidate_label->font().DeriveFont(kFontSizeDelta)); 161 candidate_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 162 163 return candidate_label; 164 } 165 166 // Creates the annotation label, and return it (never returns NULL). 167 // The label text is not set in this function. 168 views::Label* CreateAnnotationLabel( 169 IBusLookupTable::Orientation orientation, const ui::NativeTheme& theme) { 170 // Create the annotation label. 171 views::Label* annotation_label = new views::Label; 172 173 // Change the font size and color. 174 annotation_label->SetFont( 175 annotation_label->font().DeriveFont(kFontSizeDelta)); 176 annotation_label->SetEnabledColor(theme.GetSystemColor( 177 ui::NativeTheme::kColorId_LabelDisabledColor)); 178 annotation_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 179 180 return annotation_label; 181 } 182 183 // Computes shortcut column size. 184 gfx::Size ComputeShortcutColumnSize( 185 const IBusLookupTable& lookup_table, 186 const ui::NativeTheme& theme) { 187 int shortcut_column_width = 0; 188 int shortcut_column_height = 0; 189 // Create the shortcut label. The label will be owned by 190 // |wrapped_shortcut_label|, hence it's deleted when 191 // |wrapped_shortcut_label| is deleted. 192 views::Label* shortcut_label = CreateShortcutLabel( 193 lookup_table.orientation(), theme); 194 scoped_ptr<views::View> wrapped_shortcut_label( 195 CreateWrappedShortcutLabel(shortcut_label, 196 lookup_table.orientation(), 197 theme)); 198 199 // Compute the max width and height in shortcut labels. 200 // We'll create temporary shortcut labels, and choose the largest width and 201 // height. 202 for (size_t i = 0; i < lookup_table.page_size(); ++i) { 203 shortcut_label->SetText(CreateShortcutText(i, lookup_table)); 204 gfx::Size text_size = wrapped_shortcut_label->GetPreferredSize(); 205 shortcut_column_width = std::max(shortcut_column_width, text_size.width()); 206 shortcut_column_height = std::max(shortcut_column_height, 207 text_size.height()); 208 } 209 210 return gfx::Size(shortcut_column_width, shortcut_column_height); 211 } 212 213 // Computes the page index. For instance, if the page size is 9, and the 214 // cursor is pointing to 13th candidate, the page index will be 1 (2nd 215 // page, as the index is zero-origin). Returns -1 on error. 216 int ComputePageIndex(const IBusLookupTable& lookup_table) { 217 if (lookup_table.page_size() > 0) 218 return lookup_table.cursor_position() / lookup_table.page_size(); 219 return -1; 220 } 221 222 // Computes candidate column size. 223 gfx::Size ComputeCandidateColumnSize( 224 const IBusLookupTable& lookup_table) { 225 int candidate_column_width = 0; 226 int candidate_column_height = 0; 227 scoped_ptr<views::Label> candidate_label( 228 CreateCandidateLabel(lookup_table.orientation())); 229 230 // Compute the start index of |lookup_table_|. 231 const int current_page_index = ComputePageIndex(lookup_table); 232 if (current_page_index < 0) 233 return gfx::Size(0, 0); 234 const size_t start_from = current_page_index * lookup_table.page_size(); 235 236 // Compute the max width and height in candidate labels. 237 // We'll create temporary candidate labels, and choose the largest width and 238 // height. 239 for (size_t i = 0; i + start_from < lookup_table.candidates().size(); ++i) { 240 const size_t index = start_from + i; 241 242 candidate_label->SetText( 243 UTF8ToUTF16(lookup_table.candidates()[index].value)); 244 gfx::Size text_size = candidate_label->GetPreferredSize(); 245 candidate_column_width = std::max(candidate_column_width, 246 text_size.width()); 247 candidate_column_height = std::max(candidate_column_height, 248 text_size.height()); 249 } 250 251 return gfx::Size(candidate_column_width, candidate_column_height); 252 } 253 254 // Computes annotation column size. 255 gfx::Size ComputeAnnotationColumnSize( 256 const IBusLookupTable& lookup_table, const ui::NativeTheme& theme) { 257 int annotation_column_width = 0; 258 int annotation_column_height = 0; 259 scoped_ptr<views::Label> annotation_label( 260 CreateAnnotationLabel(lookup_table.orientation(), theme)); 261 262 // Compute the start index of |lookup_table_|. 263 const int current_page_index = ComputePageIndex(lookup_table); 264 if (current_page_index < 0) 265 return gfx::Size(0, 0); 266 const size_t start_from = current_page_index * lookup_table.page_size(); 267 268 // Compute max width and height in annotation labels. 269 // We'll create temporary annotation labels, and choose the largest width and 270 // height. 271 for (size_t i = 0; i + start_from < lookup_table.candidates().size(); ++i) { 272 const size_t index = start_from + i; 273 274 annotation_label->SetText( 275 UTF8ToUTF16(lookup_table.candidates()[index].annotation)); 276 gfx::Size text_size = annotation_label->GetPreferredSize(); 277 annotation_column_width = std::max(annotation_column_width, 278 text_size.width()); 279 annotation_column_height = std::max(annotation_column_height, 280 text_size.height()); 281 } 282 283 return gfx::Size(annotation_column_width, annotation_column_height); 284 } 285 286 } // namespace 287 288 // InformationTextArea is a HidableArea having a single Label in it. 289 class InformationTextArea : public HidableArea { 290 public: 291 // Specify the alignment and initialize the control. 292 InformationTextArea(gfx::HorizontalAlignment align, int minWidth) 293 : minWidth_(minWidth) { 294 label_ = new views::Label; 295 label_->SetHorizontalAlignment(align); 296 297 const gfx::Insets kInsets(2, 2, 2, 4); 298 views::View* contents = WrapWithPadding(label_, kInsets); 299 SetContents(contents); 300 contents->set_border(views::Border::CreateSolidBorder( 301 1, 302 GetNativeTheme()->GetSystemColor( 303 ui::NativeTheme::kColorId_MenuBorderColor))); 304 contents->set_background(views::Background::CreateSolidBackground( 305 color_utils::AlphaBlend(SK_ColorBLACK, 306 GetNativeTheme()->GetSystemColor( 307 ui::NativeTheme::kColorId_WindowBackground), 308 0x10))); 309 label_->SetBackgroundColor(contents->background()->get_color()); 310 } 311 312 // Set the displayed text. 313 void SetText(const std::string& utf8_text) { 314 label_->SetText(UTF8ToUTF16(utf8_text)); 315 } 316 317 protected: 318 virtual gfx::Size GetPreferredSize() OVERRIDE { 319 gfx::Size size = HidableArea::GetPreferredSize(); 320 // Hack. +2 is needed as the same reason as in VerticalCandidateLabel 321 size.set_width(size.width() + 2); 322 if (size.width() < minWidth_) { 323 size.set_width(minWidth_); 324 } 325 return size; 326 } 327 328 private: 329 views::Label* label_; 330 int minWidth_; 331 332 DISALLOW_COPY_AND_ASSIGN(InformationTextArea); 333 }; 334 335 CandidateView::CandidateView( 336 CandidateWindowView* parent_candidate_window, 337 int index_in_page, 338 IBusLookupTable::Orientation orientation) 339 : index_in_page_(index_in_page), 340 orientation_(orientation), 341 parent_candidate_window_(parent_candidate_window), 342 shortcut_label_(NULL), 343 candidate_label_(NULL), 344 annotation_label_(NULL), 345 infolist_icon_(NULL), 346 infolist_icon_enabled_(false) { 347 } 348 349 void CandidateView::Init(int shortcut_column_width, 350 int candidate_column_width, 351 int annotation_column_width, 352 int column_height) { 353 views::GridLayout* layout = new views::GridLayout(this); 354 SetLayoutManager(layout); // |this| owns |layout|. 355 356 // Create Labels. 357 const ui::NativeTheme& theme = *GetNativeTheme(); 358 shortcut_label_ = CreateShortcutLabel(orientation_, theme); 359 views::View* wrapped_shortcut_label = 360 CreateWrappedShortcutLabel(shortcut_label_, orientation_, theme); 361 candidate_label_ = CreateCandidateLabel(orientation_); 362 annotation_label_ = CreateAnnotationLabel(orientation_, theme); 363 364 // Initialize the column set with three columns. 365 views::ColumnSet* column_set = layout->AddColumnSet(0); 366 367 // If orientation is vertical, each column width is fixed. 368 // Otherwise the width is resizable. 369 const views::GridLayout::SizeType column_type = 370 orientation_ == IBusLookupTable::VERTICAL ? 371 views::GridLayout::FIXED : views::GridLayout::USE_PREF; 372 373 const int padding_column_width = 374 orientation_ == IBusLookupTable::VERTICAL ? 4 : 6; 375 376 // Set shortcut column type and width. 377 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 378 0, column_type, shortcut_column_width, 0); 379 column_set->AddPaddingColumn(0, padding_column_width); 380 381 // Set candidate column type and width. 382 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 383 1, views::GridLayout::USE_PREF, 0, 384 orientation_ == IBusLookupTable::VERTICAL ? 385 candidate_column_width : 0); 386 column_set->AddPaddingColumn(0, padding_column_width); 387 388 // Set annotation column type and width. 389 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 390 0, column_type, annotation_column_width, 0); 391 392 if (orientation_ == IBusLookupTable::VERTICAL) { 393 column_set->AddPaddingColumn(0, 1); 394 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 0, 395 views::GridLayout::FIXED, kInfolistIndicatorIconWidth, 396 0); 397 column_set->AddPaddingColumn(0, 2); 398 } else { 399 column_set->AddPaddingColumn(0, padding_column_width); 400 } 401 402 // Add the shortcut label, the candidate label, and annotation label. 403 layout->StartRow(0, 0); 404 // |wrapped_shortcut_label|, |candidate_label_|, and |annotation_label_| 405 // will be owned by |this|. 406 layout->AddView(wrapped_shortcut_label, 407 1, // Column span. 408 1, // Row span. 409 views::GridLayout::FILL, // Horizontal alignment. 410 views::GridLayout::FILL, // Vertical alignment. 411 -1, // Preferred width, not specified. 412 column_height); // Preferred height. 413 layout->AddView(candidate_label_, 414 1, // Column span. 415 1, // Row span. 416 views::GridLayout::FILL, // Horizontal alignment. 417 views::GridLayout::FILL, // Vertical alignment. 418 -1, // Preferred width, not specified. 419 column_height); // Preferred height. 420 layout->AddView(annotation_label_, 421 1, // Column span. 422 1, // Row span. 423 views::GridLayout::FILL, // Horizontal alignment. 424 views::GridLayout::FILL, // Vertical alignemnt. 425 -1, // Preferred width, not specified. 426 column_height); // Preferred height. 427 if (orientation_ == IBusLookupTable::VERTICAL) { 428 infolist_icon_ = new views::View; 429 views::View* infolist_icon_wrapper = new views::View; 430 views::GridLayout* infolist_icon_layout = 431 new views::GridLayout(infolist_icon_wrapper); 432 // |infolist_icon_layout| is owned by |infolist_icon_wrapper|. 433 infolist_icon_wrapper->SetLayoutManager(infolist_icon_layout); 434 infolist_icon_layout->AddColumnSet(0)->AddColumn( 435 views::GridLayout::FILL, views::GridLayout::FILL, 436 0, views::GridLayout::FIXED, kInfolistIndicatorIconWidth, 0); 437 infolist_icon_layout->AddPaddingRow(0, kInfolistIndicatorIconPadding); 438 infolist_icon_layout->StartRow(1.0, 0); // infolist_icon_ is resizable. 439 // |infolist_icon_| is owned by |infolist_icon_wrapper|. 440 infolist_icon_layout->AddView(infolist_icon_); 441 infolist_icon_layout->AddPaddingRow(0, kInfolistIndicatorIconPadding); 442 // |infolist_icon_wrapper| is owned by |this|. 443 layout->AddView(infolist_icon_wrapper); 444 } 445 UpdateLabelBackgroundColors(); 446 } 447 448 void CandidateView::SetCandidateText(const string16& text) { 449 candidate_label_->SetText(text); 450 } 451 452 void CandidateView::SetShortcutText(const string16& text) { 453 shortcut_label_->SetText(text); 454 } 455 456 void CandidateView::SetAnnotationText(const string16& text) { 457 annotation_label_->SetText(text); 458 } 459 460 void CandidateView::SetInfolistIcon(bool enable) { 461 if (!infolist_icon_ || (infolist_icon_enabled_ == enable)) 462 return; 463 infolist_icon_enabled_ = enable; 464 infolist_icon_->set_background( 465 enable ? 466 views::Background::CreateSolidBackground(GetNativeTheme()->GetSystemColor( 467 ui::NativeTheme::kColorId_FocusedBorderColor)) : 468 NULL); 469 UpdateLabelBackgroundColors(); 470 SchedulePaint(); 471 } 472 473 void CandidateView::Select() { 474 set_background( 475 views::Background::CreateSolidBackground(GetNativeTheme()->GetSystemColor( 476 ui::NativeTheme::kColorId_TextfieldSelectionBackgroundFocused))); 477 set_border(views::Border::CreateSolidBorder( 478 1, GetNativeTheme()->GetSystemColor( 479 ui::NativeTheme::kColorId_FocusedBorderColor))); 480 UpdateLabelBackgroundColors(); 481 // Need to call SchedulePaint() for background and border color changes. 482 SchedulePaint(); 483 } 484 485 void CandidateView::Unselect() { 486 set_background(NULL); 487 set_border(NULL); 488 UpdateLabelBackgroundColors(); 489 SchedulePaint(); // See comments at Select(). 490 } 491 492 void CandidateView::SetRowEnabled(bool enabled) { 493 shortcut_label_->SetEnabled(enabled); 494 } 495 496 gfx::Point CandidateView::GetCandidateLabelPosition() const { 497 return candidate_label_->GetMirroredPosition(); 498 } 499 500 bool CandidateView::OnMousePressed(const ui::MouseEvent& event) { 501 // TODO(kinaba): On Windows and MacOS, candidate windows typically commits a 502 // candidate at OnMouseReleased event. We have chosen OnMousePressed here for 503 // working around several obstacle rising from views implementation over GTK. 504 // See: http://crosbug.com/11423#c11. Since we have moved from GTK to Aura, 505 // the reasoning should have became obsolete. We might want to reconsider 506 // implementing mouse-up selection. 507 SelectCandidateAt(event.location()); 508 return false; 509 } 510 511 void CandidateView::OnGestureEvent(ui::GestureEvent* event) { 512 if (event->type() == ui::ET_GESTURE_TAP) { 513 SelectCandidateAt(event->location()); 514 event->SetHandled(); 515 return; 516 } 517 View::OnGestureEvent(event); 518 } 519 520 void CandidateView::SelectCandidateAt(const gfx::Point& location) { 521 gfx::Point location_in_candidate_window = location; 522 views::View::ConvertPointToTarget(this, parent_candidate_window_, 523 &location_in_candidate_window); 524 parent_candidate_window_->OnCandidatePressed(location_in_candidate_window); 525 parent_candidate_window_->CommitCandidate(); 526 } 527 528 void CandidateView::UpdateLabelBackgroundColors() { 529 SkColor color = background() ? 530 background()->get_color() : 531 GetNativeTheme()->GetSystemColor( 532 ui::NativeTheme::kColorId_WindowBackground); 533 if (orientation_ != IBusLookupTable::VERTICAL) 534 shortcut_label_->SetBackgroundColor(color); 535 candidate_label_->SetBackgroundColor(color); 536 annotation_label_->SetBackgroundColor(color); 537 } 538 539 CandidateWindowView::CandidateWindowView(views::Widget* parent_frame) 540 : selected_candidate_index_in_page_(-1), 541 parent_frame_(parent_frame), 542 preedit_area_(NULL), 543 header_area_(NULL), 544 candidate_area_(NULL), 545 footer_area_(NULL), 546 previous_shortcut_column_size_(0, 0), 547 previous_candidate_column_size_(0, 0), 548 previous_annotation_column_size_(0, 0), 549 should_show_at_composition_head_(false), 550 should_show_upper_side_(false), 551 was_candidate_window_open_(false) { 552 } 553 554 CandidateWindowView::~CandidateWindowView() { 555 } 556 557 void CandidateWindowView::Init() { 558 // Set the background and the border of the view. 559 set_background( 560 views::Background::CreateSolidBackground(GetNativeTheme()->GetSystemColor( 561 ui::NativeTheme::kColorId_WindowBackground))); 562 set_border(views::Border::CreateSolidBorder( 563 1, GetNativeTheme()->GetSystemColor( 564 ui::NativeTheme::kColorId_MenuBorderColor))); 565 566 // Create areas. 567 preedit_area_ = new InformationTextArea(gfx::ALIGN_LEFT, 568 kMinPreeditAreaWidth); 569 header_area_ = new InformationTextArea(gfx::ALIGN_LEFT, 0); 570 candidate_area_ = new HidableArea; 571 candidate_area_->SetContents(new views::View); 572 footer_area_ = new InformationTextArea(gfx::ALIGN_RIGHT, 0); 573 574 // Set the window layout of the view 575 views::GridLayout* layout = new views::GridLayout(this); 576 SetLayoutManager(layout); // |this| owns |layout|. 577 views::ColumnSet* column_set = layout->AddColumnSet(0); 578 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 579 0, views::GridLayout::USE_PREF, 0, 0); 580 581 // Add the preedit area 582 layout->StartRow(0, 0); 583 layout->AddView(preedit_area_); // |preedit_area_| is owned by |this|. 584 585 // Add the header area. 586 layout->StartRow(0, 0); 587 layout->AddView(header_area_); // |header_area_| is owned by |this|. 588 589 // Add the candidate area. 590 layout->StartRow(0, 0); 591 layout->AddView(candidate_area_); // |candidate_area_| is owned by |this|. 592 593 // Add the footer area. 594 layout->StartRow(0, 0); 595 layout->AddView(footer_area_); // |footer_area_| is owned by |this|. 596 } 597 598 void CandidateWindowView::HideAll() { 599 parent_frame_->Hide(); 600 NotifyIfCandidateWindowOpenedOrClosed(); 601 } 602 603 void CandidateWindowView::UpdateParentArea() { 604 if (candidate_area_->IsShown() || 605 header_area_->IsShown() || 606 footer_area_->IsShown() || 607 preedit_area_->IsShown()) { 608 ResizeAndMoveParentFrame(); 609 parent_frame_->Show(); 610 } else { 611 parent_frame_->Hide(); 612 } 613 NotifyIfCandidateWindowOpenedOrClosed(); 614 } 615 616 void CandidateWindowView::HideLookupTable() { 617 candidate_area_->Hide(); 618 UpdateParentArea(); 619 } 620 621 void CandidateWindowView::HideAuxiliaryText() { 622 header_area_->Hide(); 623 footer_area_->Hide(); 624 UpdateParentArea(); 625 } 626 627 void CandidateWindowView::ShowAuxiliaryText() { 628 // If candidate_area is not shown, shows auxiliary text at header_area. 629 // We expect both header_area_ and footer_area_ contain same value. 630 if (!candidate_area_->IsShown()) { 631 header_area_->Show(); 632 footer_area_->Hide(); 633 } else { 634 // If candidate_area is shown, shows auxiliary text with orientation. 635 if (lookup_table_.orientation() == IBusLookupTable::HORIZONTAL) { 636 header_area_->Show(); 637 footer_area_->Hide(); 638 } else { 639 footer_area_->Show(); 640 header_area_->Hide(); 641 } 642 } 643 UpdateParentArea(); 644 } 645 646 void CandidateWindowView::UpdateAuxiliaryText(const std::string& utf8_text) { 647 header_area_->SetText(utf8_text); 648 footer_area_->SetText(utf8_text); 649 ShowAuxiliaryText(); 650 } 651 652 void CandidateWindowView::HidePreeditText() { 653 preedit_area_->Hide(); 654 UpdateParentArea(); 655 } 656 657 void CandidateWindowView::ShowPreeditText() { 658 preedit_area_->Show(); 659 UpdateParentArea(); 660 } 661 662 void CandidateWindowView::UpdatePreeditText(const std::string& utf8_text) { 663 preedit_area_->SetText(utf8_text); 664 } 665 666 void CandidateWindowView::ShowLookupTable() { 667 if (!candidate_area_->IsShown()) 668 should_show_upper_side_ = false; 669 candidate_area_->Show(); 670 UpdateParentArea(); 671 } 672 673 void CandidateWindowView::NotifyIfCandidateWindowOpenedOrClosed() { 674 bool is_open = IsCandidateWindowOpen(); 675 if (!was_candidate_window_open_ && is_open) { 676 FOR_EACH_OBSERVER(Observer, observers_, OnCandidateWindowOpened()); 677 } else if (was_candidate_window_open_ && !is_open) { 678 FOR_EACH_OBSERVER(Observer, observers_, OnCandidateWindowClosed()); 679 } 680 was_candidate_window_open_ = is_open; 681 } 682 683 bool CandidateWindowView::ShouldUpdateCandidateViews( 684 const IBusLookupTable& old_table, 685 const IBusLookupTable& new_table) { 686 return !old_table.IsEqual(new_table); 687 } 688 689 void CandidateWindowView::UpdateCandidates( 690 const IBusLookupTable& new_lookup_table) { 691 const bool should_update = ShouldUpdateCandidateViews(lookup_table_, 692 new_lookup_table); 693 // Updating the candidate views is expensive. We'll skip this if possible. 694 if (should_update) { 695 // Initialize candidate views if necessary. 696 MaybeInitializeCandidateViews(new_lookup_table); 697 698 should_show_at_composition_head_ 699 = new_lookup_table.show_window_at_composition(); 700 // Compute the index of the current page. 701 const int current_page_index = ComputePageIndex(new_lookup_table); 702 if (current_page_index < 0) { 703 return; 704 } 705 706 // Update the candidates in the current page. 707 const size_t start_from = current_page_index * new_lookup_table.page_size(); 708 709 // In some cases, engines send empty shortcut labels. For instance, 710 // ibus-mozc sends empty labels when they show suggestions. In this 711 // case, we should not show shortcut labels. 712 bool no_shortcut_mode = true; 713 for (size_t i = 0; i < new_lookup_table.candidates().size(); ++i) { 714 if (!new_lookup_table.candidates()[i].label.empty()) { 715 no_shortcut_mode = false; 716 break; 717 } 718 } 719 720 for (size_t i = 0; i < candidate_views_.size(); ++i) { 721 const size_t index_in_page = i; 722 const size_t candidate_index = start_from + index_in_page; 723 CandidateView* candidate_view = candidate_views_[index_in_page]; 724 // Set the shortcut text. 725 if (no_shortcut_mode) { 726 candidate_view->SetShortcutText(string16()); 727 } else { 728 // At this moment, we don't use labels sent from engines for UX 729 // reasons. First, we want to show shortcut labels in empty rows 730 // (ex. show 6, 7, 8, ... in empty rows when the number of 731 // candidates is 5). Second, we want to add a period after each 732 // shortcut label when the candidate window is horizontal. 733 candidate_view->SetShortcutText( 734 CreateShortcutText(i, new_lookup_table)); 735 } 736 // Set the candidate text. 737 if (candidate_index < new_lookup_table.candidates().size()) { 738 const IBusLookupTable::Entry& entry = 739 new_lookup_table.candidates()[candidate_index]; 740 candidate_view->SetCandidateText(UTF8ToUTF16(entry.value)); 741 candidate_view->SetAnnotationText(UTF8ToUTF16(entry.annotation)); 742 candidate_view->SetRowEnabled(true); 743 candidate_view->SetInfolistIcon(!entry.description_title.empty()); 744 } else { 745 // Disable the empty row. 746 candidate_view->SetCandidateText(string16()); 747 candidate_view->SetAnnotationText(string16()); 748 candidate_view->SetRowEnabled(false); 749 candidate_view->SetInfolistIcon(false); 750 } 751 } 752 } 753 // Update the current lookup table. We'll use lookup_table_ from here. 754 // Note that SelectCandidateAt() uses lookup_table_. 755 lookup_table_.CopyFrom(new_lookup_table); 756 757 // Select the current candidate in the page. 758 if (lookup_table_.is_cursor_visible()) { 759 if (lookup_table_.page_size()) { 760 const int current_candidate_in_page = 761 lookup_table_.cursor_position() % lookup_table_.page_size(); 762 SelectCandidateAt(current_candidate_in_page); 763 } 764 } else { 765 // Unselect the currently selected candidate. 766 if (0 <= selected_candidate_index_in_page_ && 767 static_cast<size_t>(selected_candidate_index_in_page_) < 768 candidate_views_.size()) { 769 candidate_views_[selected_candidate_index_in_page_]->Unselect(); 770 selected_candidate_index_in_page_ = -1; 771 } 772 } 773 } 774 775 void CandidateWindowView::MaybeInitializeCandidateViews( 776 const IBusLookupTable& lookup_table) { 777 const IBusLookupTable::Orientation orientation = 778 lookup_table.orientation(); 779 const int page_size = lookup_table.page_size(); 780 views::View* candidate_area_contents = candidate_area_->contents(); 781 782 // Current column width. 783 gfx::Size shortcut_column_size(0, 0); 784 gfx::Size candidate_column_size(0,0); 785 gfx::Size annotation_column_size(0, 0); 786 787 // If orientation is horizontal, don't need to compute width, 788 // because each label is left aligned. 789 if (orientation == IBusLookupTable::VERTICAL) { 790 const ui::NativeTheme& theme = *GetNativeTheme(); 791 shortcut_column_size = ComputeShortcutColumnSize(lookup_table, theme); 792 candidate_column_size = ComputeCandidateColumnSize(lookup_table); 793 annotation_column_size = ComputeAnnotationColumnSize(lookup_table, theme); 794 } 795 796 // If the requested number of views matches the number of current views, and 797 // previous and current column width are same, just reuse these. 798 // 799 // Note that the early exit logic is not only useful for improving 800 // performance, but also necessary for the horizontal candidate window 801 // to be redrawn properly. If we get rid of the logic, the horizontal 802 // candidate window won't get redrawn properly for some reason when 803 // there is no size change. You can test this by removing "return" here 804 // and type "ni" with Pinyin input method. 805 if (static_cast<int>(candidate_views_.size()) == page_size && 806 lookup_table_.orientation() == orientation && 807 previous_shortcut_column_size_ == shortcut_column_size && 808 previous_candidate_column_size_ == candidate_column_size && 809 previous_annotation_column_size_ == annotation_column_size) { 810 return; 811 } 812 813 // Update the previous column widths. 814 previous_shortcut_column_size_ = shortcut_column_size; 815 previous_candidate_column_size_ = candidate_column_size; 816 previous_annotation_column_size_ = annotation_column_size; 817 818 // Clear the existing candidate_views if any. 819 for (size_t i = 0; i < candidate_views_.size(); ++i) { 820 candidate_area_contents->RemoveChildView(candidate_views_[i]); 821 // Delete the view after getting out the current message loop iteration. 822 base::MessageLoop::current()->DeleteSoon(FROM_HERE, candidate_views_[i]); 823 } 824 candidate_views_.clear(); 825 selected_candidate_index_in_page_ = -1; // Invalidates the index. 826 827 views::GridLayout* layout = new views::GridLayout(candidate_area_contents); 828 // |candidate_area_contents| owns |layout|. 829 candidate_area_contents->SetLayoutManager(layout); 830 // Initialize the column set. 831 views::ColumnSet* column_set = layout->AddColumnSet(0); 832 if (orientation == IBusLookupTable::VERTICAL) { 833 column_set->AddColumn(views::GridLayout::FILL, 834 views::GridLayout::FILL, 835 1, views::GridLayout::USE_PREF, 0, 0); 836 } else { 837 for (int i = 0; i < page_size; ++i) { 838 column_set->AddColumn(views::GridLayout::FILL, 839 views::GridLayout::FILL, 840 0, views::GridLayout::USE_PREF, 0, 0); 841 } 842 } 843 844 // Set insets so the border of the selected candidate is drawn inside of 845 // the border of the main candidate window, but we don't have the inset 846 // at the top and the bottom as we have the borders of the header and 847 // footer areas. 848 const gfx::Insets kCandidateAreaInsets(0, 1, 0, 1); 849 layout->SetInsets(kCandidateAreaInsets.top(), 850 kCandidateAreaInsets.left(), 851 kCandidateAreaInsets.bottom(), 852 kCandidateAreaInsets.right()); 853 854 // Use maximum height for all rows in candidate area. 855 const int kColumnHeight = std::max(shortcut_column_size.height(), 856 std::max(candidate_column_size.height(), 857 annotation_column_size.height())); 858 859 // Add views to the candidate area. 860 if (orientation == IBusLookupTable::HORIZONTAL) { 861 layout->StartRow(0, 0); 862 } 863 864 for (int i = 0; i < page_size; ++i) { 865 CandidateView* candidate_row = new CandidateView(this, i, orientation); 866 candidate_row->Init(shortcut_column_size.width(), 867 candidate_column_size.width(), 868 annotation_column_size.width(), 869 kColumnHeight); 870 candidate_views_.push_back(candidate_row); 871 if (orientation == IBusLookupTable::VERTICAL) { 872 layout->StartRow(0, 0); 873 } 874 // |candidate_row| will be owned by |candidate_area_contents|. 875 layout->AddView(candidate_row, 876 1, // Column span. 877 1, // Row span. 878 // Horizontal alignment. 879 orientation == IBusLookupTable::VERTICAL ? 880 views::GridLayout::FILL : views::GridLayout::CENTER, 881 views::GridLayout::CENTER, // Vertical alignment. 882 -1, // Preferred width, not specified. 883 kColumnHeight); // Preferred height. 884 } 885 886 // Compute views size in |layout|. 887 // If we don't call this function, GetHorizontalOffset() often 888 // returns invalid value (returns 0), then candidate window 889 // moves right from the correct position in ResizeAndMoveParentFrame(). 890 // TODO(nhiroki): Figure out why it returns invalid value. 891 // It seems that the x-position of the candidate labels is not set. 892 layout->Layout(candidate_area_contents); 893 } 894 895 bool CandidateWindowView::IsCandidateWindowOpen() const { 896 return !should_show_at_composition_head_ && 897 candidate_area_->visible() && candidate_area_->IsShown(); 898 } 899 900 void CandidateWindowView::SelectCandidateAt(int index_in_page) { 901 const int current_page_index = ComputePageIndex(lookup_table_); 902 if (current_page_index < 0) { 903 return; 904 } 905 906 const int cursor_absolute_index = 907 lookup_table_.page_size() * current_page_index + index_in_page; 908 // Ignore click on out of range views. 909 if (cursor_absolute_index < 0 || 910 lookup_table_.candidates().size() <= 911 static_cast<size_t>(cursor_absolute_index)) { 912 return; 913 } 914 915 // Unselect the currently selected candidate. 916 if (0 <= selected_candidate_index_in_page_ && 917 static_cast<size_t>(selected_candidate_index_in_page_) < 918 candidate_views_.size()) { 919 candidate_views_[selected_candidate_index_in_page_]->Unselect(); 920 } 921 // Remember the currently selected candidate index in the current page. 922 selected_candidate_index_in_page_ = index_in_page; 923 924 // Select the candidate specified by index_in_page. 925 candidate_views_[index_in_page]->Select(); 926 927 // Update the cursor indexes in the model. 928 lookup_table_.set_cursor_position(cursor_absolute_index); 929 } 930 931 void CandidateWindowView::OnCandidatePressed( 932 const gfx::Point& location) { 933 for (size_t i = 0; i < candidate_views_.size(); ++i) { 934 gfx::Point converted_location = location; 935 views::View::ConvertPointToTarget(this, candidate_views_[i], 936 &converted_location); 937 if (candidate_views_[i]->HitTestPoint(converted_location)) { 938 SelectCandidateAt(i); 939 break; 940 } 941 } 942 } 943 944 void CandidateWindowView::CommitCandidate() { 945 if (!(0 <= selected_candidate_index_in_page_ && 946 static_cast<size_t>(selected_candidate_index_in_page_) < 947 candidate_views_.size())) { 948 return; // Out of range, do nothing. 949 } 950 951 // For now, we don't distinguish left and right clicks. 952 const int button = 1; // Left button. 953 const int key_modifilers = 0; 954 FOR_EACH_OBSERVER(Observer, observers_, 955 OnCandidateCommitted(selected_candidate_index_in_page_, 956 button, 957 key_modifilers)); 958 } 959 960 void CandidateWindowView::ResizeAndMoveParentFrame() { 961 // If rendering operation comes from mozc-engine, uses mozc specific location, 962 // otherwise lookup table is shown under the cursor. 963 const int x = should_show_at_composition_head_? 964 composition_head_location_.x() : cursor_location_.x(); 965 // To avoid lookup-table overlapping, uses maximum y-position of mozc specific 966 // location and cursor location, because mozc-engine does not consider about 967 // multi-line composition. 968 const int y = should_show_at_composition_head_? 969 std::max(composition_head_location_.y(), cursor_location_.y()) : 970 cursor_location_.y(); 971 const int height = cursor_location_.height(); 972 const int horizontal_offset = GetHorizontalOffset(); 973 974 gfx::Rect old_bounds = parent_frame_->GetClientAreaBoundsInScreen(); 975 gfx::Rect screen_bounds = ash::Shell::GetScreen()->GetDisplayMatching( 976 cursor_location_).work_area(); 977 // The size. 978 gfx::Rect frame_bounds = old_bounds; 979 frame_bounds.set_size(GetPreferredSize()); 980 981 // The default position. 982 frame_bounds.set_x(x + horizontal_offset); 983 frame_bounds.set_y(y + height); 984 985 // Handle overflow at the left and the top. 986 frame_bounds.set_x(std::max(frame_bounds.x(), screen_bounds.x())); 987 frame_bounds.set_y(std::max(frame_bounds.y(), screen_bounds.y())); 988 989 // Handle overflow at the right. 990 const int right_overflow = frame_bounds.right() - screen_bounds.right(); 991 if (right_overflow > 0) { 992 frame_bounds.set_x(frame_bounds.x() - right_overflow); 993 } 994 995 // Handle overflow at the bottom. 996 const int bottom_overflow = frame_bounds.bottom() - screen_bounds.bottom(); 997 998 // To avoid flickering window position, the candidate window should be shown 999 // on upper side of composition string if it was shown there. 1000 if (should_show_upper_side_ || bottom_overflow > 0) { 1001 frame_bounds.set_y(frame_bounds.y() - height - frame_bounds.height()); 1002 should_show_upper_side_ = true; 1003 } 1004 1005 // TODO(nona): check top_overflow here. 1006 1007 // Move the window per the cursor location. 1008 // SetBounds() is not cheap. Only call this when it is really changed. 1009 if (frame_bounds != old_bounds) 1010 parent_frame_->SetBounds(frame_bounds); 1011 } 1012 1013 int CandidateWindowView::GetHorizontalOffset() { 1014 // Compute the horizontal offset if the lookup table is vertical. 1015 if (!candidate_views_.empty() && 1016 lookup_table_.orientation() == IBusLookupTable::VERTICAL) { 1017 return - candidate_views_[0]->GetCandidateLabelPosition().x(); 1018 } 1019 return 0; 1020 } 1021 1022 void CandidateWindowView::VisibilityChanged(View* starting_from, 1023 bool is_visible) { 1024 if (is_visible) { 1025 // If the visibility of candidate window is changed, 1026 // we should move the frame to the right position. 1027 ResizeAndMoveParentFrame(); 1028 } 1029 } 1030 1031 void CandidateWindowView::OnBoundsChanged(const gfx::Rect& previous_bounds) { 1032 // If the bounds(size) of candidate window is changed, 1033 // we should move the frame to the right position. 1034 View::OnBoundsChanged(previous_bounds); 1035 ResizeAndMoveParentFrame(); 1036 } 1037 1038 } // namespace input_method 1039 } // namespace chromeos 1040