1 // Copyright (c) 2011 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 "chrome/browser/chromeos/input_method/candidate_window.h" 6 7 #include <algorithm> 8 #include <string> 9 #include <vector> 10 11 #include "base/logging.h" 12 #include "base/memory/scoped_ptr.h" 13 #include "base/observer_list.h" 14 #include "base/string_util.h" 15 #include "base/stringprintf.h" 16 #include "base/utf_string_conversions.h" 17 #include "third_party/cros/chromeos_input_method_ui.h" 18 #include "ui/gfx/canvas.h" 19 #include "ui/gfx/font.h" 20 #include "views/controls/label.h" 21 #include "views/controls/textfield/textfield.h" 22 #include "views/events/event.h" 23 #include "views/layout/fill_layout.h" 24 #include "views/layout/grid_layout.h" 25 #include "views/screen.h" 26 #include "views/widget/root_view.h" 27 #include "views/widget/widget.h" 28 #include "views/widget/widget_gtk.h" 29 #include "views/window/non_client_view.h" 30 #include "views/window/window.h" 31 #include "views/window/window_delegate.h" 32 33 namespace { 34 35 // Colors used in the candidate window UI. 36 const SkColor kFrameColor = SkColorSetRGB(0x96, 0x96, 0x96); 37 const SkColor kShortcutBackgroundColor = SkColorSetARGB(0x10, 0x3, 0x4, 0xf); 38 const SkColor kSelectedRowBackgroundColor = SkColorSetRGB(0xd1, 0xea, 0xff); 39 const SkColor kDefaultBackgroundColor = SkColorSetRGB(0xff, 0xff, 0xff); 40 const SkColor kSelectedRowFrameColor = SkColorSetRGB(0x7f, 0xac, 0xdd); 41 const SkColor kFooterTopColor = SkColorSetRGB(0xff, 0xff, 0xff); 42 const SkColor kFooterBottomColor = SkColorSetRGB(0xee, 0xee, 0xee); 43 const SkColor kShortcutColor = SkColorSetRGB(0x61, 0x61, 0x61); 44 const SkColor kDisabledShortcutColor = SkColorSetRGB(0xcc, 0xcc, 0xcc); 45 const SkColor kAnnotationColor = SkColorSetRGB(0x88, 0x88, 0x88); 46 47 // We'll use a bigger font size, so Chinese characters are more readable 48 // in the candidate window. 49 #if defined(CROS_FONTS_USING_BCI) 50 const int kFontSizeDelta = 1; 51 #else 52 const int kFontSizeDelta = 2; 53 #endif 54 55 // The minimum width of candidate labels in the vertical candidate 56 // window. We use this value to prevent the candidate window from being 57 // too narrow when all candidates are short. 58 const int kMinCandidateLabelWidth = 100; 59 // The maximum width of candidate labels in the vertical candidate 60 // window. We use this value to prevent the candidate window from being 61 // too wide when one of candidates are long. 62 const int kMaxCandidateLabelWidth = 500; 63 64 // VerticalCandidateLabel is used for rendering candidate text in 65 // the vertical candidate window. 66 class VerticalCandidateLabel : public views::Label { 67 virtual ~VerticalCandidateLabel() {} 68 69 // Returns the preferred size, but guarantees that the width has at 70 // least kMinCandidateLabelWidth pixels. 71 virtual gfx::Size GetPreferredSize() { 72 gfx::Size size = Label::GetPreferredSize(); 73 // Hack. +2 is needed to prevent labels from getting elided like 74 // "abc..." in some cases. TODO(satorux): Figure out why it's 75 // necessary. 76 size.set_width(size.width() + 2); 77 if (size.width() < kMinCandidateLabelWidth) { 78 size.set_width(kMinCandidateLabelWidth); 79 } 80 if (size.width() > kMaxCandidateLabelWidth) { 81 size.set_width(kMaxCandidateLabelWidth); 82 } 83 return size; 84 } 85 }; 86 87 // Wraps the given view with some padding, and returns it. 88 views::View* WrapWithPadding(views::View* view, const gfx::Insets& insets) { 89 views::View* wrapper = new views::View; 90 // Use GridLayout to give some insets inside. 91 views::GridLayout* layout = new views::GridLayout(wrapper); 92 wrapper->SetLayoutManager(layout); // |wrapper| owns |layout|. 93 layout->SetInsets(insets); 94 95 views::ColumnSet* column_set = layout->AddColumnSet(0); 96 column_set->AddColumn( 97 views::GridLayout::FILL, views::GridLayout::FILL, 98 1, views::GridLayout::USE_PREF, 0, 0); 99 layout->StartRow(0, 0); 100 101 // Add the view contents. 102 layout->AddView(view); // |view| is owned by |wraper|, not |layout|. 103 return wrapper; 104 } 105 106 // Creates shortcut text from the given index and the orientation. 107 std::wstring CreateShortcutText(int index, 108 chromeos::InputMethodLookupTable::Orientation orientation) { 109 // Choose the character used for the shortcut label. 110 const wchar_t kShortcutCharacters[] = L"1234567890ABCDEF"; 111 // The default character should not be used but just in case. 112 wchar_t shortcut_character = L'?'; 113 // -1 to exclude the null character at the end. 114 if (index < static_cast<int>(arraysize(kShortcutCharacters) - 1)) { 115 shortcut_character = kShortcutCharacters[index]; 116 } 117 118 std::wstring shortcut_text; 119 if (orientation == chromeos::InputMethodLookupTable::kVertical) { 120 shortcut_text = base::StringPrintf(L"%lc", shortcut_character); 121 } else { 122 shortcut_text = base::StringPrintf(L"%lc.", shortcut_character); 123 } 124 125 return shortcut_text; 126 } 127 128 // Creates the shortcut label, and returns it (never returns NULL). 129 // The label text is not set in this function. 130 views::Label* CreateShortcutLabel( 131 chromeos::InputMethodLookupTable::Orientation orientation) { 132 // Create the shortcut label. The label will be owned by 133 // |wrapped_shortcut_label|, hence it's deleted when 134 // |wrapped_shortcut_label| is deleted. 135 views::Label* shortcut_label = new views::Label; 136 137 if (orientation == chromeos::InputMethodLookupTable::kVertical) { 138 shortcut_label->SetFont( 139 shortcut_label->font().DeriveFont(kFontSizeDelta, gfx::Font::BOLD)); 140 } else { 141 shortcut_label->SetFont( 142 shortcut_label->font().DeriveFont(kFontSizeDelta)); 143 } 144 // TODO(satorux): Maybe we need to use language specific fonts for 145 // candidate_label, like Chinese font for Chinese input method? 146 shortcut_label->SetColor(kShortcutColor); 147 148 return shortcut_label; 149 } 150 151 // Wraps the shortcut label, then decorates wrapped shortcut label 152 // and returns it (never returns NULL). 153 // The label text is not set in this function. 154 views::View* CreateWrappedShortcutLabel(views::Label* shortcut_label, 155 chromeos::InputMethodLookupTable::Orientation orientation) { 156 // Wrap it with padding. 157 const gfx::Insets kVerticalShortcutLabelInsets(1, 6, 1, 6); 158 const gfx::Insets kHorizontalShortcutLabelInsets(1, 3, 1, 0); 159 const gfx::Insets insets = 160 (orientation == chromeos::InputMethodLookupTable::kVertical ? 161 kVerticalShortcutLabelInsets : 162 kHorizontalShortcutLabelInsets); 163 views::View* wrapped_shortcut_label = 164 WrapWithPadding(shortcut_label, insets); 165 166 // Add decoration based on the orientation. 167 if (orientation == chromeos::InputMethodLookupTable::kVertical) { 168 // Set the background color. 169 wrapped_shortcut_label->set_background( 170 views::Background::CreateSolidBackground( 171 kShortcutBackgroundColor)); 172 } 173 174 return wrapped_shortcut_label; 175 } 176 177 // Creates the candidate label, and returns it (never returns NULL). 178 // The label text is not set in this function. 179 views::Label* CreateCandidateLabel( 180 chromeos::InputMethodLookupTable::Orientation orientation) { 181 views::Label* candidate_label = NULL; 182 183 // Create the candidate label. The label will be added to |this| as a 184 // child view, hence it's deleted when |this| is deleted. 185 if (orientation == chromeos::InputMethodLookupTable::kVertical) { 186 candidate_label = new VerticalCandidateLabel; 187 } else { 188 candidate_label = new views::Label; 189 } 190 191 // Change the font size. 192 candidate_label->SetFont( 193 candidate_label->font().DeriveFont(kFontSizeDelta)); 194 candidate_label->SetHorizontalAlignment(views::Label::ALIGN_LEFT); 195 196 return candidate_label; 197 } 198 199 // Creates the annotation label, and return it (never returns NULL). 200 // The label text is not set in this function. 201 views::Label* CreateAnnotationLabel( 202 chromeos::InputMethodLookupTable::Orientation orientation) { 203 // Create the annotation label. 204 views::Label* annotation_label = new views::Label; 205 206 // Change the font size and color. 207 annotation_label->SetFont( 208 annotation_label->font().DeriveFont(kFontSizeDelta)); 209 annotation_label->SetColor(kAnnotationColor); 210 annotation_label->SetHorizontalAlignment(views::Label::ALIGN_LEFT); 211 212 return annotation_label; 213 } 214 215 // Computes shortcut column width. 216 int ComputeShortcutColumnWidth( 217 const chromeos::InputMethodLookupTable& lookup_table) { 218 int shortcut_column_width = 0; 219 // Create the shortcut label. The label will be owned by 220 // |wrapped_shortcut_label|, hence it's deleted when 221 // |wrapped_shortcut_label| is deleted. 222 views::Label* shortcut_label = CreateShortcutLabel(lookup_table.orientation); 223 scoped_ptr<views::View> wrapped_shortcut_label( 224 CreateWrappedShortcutLabel(shortcut_label, lookup_table.orientation)); 225 226 // Compute the max width in shortcut labels. 227 // We'll create temporary shortcut labels, and choose the largest width. 228 for (int i = 0; i < lookup_table.page_size; ++i) { 229 shortcut_label->SetText( 230 CreateShortcutText(i, lookup_table.orientation)); 231 shortcut_column_width = 232 std::max(shortcut_column_width, 233 wrapped_shortcut_label->GetPreferredSize().width()); 234 } 235 236 return shortcut_column_width; 237 } 238 239 // Computes the page index. For instance, if the page size is 9, and the 240 // cursor is pointing to 13th candidate, the page index will be 1 (2nd 241 // page, as the index is zero-origin). Returns -1 on error. 242 int ComputePageIndex(const chromeos::InputMethodLookupTable& lookup_table) { 243 if (lookup_table.page_size > 0) 244 return lookup_table.cursor_absolute_index / lookup_table.page_size; 245 return -1; 246 } 247 248 // Computes candidate column width. 249 int ComputeCandidateColumnWidth( 250 const chromeos::InputMethodLookupTable& lookup_table) { 251 int candidate_column_width = 0; 252 scoped_ptr<views::Label> candidate_label( 253 CreateCandidateLabel(lookup_table.orientation)); 254 255 // Compute the start index of |lookup_table_|. 256 const int current_page_index = ComputePageIndex(lookup_table); 257 if (current_page_index < 0) 258 return 0; 259 const size_t start_from = current_page_index * lookup_table.page_size; 260 261 // Compute the max width in candidate labels. 262 // We'll create temporary candidate labels, and choose the largest width. 263 for (size_t i = 0; i + start_from < lookup_table.candidates.size(); ++i) { 264 const size_t index = start_from + i; 265 266 candidate_label->SetText( 267 UTF8ToWide(lookup_table.candidates[index])); 268 candidate_column_width = 269 std::max(candidate_column_width, 270 candidate_label->GetPreferredSize().width()); 271 } 272 273 return candidate_column_width; 274 } 275 276 // Computes annotation column width. 277 int ComputeAnnotationColumnWidth( 278 const chromeos::InputMethodLookupTable& lookup_table) { 279 int annotation_column_width = 0; 280 scoped_ptr<views::Label> annotation_label( 281 CreateAnnotationLabel(lookup_table.orientation)); 282 283 // Compute the start index of |lookup_table_|. 284 const int current_page_index = ComputePageIndex(lookup_table); 285 if (current_page_index < 0) 286 return 0; 287 const size_t start_from = current_page_index * lookup_table.page_size; 288 289 // Compute max width in annotation labels. 290 // We'll create temporary annotation labels, and choose the largest width. 291 for (size_t i = 0; i + start_from < lookup_table.annotations.size(); ++i) { 292 const size_t index = start_from + i; 293 294 annotation_label->SetText( 295 UTF8ToWide(lookup_table.annotations[index])); 296 annotation_column_width = 297 std::max(annotation_column_width, 298 annotation_label->GetPreferredSize().width()); 299 } 300 301 return annotation_column_width; 302 } 303 304 } // namespace 305 306 namespace chromeos { 307 308 class CandidateView; 309 310 // CandidateWindowView is the main container of the candidate window UI. 311 class CandidateWindowView : public views::View { 312 public: 313 // The object can be monitored by the observer. 314 class Observer { 315 public: 316 virtual ~Observer() {} 317 // The function is called when a candidate is committed. 318 // See comments at NotifyCandidateClicke() in chromeos_input_method_ui.h for 319 // details about the parameters. 320 virtual void OnCandidateCommitted(int index, int button, int flag) = 0; 321 }; 322 323 explicit CandidateWindowView(views::Widget* parent_frame); 324 virtual ~CandidateWindowView() {} 325 void Init(); 326 327 // Adds the given observer. The ownership is not transferred. 328 void AddObserver(Observer* observer) { 329 observers_.AddObserver(observer); 330 } 331 332 // Removes the given observer. 333 void RemoveObserver(Observer* observer) { 334 observers_.RemoveObserver(observer); 335 } 336 337 // Selects the candidate specified by the index in the current page 338 // (zero-origin). Changes the appearance of the selected candidate, 339 // updates the information in the candidate window as needed. 340 void SelectCandidateAt(int index_in_page); 341 342 // The function is called when a candidate is being dragged. From the 343 // given point, locates the candidate under the mouse cursor, and 344 // selects it. 345 void OnCandidateDragged(const gfx::Point& point); 346 347 // Commits the candidate currently being selected. 348 void CommitCandidate(); 349 350 // Hides the lookup table. 351 void HideLookupTable(); 352 353 // Hides the auxiliary text. 354 void HideAuxiliaryText(); 355 356 // Shows the auxiliary text. 357 void ShowAuxiliaryText(); 358 359 // Updates the auxiliary text. 360 void UpdateAuxiliaryText(const std::string& utf8_text); 361 362 // Returns true if we should update candidate views in the window. For 363 // instance, if we are going to show the same candidates as before, we 364 // don't have to update candidate views. This happens when the user just 365 // moves the cursor in the same page in the candidate window. 366 bool ShouldUpdateCandidateViews( 367 const InputMethodLookupTable& old_table, 368 const InputMethodLookupTable& new_table); 369 370 // Updates candidates of the candidate window from |lookup_table|. 371 // Candidates are arranged per |orientation|. 372 void UpdateCandidates(const InputMethodLookupTable& lookup_table); 373 374 // Resizes and moves the parent frame. The two actions should be 375 // performed consecutively as resizing may require the candidate window 376 // to move. For instance, we may need to move the candidate window from 377 // below the cursor to above the cursor, if the candidate window becomes 378 // too big to be shown near the bottom of the screen. This function 379 // needs to be called when the visible contents of the candidate window 380 // are modified. 381 void ResizeAndMoveParentFrame(); 382 383 // Resizes the parent frame per the current contents size. 384 // 385 // The function is rarely used solely. See comments at 386 // ResizeAndMoveParentFrame(). 387 void ResizeParentFrame(); 388 389 // Moves the candidate window per the current cursor location, and the 390 // horizontal offset. 391 // 392 // The function is rarely used solely. See comments at 393 // ResizeAndMoveParentFrame(). 394 void MoveParentFrame(); 395 396 // Returns the horizontal offset used for placing the vertical candidate 397 // window so that the first candidate is aligned with the the text being 398 // converted like: 399 // 400 // XXX <- The user is converting XXX 401 // +-----+ 402 // |1 XXX| 403 // |2 YYY| 404 // |3 ZZZ| 405 // 406 // Returns 0 if no candidate is present. 407 int GetHorizontalOffset(); 408 409 // A function to be called when one of the |candidate_views_| receives a mouse 410 // press event. 411 void OnMousePressed(); 412 // A function to be called when one of the |candidate_views_| receives a mouse 413 // release event. 414 void OnMouseReleased(); 415 416 void set_cursor_location(const gfx::Rect& cursor_location) { 417 cursor_location_ = cursor_location; 418 } 419 420 const gfx::Rect& cursor_location() const { return cursor_location_; } 421 422 protected: 423 // Override View::VisibilityChanged() 424 virtual void VisibilityChanged(View* starting_from, bool is_visible) OVERRIDE; 425 426 // Override View::OnBoundsChanged() 427 virtual void OnBoundsChanged(const gfx::Rect& previous_bounds) OVERRIDE; 428 429 private: 430 // Initializes the candidate views if needed. 431 void MaybeInitializeCandidateViews( 432 const InputMethodLookupTable& lookup_table); 433 434 // Creates the footer area, where we show status information. 435 // For instance, we show a cursor position like 2/19. 436 views::View* CreateFooterArea(); 437 438 // Creates the header area, where we show auxiliary text. 439 views::View* CreateHeaderArea(); 440 441 // The lookup table (candidates). 442 InputMethodLookupTable lookup_table_; 443 444 // The index in the current page of the candidate currently being selected. 445 int selected_candidate_index_in_page_; 446 447 // The observers of the object. 448 ObserverList<Observer> observers_; 449 450 // The parent frame. 451 views::Widget* parent_frame_; 452 453 // Views created in the class will be part of tree of |this|, so these 454 // child views will be deleted when |this| is deleted. 455 456 // The candidate area is where candidates are rendered. 457 views::View* candidate_area_; 458 // The footer area is where the auxiliary text is shown, if the 459 // orientation is vertical. Usually the auxiliary text is used for 460 // showing candidate number information like 2/19. 461 views::View* footer_area_; 462 // We use this when we show something in the footer area. 463 scoped_ptr<views::View> footer_area_contents_; 464 // We use this when we show nothing in the footer area. 465 scoped_ptr<views::View> footer_area_place_holder_; 466 // The header area is where the auxiliary text is shown, if the 467 // orientation is horizontal. If the auxiliary text is not provided, we 468 // show nothing. For instance, we show pinyin text like "zhong'guo". 469 views::View* header_area_; 470 // We use this when we show something in the header area. 471 scoped_ptr<views::View> header_area_contents_; 472 // We use this when we show nothing in the header area. 473 scoped_ptr<views::View> header_area_place_holder_; 474 // The candidate views are used for rendering candidates. 475 std::vector<CandidateView*> candidate_views_; 476 // The header label is shown in the header area. 477 views::Label* header_label_; 478 // The footer label is shown in the footer area. 479 views::Label* footer_label_; 480 481 // Current columns width in |candidate_area_|. 482 int previous_shortcut_column_width_; 483 int previous_candidate_column_width_; 484 int previous_annotation_column_width_; 485 486 // The last cursor location. 487 gfx::Rect cursor_location_; 488 489 // true if a mouse button is pressed, and is not yet released. 490 bool mouse_is_pressed_; 491 }; 492 493 // CandidateRow renderes a row of a candidate. 494 class CandidateView : public views::View { 495 public: 496 CandidateView(CandidateWindowView* parent_candidate_window, 497 int index_in_page, 498 InputMethodLookupTable::Orientation orientation); 499 virtual ~CandidateView() {} 500 // Initializes the candidate view with the given column widths. 501 // A width of 0 means that the column is resizable. 502 void Init(int shortcut_column_width, 503 int candidate_column_width, 504 int annotation_column_width); 505 506 // Sets candidate text to the given text. 507 void SetCandidateText(const std::wstring& text); 508 509 // Sets shortcut text to the given text. 510 void SetShortcutText(const std::wstring& text); 511 512 // Sets annotation text to the given text. 513 void SetAnnotationText(const std::wstring& text); 514 515 // Selects the candidate row. Changes the appearance to make it look 516 // like a selected candidate. 517 void Select(); 518 519 // Unselects the candidate row. Changes the appearance to make it look 520 // like an unselected candidate. 521 void Unselect(); 522 523 // Enables or disables the candidate row based on |enabled|. Changes the 524 // appearance to make it look like unclickable area. 525 void SetRowEnabled(bool enabled); 526 527 // Returns the relative position of the candidate label. 528 gfx::Point GetCandidateLabelPosition() const; 529 530 private: 531 // Overridden from View: 532 virtual bool OnMousePressed(const views::MouseEvent& event) OVERRIDE; 533 virtual bool OnMouseDragged(const views::MouseEvent& event) OVERRIDE; 534 virtual void OnMouseReleased(const views::MouseEvent& event) OVERRIDE; 535 virtual void OnMouseCaptureLost() OVERRIDE; 536 537 // Zero-origin index in the current page. 538 int index_in_page_; 539 540 // The orientation of the candidate view. 541 InputMethodLookupTable::Orientation orientation_; 542 543 // The parent candidate window that contains this view. 544 CandidateWindowView* parent_candidate_window_; 545 546 // Views created in the class will be part of tree of |this|, so these 547 // child views will be deleted when |this| is deleted. 548 549 // The shortcut label renders shortcut numbers like 1, 2, and 3. 550 views::Label* shortcut_label_; 551 // The candidate label renders candidates. 552 views::Label* candidate_label_; 553 // The annotation label renders annotations. 554 views::Label* annotation_label_; 555 }; 556 557 // The implementation of CandidateWindowController. 558 // CandidateWindowController controls the CandidateWindow. 559 class CandidateWindowController::Impl : public CandidateWindowView::Observer { 560 public: 561 Impl(); 562 virtual ~Impl(); 563 564 // Initializes the candidate window. Returns true on success. 565 bool Init(); 566 567 private: 568 // CandidateWindowView::Observer implementation. 569 virtual void OnCandidateCommitted(int index, 570 int button, 571 int flags); 572 573 // Creates the candidate window view. 574 void CreateView(); 575 576 // The function is called when |HideAuxiliaryText| signal is received in 577 // libcros. |input_method_library| is a void pointer to this object. 578 static void OnHideAuxiliaryText(void* input_method_library); 579 580 // The function is called when |HideLookupTable| signal is received in 581 // libcros. |input_method_library| is a void pointer to this object. 582 static void OnHideLookupTable(void* input_method_library); 583 584 // The function is called when |SetCursorLocation| signal is received 585 // in libcros. |input_method_library| is a void pointer to this object. 586 static void OnSetCursorLocation(void* input_method_library, 587 int x, 588 int y, 589 int width, 590 int height); 591 592 // The function is called when |UpdateAuxiliaryText| signal is received 593 // in libcros. |input_method_library| is a void pointer to this object. 594 static void OnUpdateAuxiliaryText(void* input_method_library, 595 const std::string& utf8_text, 596 bool visible); 597 598 // The function is called when |UpdateLookupTable| signal is received 599 // in libcros. |input_method_library| is a void pointer to this object. 600 static void OnUpdateLookupTable(void* input_method_library, 601 const InputMethodLookupTable& lookup_table); 602 603 // This function is called by libcros when ibus connects or disconnects. 604 // |input_method_library| is a void pointer to this object. 605 static void OnConnectionChange(void* input_method_library, bool connected); 606 607 // The connection is used for communicating with input method UI logic 608 // in libcros. 609 InputMethodUiStatusConnection* ui_status_connection_; 610 611 // The candidate window view. 612 CandidateWindowView* candidate_window_; 613 614 // This is the outer frame of the candidate window view. The frame will 615 // own |candidate_window_|. 616 scoped_ptr<views::Widget> frame_; 617 }; 618 619 CandidateView::CandidateView( 620 CandidateWindowView* parent_candidate_window, 621 int index_in_page, 622 InputMethodLookupTable::Orientation orientation) 623 : index_in_page_(index_in_page), 624 orientation_(orientation), 625 parent_candidate_window_(parent_candidate_window), 626 shortcut_label_(NULL), 627 candidate_label_(NULL), 628 annotation_label_(NULL) { 629 } 630 631 void CandidateView::Init(int shortcut_column_width, 632 int candidate_column_width, 633 int annotation_column_width) { 634 views::GridLayout* layout = new views::GridLayout(this); 635 SetLayoutManager(layout); // |this| owns |layout|. 636 637 // Create Labels. 638 shortcut_label_ = CreateShortcutLabel(orientation_); 639 views::View* wrapped_shortcut_label = 640 CreateWrappedShortcutLabel(shortcut_label_, orientation_); 641 candidate_label_ = CreateCandidateLabel(orientation_); 642 annotation_label_ = CreateAnnotationLabel(orientation_); 643 644 // Initialize the column set with three columns. 645 views::ColumnSet* column_set = layout->AddColumnSet(0); 646 647 // If orientation is vertical, each column width is fixed. 648 // Otherwise the width is resizable. 649 const views::GridLayout::SizeType column_type = 650 orientation_ == InputMethodLookupTable::kVertical ? 651 views::GridLayout::FIXED : views::GridLayout::USE_PREF; 652 653 const int padding_column_width = 654 orientation_ == InputMethodLookupTable::kVertical ? 4 : 6; 655 656 // Set shortcut column type and width. 657 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 658 0, column_type, shortcut_column_width, 0); 659 column_set->AddPaddingColumn(0, padding_column_width); 660 661 // Set candidate column type and width. 662 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 663 0, column_type, candidate_column_width, 0); 664 column_set->AddPaddingColumn(0, padding_column_width); 665 666 // Set annotation column type and width. 667 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 668 0, column_type, annotation_column_width, 0); 669 column_set->AddPaddingColumn(0, padding_column_width); 670 671 // Add the shortcut label, the candidate label, and annotation label. 672 layout->StartRow(0, 0); 673 // |wrapped_shortcut_label|, |candidate_label_|, and |annotation_label_| 674 // will be owned by |this|. 675 layout->AddView(wrapped_shortcut_label); 676 layout->AddView(candidate_label_); 677 layout->AddView(annotation_label_); 678 } 679 680 void CandidateView::SetCandidateText(const std::wstring& text) { 681 candidate_label_->SetText(text); 682 } 683 684 void CandidateView::SetShortcutText(const std::wstring& text) { 685 shortcut_label_->SetText(text); 686 } 687 688 void CandidateView::SetAnnotationText(const std::wstring& text) { 689 annotation_label_->SetText(text); 690 } 691 692 void CandidateView::Select() { 693 set_background( 694 views::Background::CreateSolidBackground(kSelectedRowBackgroundColor)); 695 set_border(views::Border::CreateSolidBorder(1, kSelectedRowFrameColor)); 696 // Need to call SchedulePaint() for background and border color changes. 697 SchedulePaint(); 698 } 699 700 void CandidateView::Unselect() { 701 set_background(NULL); 702 set_border(NULL); 703 SchedulePaint(); // See comments at Select(). 704 } 705 706 void CandidateView::SetRowEnabled(bool enabled) { 707 shortcut_label_->SetColor( 708 enabled ? kShortcutColor : kDisabledShortcutColor); 709 } 710 711 gfx::Point CandidateView::GetCandidateLabelPosition() const { 712 return candidate_label_->GetMirroredPosition(); 713 } 714 715 bool CandidateView::OnMousePressed(const views::MouseEvent& event) { 716 parent_candidate_window_->OnMousePressed(); 717 // Select the candidate. We'll commit the candidate when the mouse 718 // button is released. 719 parent_candidate_window_->SelectCandidateAt(index_in_page_); 720 // Request MouseDraggged and MouseReleased events. 721 return true; 722 } 723 724 bool CandidateView::OnMouseDragged(const views::MouseEvent& event) { 725 gfx::Point location_in_candidate_window = event.location(); 726 views::View::ConvertPointToView(this, parent_candidate_window_, 727 &location_in_candidate_window); 728 // Notify the candidate window that a candidate is now being dragged. 729 parent_candidate_window_->OnCandidateDragged(location_in_candidate_window); 730 // Request MouseReleased event. 731 return true; 732 } 733 734 void CandidateView::OnMouseReleased(const views::MouseEvent& event) { 735 // Commit the current candidate. 736 parent_candidate_window_->CommitCandidate(); 737 OnMouseCaptureLost(); 738 } 739 740 void CandidateView::OnMouseCaptureLost() { 741 parent_candidate_window_->OnMouseReleased(); 742 } 743 744 CandidateWindowView::CandidateWindowView( 745 views::Widget* parent_frame) 746 : selected_candidate_index_in_page_(0), 747 parent_frame_(parent_frame), 748 candidate_area_(NULL), 749 footer_area_(NULL), 750 header_area_(NULL), 751 header_label_(NULL), 752 footer_label_(NULL), 753 previous_shortcut_column_width_(0), 754 previous_candidate_column_width_(0), 755 previous_annotation_column_width_(0), 756 mouse_is_pressed_(false) { 757 } 758 759 void CandidateWindowView::Init() { 760 // Set the background and the border of the view. 761 set_background( 762 views::Background::CreateSolidBackground(kDefaultBackgroundColor)); 763 set_border(views::Border::CreateSolidBorder(1, kFrameColor)); 764 765 // Create the header area. 766 header_area_ = CreateHeaderArea(); 767 // Create the candidate area. 768 candidate_area_ = new views::View; 769 // Create the footer area. 770 footer_area_ = CreateFooterArea(); 771 772 // Set the window layout of the view 773 views::GridLayout* layout = new views::GridLayout(this); 774 SetLayoutManager(layout); // |this| owns layout|. 775 views::ColumnSet* column_set = layout->AddColumnSet(0); 776 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 777 0, views::GridLayout::USE_PREF, 0, 0); 778 779 // Add the header area. 780 layout->StartRow(0, 0); 781 layout->AddView(header_area_); // |header_area_| is owned by |this|. 782 783 // Add the candidate area. 784 layout->StartRow(0, 0); 785 layout->AddView(candidate_area_); // |candidate_area_| is owned by |this|. 786 787 // Add the footer area. 788 layout->StartRow(0, 0); 789 layout->AddView(footer_area_); // |footer_area_| is owned by |this|. 790 } 791 792 void CandidateWindowView::HideLookupTable() { 793 if (!mouse_is_pressed_) { 794 parent_frame_->Hide(); 795 return; 796 } 797 798 // We should not hide the |frame_| when a mouse is pressed, so we don't run 799 // into issues below. 800 // 801 // First, in the following scenario, it seems that the Views popup window does 802 // not release mouse/keyboard grab even after it gets hidden. 803 // 804 // 1. create a popup window by views::Widget::CreateWidget() with the 805 // accept_events flag set to true on the CreateParams. 806 // 2. press a mouse button on the window. 807 // 3. before releasing the mouse button, Hide() the window. 808 // 4. release the button. 809 // 810 // And if we embed IME candidate window into Chrome, the window sometimes 811 // receives an extra 'hide-lookup-table' event before mouse button is 812 // released: 813 // 814 // 1. the candidate window is clicked. 815 // 2. The mouse click handler in this file, OnMousePressed() in CandidateView, 816 // is called, and the handler consumes the event by returning true. 817 // 3. HOWEVER, if the candidate window is embedded into Chrome, the event is 818 // also sent to Chrome! (problem #1) 819 // 4. im-ibus.so in Chrome sends 'focus-out' event to ibus-daemon. 820 // 5. ibus-daemon sends 'hide-lookup-table' event to the candidate window. 821 // 6. the window is hidden, but the window does not release mouse/keyboard 822 // grab! (problem #2) 823 // 7. mouse button is released. 824 // 8. now all mouse/keyboard events are consumed by the hidden popup, and are 825 // not sent to Chrome. 826 // 827 // TODO(yusukes): investigate why the click event is sent to both candidate 828 // window and Chrome. http://crosbug.com/11423 829 // TODO(yusukes): investigate if we could fix Views so it always releases grab 830 // when a popup window gets hidden. http://crosbug.com/11422 831 // 832 LOG(WARNING) << "Can't hide the table since a mouse button is not released."; 833 } 834 835 void CandidateWindowView::OnMousePressed() { 836 mouse_is_pressed_ = true; 837 } 838 839 void CandidateWindowView::OnMouseReleased() { 840 mouse_is_pressed_ = false; 841 } 842 843 void CandidateWindowView::HideAuxiliaryText() { 844 views::View* target_area = ( 845 lookup_table_.orientation == InputMethodLookupTable::kHorizontal ? 846 header_area_ : footer_area_); 847 views::View* target_place_holder = ( 848 lookup_table_.orientation == InputMethodLookupTable::kHorizontal ? 849 header_area_place_holder_.get() : 850 footer_area_place_holder_.get()); 851 // Put the place holder to the target display area. 852 target_area->RemoveAllChildViews(false); // Don't delete child views. 853 target_area->AddChildView(target_place_holder); 854 } 855 856 void CandidateWindowView::ShowAuxiliaryText() { 857 views::View* target_area = ( 858 lookup_table_.orientation == InputMethodLookupTable::kHorizontal ? 859 header_area_ : footer_area_); 860 views::View* target_contents = ( 861 lookup_table_.orientation == InputMethodLookupTable::kHorizontal ? 862 header_area_contents_.get() : 863 footer_area_contents_.get()); 864 865 if (target_contents->parent() != target_area) { 866 // If contents not in display area, put it in. 867 target_area->RemoveAllChildViews(false); // Don't delete child views. 868 target_area->AddChildView(target_contents); 869 } 870 } 871 872 void CandidateWindowView::UpdateAuxiliaryText(const std::string& utf8_text) { 873 views::Label* target_label = ( 874 lookup_table_.orientation == InputMethodLookupTable::kHorizontal ? 875 header_label_ : footer_label_); 876 target_label->SetText(UTF8ToWide(utf8_text)); 877 } 878 879 bool CandidateWindowView::ShouldUpdateCandidateViews( 880 const InputMethodLookupTable& old_table, 881 const InputMethodLookupTable& new_table) { 882 // Check if most table contents are identical. 883 if (old_table.page_size == new_table.page_size && 884 old_table.orientation == new_table.orientation && 885 old_table.candidates == new_table.candidates && 886 old_table.labels == new_table.labels && 887 old_table.annotations == new_table.annotations && 888 // Check if the page indexes are identical. 889 ComputePageIndex(old_table) == ComputePageIndex(new_table)) { 890 // If all of the conditions are met, we don't have to update candidate 891 // views. 892 return false; 893 } 894 return true; 895 } 896 897 void CandidateWindowView::UpdateCandidates( 898 const InputMethodLookupTable& new_lookup_table) { 899 const bool should_update = ShouldUpdateCandidateViews(lookup_table_, 900 new_lookup_table); 901 // Updating the candidate views is expensive. We'll skip this if possible. 902 if (should_update) { 903 // Initialize candidate views if necessary. 904 MaybeInitializeCandidateViews(new_lookup_table); 905 906 // Compute the index of the current page. 907 const int current_page_index = ComputePageIndex(new_lookup_table); 908 if (current_page_index < 0) { 909 LOG(ERROR) << "Invalid lookup_table: " << new_lookup_table.ToString(); 910 return; 911 } 912 913 // Update the candidates in the current page. 914 const size_t start_from = current_page_index * new_lookup_table.page_size; 915 916 // In some cases, engines send empty shortcut labels. For instance, 917 // ibus-mozc sends empty labels when they show suggestions. In this 918 // case, we should not show shortcut labels. 919 const bool no_shortcut_mode = 920 (start_from < new_lookup_table.labels.size() && 921 new_lookup_table.labels[start_from] == ""); 922 for (size_t i = 0; i < candidate_views_.size(); ++i) { 923 const size_t index_in_page = i; 924 const size_t candidate_index = start_from + index_in_page; 925 CandidateView* candidate_view = candidate_views_[index_in_page]; 926 // Set the shortcut text. 927 if (no_shortcut_mode) { 928 candidate_view->SetShortcutText(L""); 929 } else { 930 // At this moment, we don't use labels sent from engines for UX 931 // reasons. First, we want to show shortcut labels in empty rows 932 // (ex. show 6, 7, 8, ... in empty rows when the number of 933 // candidates is 5). Second, we want to add a period after each 934 // shortcut label when the candidate window is horizontal. 935 candidate_view->SetShortcutText( 936 CreateShortcutText(i, new_lookup_table.orientation)); 937 } 938 // Set the candidate text. 939 if (candidate_index < new_lookup_table.candidates.size() && 940 candidate_index < new_lookup_table.annotations.size()) { 941 candidate_view->SetCandidateText( 942 UTF8ToWide(new_lookup_table.candidates[candidate_index])); 943 candidate_view->SetAnnotationText( 944 UTF8ToWide(new_lookup_table.annotations[candidate_index])); 945 candidate_view->SetRowEnabled(true); 946 } else { 947 // Disable the empty row. 948 candidate_view->SetCandidateText(L""); 949 candidate_view->SetAnnotationText(L""); 950 candidate_view->SetRowEnabled(false); 951 } 952 } 953 } 954 // Update the current lookup table. We'll use lookup_table_ from here. 955 // Note that SelectCandidateAt() uses lookup_table_. 956 lookup_table_ = new_lookup_table; 957 958 // Select the current candidate in the page. 959 const int current_candidate_in_page = 960 lookup_table_.cursor_absolute_index % lookup_table_.page_size; 961 SelectCandidateAt(current_candidate_in_page); 962 } 963 964 void CandidateWindowView::MaybeInitializeCandidateViews( 965 const InputMethodLookupTable& lookup_table) { 966 const InputMethodLookupTable::Orientation orientation = 967 lookup_table.orientation; 968 const int page_size = lookup_table.page_size; 969 970 // Current column width. 971 int shortcut_column_width = 0; 972 int candidate_column_width = 0; 973 int annotation_column_width = 0; 974 975 // If orientation is horizontal, don't need to compute width, 976 // because each label is left aligned. 977 if (orientation == InputMethodLookupTable::kVertical) { 978 shortcut_column_width = ComputeShortcutColumnWidth(lookup_table); 979 candidate_column_width = ComputeCandidateColumnWidth(lookup_table); 980 annotation_column_width = ComputeAnnotationColumnWidth(lookup_table); 981 } 982 983 // If the requested number of views matches the number of current views, and 984 // previous and current column width are same, just reuse these. 985 // 986 // Note that the early exit logic is not only useful for improving 987 // performance, but also necessary for the horizontal candidate window 988 // to be redrawn properly. If we get rid of the logic, the horizontal 989 // candidate window won't get redrawn properly for some reason when 990 // there is no size change. You can test this by removing "return" here 991 // and type "ni" with Pinyin input method. 992 if (static_cast<int>(candidate_views_.size()) == page_size && 993 lookup_table_.orientation == orientation && 994 previous_shortcut_column_width_ == shortcut_column_width && 995 previous_candidate_column_width_ == candidate_column_width && 996 previous_annotation_column_width_ == annotation_column_width) { 997 return; 998 } 999 1000 // Update the previous column widths. 1001 previous_shortcut_column_width_ = shortcut_column_width; 1002 previous_candidate_column_width_ = candidate_column_width; 1003 previous_annotation_column_width_ = annotation_column_width; 1004 1005 // Clear the existing candidate_views if any. 1006 for (size_t i = 0; i < candidate_views_.size(); ++i) { 1007 candidate_area_->RemoveChildView(candidate_views_[i]); 1008 } 1009 candidate_views_.clear(); 1010 1011 views::GridLayout* layout = new views::GridLayout(candidate_area_); 1012 // |candidate_area_| owns |layout|. 1013 candidate_area_->SetLayoutManager(layout); 1014 // Initialize the column set. 1015 views::ColumnSet* column_set = layout->AddColumnSet(0); 1016 if (orientation == InputMethodLookupTable::kVertical) { 1017 column_set->AddColumn(views::GridLayout::FILL, 1018 views::GridLayout::FILL, 1019 0, views::GridLayout::USE_PREF, 0, 0); 1020 } else { 1021 for (int i = 0; i < page_size; ++i) { 1022 column_set->AddColumn(views::GridLayout::FILL, 1023 views::GridLayout::FILL, 1024 0, views::GridLayout::USE_PREF, 0, 0); 1025 } 1026 } 1027 1028 // Set insets so the border of the selected candidate is drawn inside of 1029 // the border of the main candidate window, but we don't have the inset 1030 // at the top and the bottom as we have the borders of the header and 1031 // footer areas. 1032 const gfx::Insets kCandidateAreaInsets(0, 1, 0, 1); 1033 layout->SetInsets(kCandidateAreaInsets.top(), 1034 kCandidateAreaInsets.left(), 1035 kCandidateAreaInsets.bottom(), 1036 kCandidateAreaInsets.right()); 1037 1038 // Add views to the candidate area. 1039 if (orientation == InputMethodLookupTable::kHorizontal) { 1040 layout->StartRow(0, 0); 1041 } 1042 1043 for (int i = 0; i < page_size; ++i) { 1044 CandidateView* candidate_row = new CandidateView(this, i, orientation); 1045 candidate_row->Init(shortcut_column_width, 1046 candidate_column_width, 1047 annotation_column_width); 1048 candidate_views_.push_back(candidate_row); 1049 if (orientation == InputMethodLookupTable::kVertical) { 1050 layout->StartRow(0, 0); 1051 } 1052 // |candidate_row| will be owned by |candidate_area_|. 1053 layout->AddView(candidate_row); 1054 } 1055 1056 // Compute views size in |layout|. 1057 // If we don't call this function, GetHorizontalOffset() often 1058 // returns invalid value (returns 0), then candidate window 1059 // moves right from the correct position in MoveParentFrame(). 1060 // TODO(nhiroki): Figure out why it returns invalid value. 1061 // It seems that the x-position of the candidate labels is not set. 1062 layout->Layout(candidate_area_); 1063 } 1064 1065 views::View* CandidateWindowView::CreateHeaderArea() { 1066 // |header_area_place_holder_| will not be owned by another view. 1067 // This will be deleted by scoped_ptr. 1068 // 1069 // This is because we swap the contents of |header_area_| between 1070 // |header_area_place_holder_| (to show nothing) and 1071 // |header_area_contents_| (to show something). In other words, 1072 // |header_area_| only contains one of the two views hence cannot own 1073 // the two views at the same time. 1074 header_area_place_holder_.reset(new views::View); 1075 header_area_place_holder_->set_parent_owned(false); // Won't be owened. 1076 1077 // |header_label_| will be owned by |header_area_contents_|. 1078 header_label_ = new views::Label; 1079 header_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); 1080 1081 const gfx::Insets kHeaderInsets(2, 2, 2, 4); 1082 // |header_area_contents_| will not be owned by another view. 1083 // See a comment at |header_area_place_holder_| for why. 1084 header_area_contents_.reset( 1085 WrapWithPadding(header_label_, kHeaderInsets)); 1086 header_area_contents_->set_parent_owned(false); // Won't be owened. 1087 header_area_contents_->set_border( 1088 views::Border::CreateSolidBorder(1, kFrameColor)); 1089 header_area_contents_->set_background( 1090 views::Background::CreateVerticalGradientBackground( 1091 kFooterTopColor, 1092 kFooterBottomColor)); 1093 1094 views::View* header_area = new views::View; 1095 header_area->SetLayoutManager(new views::FillLayout); 1096 // Initialize the header area with the place holder (i.e. show nothing). 1097 header_area->AddChildView(header_area_place_holder_.get()); 1098 return header_area; 1099 } 1100 1101 views::View* CandidateWindowView::CreateFooterArea() { 1102 // |footer_area_place_holder_| will not be owned by another view. 1103 // See also the comment about |header_area_place_holder_| in 1104 // CreateHeaderArea(). 1105 footer_area_place_holder_.reset(new views::View); 1106 footer_area_place_holder_->set_parent_owned(false); // Won't be owened. 1107 1108 footer_label_ = new views::Label(); 1109 footer_label_->SetHorizontalAlignment(views::Label::ALIGN_RIGHT); 1110 1111 const gfx::Insets kFooterInsets(2, 2, 2, 4); 1112 footer_area_contents_.reset( 1113 WrapWithPadding(footer_label_, kFooterInsets)); 1114 footer_area_contents_->set_parent_owned(false); // Won't be owened. 1115 footer_area_contents_->set_border( 1116 views::Border::CreateSolidBorder(1, kFrameColor)); 1117 footer_area_contents_->set_background( 1118 views::Background::CreateVerticalGradientBackground( 1119 kFooterTopColor, 1120 kFooterBottomColor)); 1121 1122 views::View* footer_area = new views::View; 1123 footer_area->SetLayoutManager(new views::FillLayout); 1124 // Initialize the footer area with the place holder (i.e. show nothing). 1125 footer_area->AddChildView(footer_area_place_holder_.get()); 1126 return footer_area; 1127 } 1128 1129 void CandidateWindowView::SelectCandidateAt(int index_in_page) { 1130 const int current_page_index = ComputePageIndex(lookup_table_); 1131 if (current_page_index < 0) { 1132 LOG(ERROR) << "Invalid lookup_table: " << lookup_table_.ToString(); 1133 return; 1134 } 1135 1136 const int cursor_absolute_index = 1137 lookup_table_.page_size * current_page_index + index_in_page; 1138 // Ignore click on out of range views. 1139 if (cursor_absolute_index < 0 || 1140 cursor_absolute_index >= 1141 static_cast<int>(lookup_table_.candidates.size())) { 1142 return; 1143 } 1144 1145 // Unselect the currently selected candidate. 1146 candidate_views_[selected_candidate_index_in_page_]->Unselect(); 1147 // Remember the currently selected candidate index in the current page. 1148 selected_candidate_index_in_page_ = index_in_page; 1149 1150 // Select the candidate specified by index_in_page. 1151 candidate_views_[index_in_page]->Select(); 1152 1153 // Update the cursor indexes in the model. 1154 lookup_table_.cursor_absolute_index = cursor_absolute_index; 1155 } 1156 1157 void CandidateWindowView::OnCandidateDragged( 1158 const gfx::Point& location) { 1159 for (size_t i = 0; i < candidate_views_.size(); ++i) { 1160 gfx::Point converted_location = location; 1161 views::View::ConvertPointToView(this, candidate_views_[i], 1162 &converted_location); 1163 if (candidate_views_[i]->HitTest(converted_location)) { 1164 SelectCandidateAt(i); 1165 break; 1166 } 1167 } 1168 } 1169 1170 void CandidateWindowView::CommitCandidate() { 1171 // For now, we don't distinguish left and right clicks. 1172 const int button = 1; // Left button. 1173 const int key_modifilers = 0; 1174 FOR_EACH_OBSERVER(Observer, observers_, 1175 OnCandidateCommitted(selected_candidate_index_in_page_, 1176 button, 1177 key_modifilers)); 1178 } 1179 1180 void CandidateWindowView::ResizeAndMoveParentFrame() { 1181 ResizeParentFrame(); 1182 MoveParentFrame(); 1183 } 1184 1185 void CandidateWindowView::ResizeParentFrame() { 1186 // Resize the parent frame, with the current candidate window size. 1187 gfx::Size size = GetPreferredSize(); 1188 gfx::Rect bounds = parent_frame_->GetClientAreaScreenBounds(); 1189 // SetBounds() is not cheap. Only call this when the size is changed. 1190 if (bounds.size() != size) { 1191 bounds.set_size(size); 1192 parent_frame_->SetBounds(bounds); 1193 } 1194 } 1195 1196 void CandidateWindowView::MoveParentFrame() { 1197 const int x = cursor_location_.x(); 1198 const int y = cursor_location_.y(); 1199 const int height = cursor_location_.height(); 1200 const int horizontal_offset = GetHorizontalOffset(); 1201 1202 gfx::Rect frame_bounds = parent_frame_->GetClientAreaScreenBounds(); 1203 gfx::Rect screen_bounds = views::Screen::GetMonitorWorkAreaNearestWindow( 1204 parent_frame_->GetNativeView()); 1205 1206 // The default position. 1207 frame_bounds.set_x(x + horizontal_offset); 1208 frame_bounds.set_y(y + height); 1209 1210 // Handle overflow at the left and the top. 1211 frame_bounds.set_x(std::max(frame_bounds.x(), screen_bounds.x())); 1212 frame_bounds.set_y(std::max(frame_bounds.y(), screen_bounds.y())); 1213 1214 // Handle overflow at the right. 1215 const int right_overflow = frame_bounds.right() - screen_bounds.right(); 1216 if (right_overflow > 0) { 1217 frame_bounds.set_x(frame_bounds.x() - right_overflow); 1218 } 1219 1220 // Handle overflow at the bottom. 1221 const int bottom_overflow = frame_bounds.bottom() - screen_bounds.bottom(); 1222 if (bottom_overflow > 0) { 1223 frame_bounds.set_y(frame_bounds.y() - height - frame_bounds.height()); 1224 } 1225 1226 // Move the window per the cursor location. 1227 parent_frame_->SetBounds(frame_bounds); 1228 } 1229 1230 int CandidateWindowView::GetHorizontalOffset() { 1231 // Compute the horizontal offset if the lookup table is vertical. 1232 if (!candidate_views_.empty() && 1233 lookup_table_.orientation == InputMethodLookupTable::kVertical) { 1234 return - candidate_views_[0]->GetCandidateLabelPosition().x(); 1235 } 1236 return 0; 1237 } 1238 1239 void CandidateWindowView::VisibilityChanged(View* starting_from, 1240 bool is_visible) { 1241 if (is_visible) { 1242 // If the visibility of candidate window is changed, 1243 // we should move the frame to the right position. 1244 MoveParentFrame(); 1245 } 1246 } 1247 1248 void CandidateWindowView::OnBoundsChanged(const gfx::Rect& previous_bounds) { 1249 // If the bounds(size) of candidate window is changed, 1250 // we should move the frame to the right position. 1251 View::OnBoundsChanged(previous_bounds); 1252 MoveParentFrame(); 1253 } 1254 1255 bool CandidateWindowController::Impl::Init() { 1256 // Initialize the input method UI status connection. 1257 InputMethodUiStatusMonitorFunctions functions; 1258 functions.hide_auxiliary_text = 1259 &CandidateWindowController::Impl::OnHideAuxiliaryText; 1260 functions.hide_lookup_table = 1261 &CandidateWindowController::Impl::OnHideLookupTable; 1262 functions.set_cursor_location = 1263 &CandidateWindowController::Impl::OnSetCursorLocation; 1264 functions.update_auxiliary_text = 1265 &CandidateWindowController::Impl::OnUpdateAuxiliaryText; 1266 functions.update_lookup_table = 1267 &CandidateWindowController::Impl::OnUpdateLookupTable; 1268 ui_status_connection_ = MonitorInputMethodUiStatus(functions, this); 1269 if (!ui_status_connection_) { 1270 LOG(ERROR) << "MonitorInputMethodUiStatus() failed."; 1271 return false; 1272 } 1273 MonitorInputMethodConnection( 1274 ui_status_connection_, 1275 &CandidateWindowController::Impl::OnConnectionChange); 1276 1277 // Create the candidate window view. 1278 CreateView(); 1279 1280 return true; 1281 } 1282 1283 void CandidateWindowController::Impl::CreateView() { 1284 // Create a non-decorated frame. 1285 frame_.reset(views::Widget::CreateWidget( 1286 views::Widget::CreateParams(views::Widget::CreateParams::TYPE_POPUP))); 1287 // The size is initially zero. 1288 frame_->Init(NULL, gfx::Rect(0, 0)); 1289 1290 // Create the candidate window. 1291 candidate_window_ = new CandidateWindowView(frame_.get()); 1292 candidate_window_->Init(); 1293 candidate_window_->AddObserver(this); 1294 1295 // Put the candidate window view on the frame. The frame is resized 1296 // later when the candidate window is shown. 1297 views::RootView* root_view = frame_->GetRootView(); 1298 // |root_view| owns the |candidate_window_|, thus |frame_| effectively 1299 // owns |candidate_window_|. 1300 root_view->SetContentsView(candidate_window_); 1301 } 1302 1303 CandidateWindowController::Impl::Impl() 1304 : ui_status_connection_(NULL), 1305 frame_(NULL) { 1306 } 1307 1308 CandidateWindowController::Impl::~Impl() { 1309 candidate_window_->RemoveObserver(this); 1310 chromeos::DisconnectInputMethodUiStatus(ui_status_connection_); 1311 } 1312 1313 void CandidateWindowController::Impl::OnHideAuxiliaryText( 1314 void* input_method_library) { 1315 CandidateWindowController::Impl* controller = 1316 static_cast<CandidateWindowController::Impl*>(input_method_library); 1317 1318 controller->candidate_window_->HideAuxiliaryText(); 1319 controller->candidate_window_->ResizeParentFrame(); 1320 } 1321 1322 void CandidateWindowController::Impl::OnHideLookupTable( 1323 void* input_method_library) { 1324 CandidateWindowController::Impl* controller = 1325 static_cast<CandidateWindowController::Impl*>(input_method_library); 1326 1327 controller->candidate_window_->HideLookupTable(); 1328 } 1329 1330 void CandidateWindowController::Impl::OnSetCursorLocation( 1331 void* input_method_library, 1332 int x, 1333 int y, 1334 int width, 1335 int height) { 1336 CandidateWindowController::Impl* controller = 1337 static_cast<CandidateWindowController::Impl*>(input_method_library); 1338 1339 // A workaround for http://crosbug.com/6460. We should ignore very short Y 1340 // move to prevent the window from shaking up and down. 1341 const int kKeepPositionThreshold = 2; // px 1342 const gfx::Rect& last_location = 1343 controller->candidate_window_->cursor_location(); 1344 const int delta_y = abs(last_location.y() - y); 1345 if ((last_location.x() == x) && (delta_y <= kKeepPositionThreshold)) { 1346 DLOG(INFO) << "Ignored set_cursor_location signal to prevent window shake"; 1347 return; 1348 } 1349 1350 // Remember the cursor location. 1351 controller->candidate_window_->set_cursor_location( 1352 gfx::Rect(x, y, width, height)); 1353 // Move the window per the cursor location. 1354 controller->candidate_window_->MoveParentFrame(); 1355 } 1356 1357 void CandidateWindowController::Impl::OnUpdateAuxiliaryText( 1358 void* input_method_library, 1359 const std::string& utf8_text, 1360 bool visible) { 1361 CandidateWindowController::Impl* controller = 1362 static_cast<CandidateWindowController::Impl*>(input_method_library); 1363 // If it's not visible, hide the auxiliary text and return. 1364 if (!visible) { 1365 controller->candidate_window_->HideAuxiliaryText(); 1366 return; 1367 } 1368 controller->candidate_window_->UpdateAuxiliaryText(utf8_text); 1369 controller->candidate_window_->ShowAuxiliaryText(); 1370 controller->candidate_window_->ResizeParentFrame(); 1371 } 1372 1373 void CandidateWindowController::Impl::OnUpdateLookupTable( 1374 void* input_method_library, 1375 const InputMethodLookupTable& lookup_table) { 1376 CandidateWindowController::Impl* controller = 1377 static_cast<CandidateWindowController::Impl*>(input_method_library); 1378 1379 // If it's not visible, hide the window and return. 1380 if (!lookup_table.visible) { 1381 controller->candidate_window_->HideLookupTable(); 1382 return; 1383 } 1384 1385 controller->candidate_window_->UpdateCandidates(lookup_table); 1386 controller->candidate_window_->ResizeParentFrame(); 1387 controller->frame_->Show(); 1388 } 1389 1390 void CandidateWindowController::Impl::OnCandidateCommitted(int index, 1391 int button, 1392 int flags) { 1393 NotifyCandidateClicked(ui_status_connection_, index, button, flags); 1394 } 1395 1396 void CandidateWindowController::Impl::OnConnectionChange( 1397 void* input_method_library, 1398 bool connected) { 1399 if (!connected) { 1400 CandidateWindowController::Impl* controller = 1401 static_cast<CandidateWindowController::Impl*>(input_method_library); 1402 controller->candidate_window_->HideLookupTable(); 1403 } 1404 } 1405 1406 CandidateWindowController::CandidateWindowController() 1407 : impl_(new CandidateWindowController::Impl) { 1408 } 1409 1410 CandidateWindowController::~CandidateWindowController() { 1411 delete impl_; 1412 } 1413 1414 bool CandidateWindowController::Init() { 1415 return impl_->Init(); 1416 } 1417 1418 } // namespace chromeos 1419