// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/chromeos/input_method/candidate_window.h" #include <algorithm> #include <string> #include <vector> #include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "base/observer_list.h" #include "base/string_util.h" #include "base/stringprintf.h" #include "base/utf_string_conversions.h" #include "third_party/cros/chromeos_input_method_ui.h" #include "ui/gfx/canvas.h" #include "ui/gfx/font.h" #include "views/controls/label.h" #include "views/controls/textfield/textfield.h" #include "views/events/event.h" #include "views/layout/fill_layout.h" #include "views/layout/grid_layout.h" #include "views/screen.h" #include "views/widget/root_view.h" #include "views/widget/widget.h" #include "views/widget/widget_gtk.h" #include "views/window/non_client_view.h" #include "views/window/window.h" #include "views/window/window_delegate.h" namespace { // Colors used in the candidate window UI. const SkColor kFrameColor = SkColorSetRGB(0x96, 0x96, 0x96); const SkColor kShortcutBackgroundColor = SkColorSetARGB(0x10, 0x3, 0x4, 0xf); const SkColor kSelectedRowBackgroundColor = SkColorSetRGB(0xd1, 0xea, 0xff); const SkColor kDefaultBackgroundColor = SkColorSetRGB(0xff, 0xff, 0xff); const SkColor kSelectedRowFrameColor = SkColorSetRGB(0x7f, 0xac, 0xdd); const SkColor kFooterTopColor = SkColorSetRGB(0xff, 0xff, 0xff); const SkColor kFooterBottomColor = SkColorSetRGB(0xee, 0xee, 0xee); const SkColor kShortcutColor = SkColorSetRGB(0x61, 0x61, 0x61); const SkColor kDisabledShortcutColor = SkColorSetRGB(0xcc, 0xcc, 0xcc); const SkColor kAnnotationColor = SkColorSetRGB(0x88, 0x88, 0x88); // We'll use a bigger font size, so Chinese characters are more readable // in the candidate window. #if defined(CROS_FONTS_USING_BCI) const int kFontSizeDelta = 1; #else const int kFontSizeDelta = 2; #endif // The minimum width of candidate labels in the vertical candidate // window. We use this value to prevent the candidate window from being // too narrow when all candidates are short. const int kMinCandidateLabelWidth = 100; // The maximum width of candidate labels in the vertical candidate // window. We use this value to prevent the candidate window from being // too wide when one of candidates are long. const int kMaxCandidateLabelWidth = 500; // VerticalCandidateLabel is used for rendering candidate text in // the vertical candidate window. class VerticalCandidateLabel : public views::Label { virtual ~VerticalCandidateLabel() {} // Returns the preferred size, but guarantees that the width has at // least kMinCandidateLabelWidth pixels. virtual gfx::Size GetPreferredSize() { gfx::Size size = Label::GetPreferredSize(); // Hack. +2 is needed to prevent labels from getting elided like // "abc..." in some cases. TODO(satorux): Figure out why it's // necessary. size.set_width(size.width() + 2); if (size.width() < kMinCandidateLabelWidth) { size.set_width(kMinCandidateLabelWidth); } if (size.width() > kMaxCandidateLabelWidth) { size.set_width(kMaxCandidateLabelWidth); } return size; } }; // Wraps the given view with some padding, and returns it. views::View* WrapWithPadding(views::View* view, const gfx::Insets& insets) { views::View* wrapper = new views::View; // Use GridLayout to give some insets inside. views::GridLayout* layout = new views::GridLayout(wrapper); wrapper->SetLayoutManager(layout); // |wrapper| owns |layout|. layout->SetInsets(insets); views::ColumnSet* column_set = layout->AddColumnSet(0); column_set->AddColumn( views::GridLayout::FILL, views::GridLayout::FILL, 1, views::GridLayout::USE_PREF, 0, 0); layout->StartRow(0, 0); // Add the view contents. layout->AddView(view); // |view| is owned by |wraper|, not |layout|. return wrapper; } // Creates shortcut text from the given index and the orientation. std::wstring CreateShortcutText(int index, chromeos::InputMethodLookupTable::Orientation orientation) { // Choose the character used for the shortcut label. const wchar_t kShortcutCharacters[] = L"1234567890ABCDEF"; // The default character should not be used but just in case. wchar_t shortcut_character = L'?'; // -1 to exclude the null character at the end. if (index < static_cast<int>(arraysize(kShortcutCharacters) - 1)) { shortcut_character = kShortcutCharacters[index]; } std::wstring shortcut_text; if (orientation == chromeos::InputMethodLookupTable::kVertical) { shortcut_text = base::StringPrintf(L"%lc", shortcut_character); } else { shortcut_text = base::StringPrintf(L"%lc.", shortcut_character); } return shortcut_text; } // Creates the shortcut label, and returns it (never returns NULL). // The label text is not set in this function. views::Label* CreateShortcutLabel( chromeos::InputMethodLookupTable::Orientation orientation) { // Create the shortcut label. The label will be owned by // |wrapped_shortcut_label|, hence it's deleted when // |wrapped_shortcut_label| is deleted. views::Label* shortcut_label = new views::Label; if (orientation == chromeos::InputMethodLookupTable::kVertical) { shortcut_label->SetFont( shortcut_label->font().DeriveFont(kFontSizeDelta, gfx::Font::BOLD)); } else { shortcut_label->SetFont( shortcut_label->font().DeriveFont(kFontSizeDelta)); } // TODO(satorux): Maybe we need to use language specific fonts for // candidate_label, like Chinese font for Chinese input method? shortcut_label->SetColor(kShortcutColor); return shortcut_label; } // Wraps the shortcut label, then decorates wrapped shortcut label // and returns it (never returns NULL). // The label text is not set in this function. views::View* CreateWrappedShortcutLabel(views::Label* shortcut_label, chromeos::InputMethodLookupTable::Orientation orientation) { // Wrap it with padding. const gfx::Insets kVerticalShortcutLabelInsets(1, 6, 1, 6); const gfx::Insets kHorizontalShortcutLabelInsets(1, 3, 1, 0); const gfx::Insets insets = (orientation == chromeos::InputMethodLookupTable::kVertical ? kVerticalShortcutLabelInsets : kHorizontalShortcutLabelInsets); views::View* wrapped_shortcut_label = WrapWithPadding(shortcut_label, insets); // Add decoration based on the orientation. if (orientation == chromeos::InputMethodLookupTable::kVertical) { // Set the background color. wrapped_shortcut_label->set_background( views::Background::CreateSolidBackground( kShortcutBackgroundColor)); } return wrapped_shortcut_label; } // Creates the candidate label, and returns it (never returns NULL). // The label text is not set in this function. views::Label* CreateCandidateLabel( chromeos::InputMethodLookupTable::Orientation orientation) { views::Label* candidate_label = NULL; // Create the candidate label. The label will be added to |this| as a // child view, hence it's deleted when |this| is deleted. if (orientation == chromeos::InputMethodLookupTable::kVertical) { candidate_label = new VerticalCandidateLabel; } else { candidate_label = new views::Label; } // Change the font size. candidate_label->SetFont( candidate_label->font().DeriveFont(kFontSizeDelta)); candidate_label->SetHorizontalAlignment(views::Label::ALIGN_LEFT); return candidate_label; } // Creates the annotation label, and return it (never returns NULL). // The label text is not set in this function. views::Label* CreateAnnotationLabel( chromeos::InputMethodLookupTable::Orientation orientation) { // Create the annotation label. views::Label* annotation_label = new views::Label; // Change the font size and color. annotation_label->SetFont( annotation_label->font().DeriveFont(kFontSizeDelta)); annotation_label->SetColor(kAnnotationColor); annotation_label->SetHorizontalAlignment(views::Label::ALIGN_LEFT); return annotation_label; } // Computes shortcut column width. int ComputeShortcutColumnWidth( const chromeos::InputMethodLookupTable& lookup_table) { int shortcut_column_width = 0; // Create the shortcut label. The label will be owned by // |wrapped_shortcut_label|, hence it's deleted when // |wrapped_shortcut_label| is deleted. views::Label* shortcut_label = CreateShortcutLabel(lookup_table.orientation); scoped_ptr<views::View> wrapped_shortcut_label( CreateWrappedShortcutLabel(shortcut_label, lookup_table.orientation)); // Compute the max width in shortcut labels. // We'll create temporary shortcut labels, and choose the largest width. for (int i = 0; i < lookup_table.page_size; ++i) { shortcut_label->SetText( CreateShortcutText(i, lookup_table.orientation)); shortcut_column_width = std::max(shortcut_column_width, wrapped_shortcut_label->GetPreferredSize().width()); } return shortcut_column_width; } // Computes the page index. For instance, if the page size is 9, and the // cursor is pointing to 13th candidate, the page index will be 1 (2nd // page, as the index is zero-origin). Returns -1 on error. int ComputePageIndex(const chromeos::InputMethodLookupTable& lookup_table) { if (lookup_table.page_size > 0) return lookup_table.cursor_absolute_index / lookup_table.page_size; return -1; } // Computes candidate column width. int ComputeCandidateColumnWidth( const chromeos::InputMethodLookupTable& lookup_table) { int candidate_column_width = 0; scoped_ptr<views::Label> candidate_label( CreateCandidateLabel(lookup_table.orientation)); // Compute the start index of |lookup_table_|. const int current_page_index = ComputePageIndex(lookup_table); if (current_page_index < 0) return 0; const size_t start_from = current_page_index * lookup_table.page_size; // Compute the max width in candidate labels. // We'll create temporary candidate labels, and choose the largest width. for (size_t i = 0; i + start_from < lookup_table.candidates.size(); ++i) { const size_t index = start_from + i; candidate_label->SetText( UTF8ToWide(lookup_table.candidates[index])); candidate_column_width = std::max(candidate_column_width, candidate_label->GetPreferredSize().width()); } return candidate_column_width; } // Computes annotation column width. int ComputeAnnotationColumnWidth( const chromeos::InputMethodLookupTable& lookup_table) { int annotation_column_width = 0; scoped_ptr<views::Label> annotation_label( CreateAnnotationLabel(lookup_table.orientation)); // Compute the start index of |lookup_table_|. const int current_page_index = ComputePageIndex(lookup_table); if (current_page_index < 0) return 0; const size_t start_from = current_page_index * lookup_table.page_size; // Compute max width in annotation labels. // We'll create temporary annotation labels, and choose the largest width. for (size_t i = 0; i + start_from < lookup_table.annotations.size(); ++i) { const size_t index = start_from + i; annotation_label->SetText( UTF8ToWide(lookup_table.annotations[index])); annotation_column_width = std::max(annotation_column_width, annotation_label->GetPreferredSize().width()); } return annotation_column_width; } } // namespace namespace chromeos { class CandidateView; // CandidateWindowView is the main container of the candidate window UI. class CandidateWindowView : public views::View { public: // The object can be monitored by the observer. class Observer { public: virtual ~Observer() {} // The function is called when a candidate is committed. // See comments at NotifyCandidateClicke() in chromeos_input_method_ui.h for // details about the parameters. virtual void OnCandidateCommitted(int index, int button, int flag) = 0; }; explicit CandidateWindowView(views::Widget* parent_frame); virtual ~CandidateWindowView() {} void Init(); // Adds the given observer. The ownership is not transferred. void AddObserver(Observer* observer) { observers_.AddObserver(observer); } // Removes the given observer. void RemoveObserver(Observer* observer) { observers_.RemoveObserver(observer); } // Selects the candidate specified by the index in the current page // (zero-origin). Changes the appearance of the selected candidate, // updates the information in the candidate window as needed. void SelectCandidateAt(int index_in_page); // The function is called when a candidate is being dragged. From the // given point, locates the candidate under the mouse cursor, and // selects it. void OnCandidateDragged(const gfx::Point& point); // Commits the candidate currently being selected. void CommitCandidate(); // Hides the lookup table. void HideLookupTable(); // Hides the auxiliary text. void HideAuxiliaryText(); // Shows the auxiliary text. void ShowAuxiliaryText(); // Updates the auxiliary text. void UpdateAuxiliaryText(const std::string& utf8_text); // Returns true if we should update candidate views in the window. For // instance, if we are going to show the same candidates as before, we // don't have to update candidate views. This happens when the user just // moves the cursor in the same page in the candidate window. bool ShouldUpdateCandidateViews( const InputMethodLookupTable& old_table, const InputMethodLookupTable& new_table); // Updates candidates of the candidate window from |lookup_table|. // Candidates are arranged per |orientation|. void UpdateCandidates(const InputMethodLookupTable& lookup_table); // Resizes and moves the parent frame. The two actions should be // performed consecutively as resizing may require the candidate window // to move. For instance, we may need to move the candidate window from // below the cursor to above the cursor, if the candidate window becomes // too big to be shown near the bottom of the screen. This function // needs to be called when the visible contents of the candidate window // are modified. void ResizeAndMoveParentFrame(); // Resizes the parent frame per the current contents size. // // The function is rarely used solely. See comments at // ResizeAndMoveParentFrame(). void ResizeParentFrame(); // Moves the candidate window per the current cursor location, and the // horizontal offset. // // The function is rarely used solely. See comments at // ResizeAndMoveParentFrame(). void MoveParentFrame(); // Returns the horizontal offset used for placing the vertical candidate // window so that the first candidate is aligned with the the text being // converted like: // // XXX <- The user is converting XXX // +-----+ // |1 XXX| // |2 YYY| // |3 ZZZ| // // Returns 0 if no candidate is present. int GetHorizontalOffset(); // A function to be called when one of the |candidate_views_| receives a mouse // press event. void OnMousePressed(); // A function to be called when one of the |candidate_views_| receives a mouse // release event. void OnMouseReleased(); void set_cursor_location(const gfx::Rect& cursor_location) { cursor_location_ = cursor_location; } const gfx::Rect& cursor_location() const { return cursor_location_; } protected: // Override View::VisibilityChanged() virtual void VisibilityChanged(View* starting_from, bool is_visible) OVERRIDE; // Override View::OnBoundsChanged() virtual void OnBoundsChanged(const gfx::Rect& previous_bounds) OVERRIDE; private: // Initializes the candidate views if needed. void MaybeInitializeCandidateViews( const InputMethodLookupTable& lookup_table); // Creates the footer area, where we show status information. // For instance, we show a cursor position like 2/19. views::View* CreateFooterArea(); // Creates the header area, where we show auxiliary text. views::View* CreateHeaderArea(); // The lookup table (candidates). InputMethodLookupTable lookup_table_; // The index in the current page of the candidate currently being selected. int selected_candidate_index_in_page_; // The observers of the object. ObserverList<Observer> observers_; // The parent frame. views::Widget* parent_frame_; // Views created in the class will be part of tree of |this|, so these // child views will be deleted when |this| is deleted. // The candidate area is where candidates are rendered. views::View* candidate_area_; // The footer area is where the auxiliary text is shown, if the // orientation is vertical. Usually the auxiliary text is used for // showing candidate number information like 2/19. views::View* footer_area_; // We use this when we show something in the footer area. scoped_ptr<views::View> footer_area_contents_; // We use this when we show nothing in the footer area. scoped_ptr<views::View> footer_area_place_holder_; // The header area is where the auxiliary text is shown, if the // orientation is horizontal. If the auxiliary text is not provided, we // show nothing. For instance, we show pinyin text like "zhong'guo". views::View* header_area_; // We use this when we show something in the header area. scoped_ptr<views::View> header_area_contents_; // We use this when we show nothing in the header area. scoped_ptr<views::View> header_area_place_holder_; // The candidate views are used for rendering candidates. std::vector<CandidateView*> candidate_views_; // The header label is shown in the header area. views::Label* header_label_; // The footer label is shown in the footer area. views::Label* footer_label_; // Current columns width in |candidate_area_|. int previous_shortcut_column_width_; int previous_candidate_column_width_; int previous_annotation_column_width_; // The last cursor location. gfx::Rect cursor_location_; // true if a mouse button is pressed, and is not yet released. bool mouse_is_pressed_; }; // CandidateRow renderes a row of a candidate. class CandidateView : public views::View { public: CandidateView(CandidateWindowView* parent_candidate_window, int index_in_page, InputMethodLookupTable::Orientation orientation); virtual ~CandidateView() {} // Initializes the candidate view with the given column widths. // A width of 0 means that the column is resizable. void Init(int shortcut_column_width, int candidate_column_width, int annotation_column_width); // Sets candidate text to the given text. void SetCandidateText(const std::wstring& text); // Sets shortcut text to the given text. void SetShortcutText(const std::wstring& text); // Sets annotation text to the given text. void SetAnnotationText(const std::wstring& text); // Selects the candidate row. Changes the appearance to make it look // like a selected candidate. void Select(); // Unselects the candidate row. Changes the appearance to make it look // like an unselected candidate. void Unselect(); // Enables or disables the candidate row based on |enabled|. Changes the // appearance to make it look like unclickable area. void SetRowEnabled(bool enabled); // Returns the relative position of the candidate label. gfx::Point GetCandidateLabelPosition() const; private: // Overridden from View: virtual bool OnMousePressed(const views::MouseEvent& event) OVERRIDE; virtual bool OnMouseDragged(const views::MouseEvent& event) OVERRIDE; virtual void OnMouseReleased(const views::MouseEvent& event) OVERRIDE; virtual void OnMouseCaptureLost() OVERRIDE; // Zero-origin index in the current page. int index_in_page_; // The orientation of the candidate view. InputMethodLookupTable::Orientation orientation_; // The parent candidate window that contains this view. CandidateWindowView* parent_candidate_window_; // Views created in the class will be part of tree of |this|, so these // child views will be deleted when |this| is deleted. // The shortcut label renders shortcut numbers like 1, 2, and 3. views::Label* shortcut_label_; // The candidate label renders candidates. views::Label* candidate_label_; // The annotation label renders annotations. views::Label* annotation_label_; }; // The implementation of CandidateWindowController. // CandidateWindowController controls the CandidateWindow. class CandidateWindowController::Impl : public CandidateWindowView::Observer { public: Impl(); virtual ~Impl(); // Initializes the candidate window. Returns true on success. bool Init(); private: // CandidateWindowView::Observer implementation. virtual void OnCandidateCommitted(int index, int button, int flags); // Creates the candidate window view. void CreateView(); // The function is called when |HideAuxiliaryText| signal is received in // libcros. |input_method_library| is a void pointer to this object. static void OnHideAuxiliaryText(void* input_method_library); // The function is called when |HideLookupTable| signal is received in // libcros. |input_method_library| is a void pointer to this object. static void OnHideLookupTable(void* input_method_library); // The function is called when |SetCursorLocation| signal is received // in libcros. |input_method_library| is a void pointer to this object. static void OnSetCursorLocation(void* input_method_library, int x, int y, int width, int height); // The function is called when |UpdateAuxiliaryText| signal is received // in libcros. |input_method_library| is a void pointer to this object. static void OnUpdateAuxiliaryText(void* input_method_library, const std::string& utf8_text, bool visible); // The function is called when |UpdateLookupTable| signal is received // in libcros. |input_method_library| is a void pointer to this object. static void OnUpdateLookupTable(void* input_method_library, const InputMethodLookupTable& lookup_table); // This function is called by libcros when ibus connects or disconnects. // |input_method_library| is a void pointer to this object. static void OnConnectionChange(void* input_method_library, bool connected); // The connection is used for communicating with input method UI logic // in libcros. InputMethodUiStatusConnection* ui_status_connection_; // The candidate window view. CandidateWindowView* candidate_window_; // This is the outer frame of the candidate window view. The frame will // own |candidate_window_|. scoped_ptr<views::Widget> frame_; }; CandidateView::CandidateView( CandidateWindowView* parent_candidate_window, int index_in_page, InputMethodLookupTable::Orientation orientation) : index_in_page_(index_in_page), orientation_(orientation), parent_candidate_window_(parent_candidate_window), shortcut_label_(NULL), candidate_label_(NULL), annotation_label_(NULL) { } void CandidateView::Init(int shortcut_column_width, int candidate_column_width, int annotation_column_width) { views::GridLayout* layout = new views::GridLayout(this); SetLayoutManager(layout); // |this| owns |layout|. // Create Labels. shortcut_label_ = CreateShortcutLabel(orientation_); views::View* wrapped_shortcut_label = CreateWrappedShortcutLabel(shortcut_label_, orientation_); candidate_label_ = CreateCandidateLabel(orientation_); annotation_label_ = CreateAnnotationLabel(orientation_); // Initialize the column set with three columns. views::ColumnSet* column_set = layout->AddColumnSet(0); // If orientation is vertical, each column width is fixed. // Otherwise the width is resizable. const views::GridLayout::SizeType column_type = orientation_ == InputMethodLookupTable::kVertical ? views::GridLayout::FIXED : views::GridLayout::USE_PREF; const int padding_column_width = orientation_ == InputMethodLookupTable::kVertical ? 4 : 6; // Set shortcut column type and width. column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 0, column_type, shortcut_column_width, 0); column_set->AddPaddingColumn(0, padding_column_width); // Set candidate column type and width. column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 0, column_type, candidate_column_width, 0); column_set->AddPaddingColumn(0, padding_column_width); // Set annotation column type and width. column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 0, column_type, annotation_column_width, 0); column_set->AddPaddingColumn(0, padding_column_width); // Add the shortcut label, the candidate label, and annotation label. layout->StartRow(0, 0); // |wrapped_shortcut_label|, |candidate_label_|, and |annotation_label_| // will be owned by |this|. layout->AddView(wrapped_shortcut_label); layout->AddView(candidate_label_); layout->AddView(annotation_label_); } void CandidateView::SetCandidateText(const std::wstring& text) { candidate_label_->SetText(text); } void CandidateView::SetShortcutText(const std::wstring& text) { shortcut_label_->SetText(text); } void CandidateView::SetAnnotationText(const std::wstring& text) { annotation_label_->SetText(text); } void CandidateView::Select() { set_background( views::Background::CreateSolidBackground(kSelectedRowBackgroundColor)); set_border(views::Border::CreateSolidBorder(1, kSelectedRowFrameColor)); // Need to call SchedulePaint() for background and border color changes. SchedulePaint(); } void CandidateView::Unselect() { set_background(NULL); set_border(NULL); SchedulePaint(); // See comments at Select(). } void CandidateView::SetRowEnabled(bool enabled) { shortcut_label_->SetColor( enabled ? kShortcutColor : kDisabledShortcutColor); } gfx::Point CandidateView::GetCandidateLabelPosition() const { return candidate_label_->GetMirroredPosition(); } bool CandidateView::OnMousePressed(const views::MouseEvent& event) { parent_candidate_window_->OnMousePressed(); // Select the candidate. We'll commit the candidate when the mouse // button is released. parent_candidate_window_->SelectCandidateAt(index_in_page_); // Request MouseDraggged and MouseReleased events. return true; } bool CandidateView::OnMouseDragged(const views::MouseEvent& event) { gfx::Point location_in_candidate_window = event.location(); views::View::ConvertPointToView(this, parent_candidate_window_, &location_in_candidate_window); // Notify the candidate window that a candidate is now being dragged. parent_candidate_window_->OnCandidateDragged(location_in_candidate_window); // Request MouseReleased event. return true; } void CandidateView::OnMouseReleased(const views::MouseEvent& event) { // Commit the current candidate. parent_candidate_window_->CommitCandidate(); OnMouseCaptureLost(); } void CandidateView::OnMouseCaptureLost() { parent_candidate_window_->OnMouseReleased(); } CandidateWindowView::CandidateWindowView( views::Widget* parent_frame) : selected_candidate_index_in_page_(0), parent_frame_(parent_frame), candidate_area_(NULL), footer_area_(NULL), header_area_(NULL), header_label_(NULL), footer_label_(NULL), previous_shortcut_column_width_(0), previous_candidate_column_width_(0), previous_annotation_column_width_(0), mouse_is_pressed_(false) { } void CandidateWindowView::Init() { // Set the background and the border of the view. set_background( views::Background::CreateSolidBackground(kDefaultBackgroundColor)); set_border(views::Border::CreateSolidBorder(1, kFrameColor)); // Create the header area. header_area_ = CreateHeaderArea(); // Create the candidate area. candidate_area_ = new views::View; // Create the footer area. footer_area_ = CreateFooterArea(); // Set the window layout of the view views::GridLayout* layout = new views::GridLayout(this); SetLayoutManager(layout); // |this| owns layout|. views::ColumnSet* column_set = layout->AddColumnSet(0); column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 0, views::GridLayout::USE_PREF, 0, 0); // Add the header area. layout->StartRow(0, 0); layout->AddView(header_area_); // |header_area_| is owned by |this|. // Add the candidate area. layout->StartRow(0, 0); layout->AddView(candidate_area_); // |candidate_area_| is owned by |this|. // Add the footer area. layout->StartRow(0, 0); layout->AddView(footer_area_); // |footer_area_| is owned by |this|. } void CandidateWindowView::HideLookupTable() { if (!mouse_is_pressed_) { parent_frame_->Hide(); return; } // We should not hide the |frame_| when a mouse is pressed, so we don't run // into issues below. // // First, in the following scenario, it seems that the Views popup window does // not release mouse/keyboard grab even after it gets hidden. // // 1. create a popup window by views::Widget::CreateWidget() with the // accept_events flag set to true on the CreateParams. // 2. press a mouse button on the window. // 3. before releasing the mouse button, Hide() the window. // 4. release the button. // // And if we embed IME candidate window into Chrome, the window sometimes // receives an extra 'hide-lookup-table' event before mouse button is // released: // // 1. the candidate window is clicked. // 2. The mouse click handler in this file, OnMousePressed() in CandidateView, // is called, and the handler consumes the event by returning true. // 3. HOWEVER, if the candidate window is embedded into Chrome, the event is // also sent to Chrome! (problem #1) // 4. im-ibus.so in Chrome sends 'focus-out' event to ibus-daemon. // 5. ibus-daemon sends 'hide-lookup-table' event to the candidate window. // 6. the window is hidden, but the window does not release mouse/keyboard // grab! (problem #2) // 7. mouse button is released. // 8. now all mouse/keyboard events are consumed by the hidden popup, and are // not sent to Chrome. // // TODO(yusukes): investigate why the click event is sent to both candidate // window and Chrome. http://crosbug.com/11423 // TODO(yusukes): investigate if we could fix Views so it always releases grab // when a popup window gets hidden. http://crosbug.com/11422 // LOG(WARNING) << "Can't hide the table since a mouse button is not released."; } void CandidateWindowView::OnMousePressed() { mouse_is_pressed_ = true; } void CandidateWindowView::OnMouseReleased() { mouse_is_pressed_ = false; } void CandidateWindowView::HideAuxiliaryText() { views::View* target_area = ( lookup_table_.orientation == InputMethodLookupTable::kHorizontal ? header_area_ : footer_area_); views::View* target_place_holder = ( lookup_table_.orientation == InputMethodLookupTable::kHorizontal ? header_area_place_holder_.get() : footer_area_place_holder_.get()); // Put the place holder to the target display area. target_area->RemoveAllChildViews(false); // Don't delete child views. target_area->AddChildView(target_place_holder); } void CandidateWindowView::ShowAuxiliaryText() { views::View* target_area = ( lookup_table_.orientation == InputMethodLookupTable::kHorizontal ? header_area_ : footer_area_); views::View* target_contents = ( lookup_table_.orientation == InputMethodLookupTable::kHorizontal ? header_area_contents_.get() : footer_area_contents_.get()); if (target_contents->parent() != target_area) { // If contents not in display area, put it in. target_area->RemoveAllChildViews(false); // Don't delete child views. target_area->AddChildView(target_contents); } } void CandidateWindowView::UpdateAuxiliaryText(const std::string& utf8_text) { views::Label* target_label = ( lookup_table_.orientation == InputMethodLookupTable::kHorizontal ? header_label_ : footer_label_); target_label->SetText(UTF8ToWide(utf8_text)); } bool CandidateWindowView::ShouldUpdateCandidateViews( const InputMethodLookupTable& old_table, const InputMethodLookupTable& new_table) { // Check if most table contents are identical. if (old_table.page_size == new_table.page_size && old_table.orientation == new_table.orientation && old_table.candidates == new_table.candidates && old_table.labels == new_table.labels && old_table.annotations == new_table.annotations && // Check if the page indexes are identical. ComputePageIndex(old_table) == ComputePageIndex(new_table)) { // If all of the conditions are met, we don't have to update candidate // views. return false; } return true; } void CandidateWindowView::UpdateCandidates( const InputMethodLookupTable& new_lookup_table) { const bool should_update = ShouldUpdateCandidateViews(lookup_table_, new_lookup_table); // Updating the candidate views is expensive. We'll skip this if possible. if (should_update) { // Initialize candidate views if necessary. MaybeInitializeCandidateViews(new_lookup_table); // Compute the index of the current page. const int current_page_index = ComputePageIndex(new_lookup_table); if (current_page_index < 0) { LOG(ERROR) << "Invalid lookup_table: " << new_lookup_table.ToString(); return; } // Update the candidates in the current page. const size_t start_from = current_page_index * new_lookup_table.page_size; // In some cases, engines send empty shortcut labels. For instance, // ibus-mozc sends empty labels when they show suggestions. In this // case, we should not show shortcut labels. const bool no_shortcut_mode = (start_from < new_lookup_table.labels.size() && new_lookup_table.labels[start_from] == ""); for (size_t i = 0; i < candidate_views_.size(); ++i) { const size_t index_in_page = i; const size_t candidate_index = start_from + index_in_page; CandidateView* candidate_view = candidate_views_[index_in_page]; // Set the shortcut text. if (no_shortcut_mode) { candidate_view->SetShortcutText(L""); } else { // At this moment, we don't use labels sent from engines for UX // reasons. First, we want to show shortcut labels in empty rows // (ex. show 6, 7, 8, ... in empty rows when the number of // candidates is 5). Second, we want to add a period after each // shortcut label when the candidate window is horizontal. candidate_view->SetShortcutText( CreateShortcutText(i, new_lookup_table.orientation)); } // Set the candidate text. if (candidate_index < new_lookup_table.candidates.size() && candidate_index < new_lookup_table.annotations.size()) { candidate_view->SetCandidateText( UTF8ToWide(new_lookup_table.candidates[candidate_index])); candidate_view->SetAnnotationText( UTF8ToWide(new_lookup_table.annotations[candidate_index])); candidate_view->SetRowEnabled(true); } else { // Disable the empty row. candidate_view->SetCandidateText(L""); candidate_view->SetAnnotationText(L""); candidate_view->SetRowEnabled(false); } } } // Update the current lookup table. We'll use lookup_table_ from here. // Note that SelectCandidateAt() uses lookup_table_. lookup_table_ = new_lookup_table; // Select the current candidate in the page. const int current_candidate_in_page = lookup_table_.cursor_absolute_index % lookup_table_.page_size; SelectCandidateAt(current_candidate_in_page); } void CandidateWindowView::MaybeInitializeCandidateViews( const InputMethodLookupTable& lookup_table) { const InputMethodLookupTable::Orientation orientation = lookup_table.orientation; const int page_size = lookup_table.page_size; // Current column width. int shortcut_column_width = 0; int candidate_column_width = 0; int annotation_column_width = 0; // If orientation is horizontal, don't need to compute width, // because each label is left aligned. if (orientation == InputMethodLookupTable::kVertical) { shortcut_column_width = ComputeShortcutColumnWidth(lookup_table); candidate_column_width = ComputeCandidateColumnWidth(lookup_table); annotation_column_width = ComputeAnnotationColumnWidth(lookup_table); } // If the requested number of views matches the number of current views, and // previous and current column width are same, just reuse these. // // Note that the early exit logic is not only useful for improving // performance, but also necessary for the horizontal candidate window // to be redrawn properly. If we get rid of the logic, the horizontal // candidate window won't get redrawn properly for some reason when // there is no size change. You can test this by removing "return" here // and type "ni" with Pinyin input method. if (static_cast<int>(candidate_views_.size()) == page_size && lookup_table_.orientation == orientation && previous_shortcut_column_width_ == shortcut_column_width && previous_candidate_column_width_ == candidate_column_width && previous_annotation_column_width_ == annotation_column_width) { return; } // Update the previous column widths. previous_shortcut_column_width_ = shortcut_column_width; previous_candidate_column_width_ = candidate_column_width; previous_annotation_column_width_ = annotation_column_width; // Clear the existing candidate_views if any. for (size_t i = 0; i < candidate_views_.size(); ++i) { candidate_area_->RemoveChildView(candidate_views_[i]); } candidate_views_.clear(); views::GridLayout* layout = new views::GridLayout(candidate_area_); // |candidate_area_| owns |layout|. candidate_area_->SetLayoutManager(layout); // Initialize the column set. views::ColumnSet* column_set = layout->AddColumnSet(0); if (orientation == InputMethodLookupTable::kVertical) { column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 0, views::GridLayout::USE_PREF, 0, 0); } else { for (int i = 0; i < page_size; ++i) { column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 0, views::GridLayout::USE_PREF, 0, 0); } } // Set insets so the border of the selected candidate is drawn inside of // the border of the main candidate window, but we don't have the inset // at the top and the bottom as we have the borders of the header and // footer areas. const gfx::Insets kCandidateAreaInsets(0, 1, 0, 1); layout->SetInsets(kCandidateAreaInsets.top(), kCandidateAreaInsets.left(), kCandidateAreaInsets.bottom(), kCandidateAreaInsets.right()); // Add views to the candidate area. if (orientation == InputMethodLookupTable::kHorizontal) { layout->StartRow(0, 0); } for (int i = 0; i < page_size; ++i) { CandidateView* candidate_row = new CandidateView(this, i, orientation); candidate_row->Init(shortcut_column_width, candidate_column_width, annotation_column_width); candidate_views_.push_back(candidate_row); if (orientation == InputMethodLookupTable::kVertical) { layout->StartRow(0, 0); } // |candidate_row| will be owned by |candidate_area_|. layout->AddView(candidate_row); } // Compute views size in |layout|. // If we don't call this function, GetHorizontalOffset() often // returns invalid value (returns 0), then candidate window // moves right from the correct position in MoveParentFrame(). // TODO(nhiroki): Figure out why it returns invalid value. // It seems that the x-position of the candidate labels is not set. layout->Layout(candidate_area_); } views::View* CandidateWindowView::CreateHeaderArea() { // |header_area_place_holder_| will not be owned by another view. // This will be deleted by scoped_ptr. // // This is because we swap the contents of |header_area_| between // |header_area_place_holder_| (to show nothing) and // |header_area_contents_| (to show something). In other words, // |header_area_| only contains one of the two views hence cannot own // the two views at the same time. header_area_place_holder_.reset(new views::View); header_area_place_holder_->set_parent_owned(false); // Won't be owened. // |header_label_| will be owned by |header_area_contents_|. header_label_ = new views::Label; header_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); const gfx::Insets kHeaderInsets(2, 2, 2, 4); // |header_area_contents_| will not be owned by another view. // See a comment at |header_area_place_holder_| for why. header_area_contents_.reset( WrapWithPadding(header_label_, kHeaderInsets)); header_area_contents_->set_parent_owned(false); // Won't be owened. header_area_contents_->set_border( views::Border::CreateSolidBorder(1, kFrameColor)); header_area_contents_->set_background( views::Background::CreateVerticalGradientBackground( kFooterTopColor, kFooterBottomColor)); views::View* header_area = new views::View; header_area->SetLayoutManager(new views::FillLayout); // Initialize the header area with the place holder (i.e. show nothing). header_area->AddChildView(header_area_place_holder_.get()); return header_area; } views::View* CandidateWindowView::CreateFooterArea() { // |footer_area_place_holder_| will not be owned by another view. // See also the comment about |header_area_place_holder_| in // CreateHeaderArea(). footer_area_place_holder_.reset(new views::View); footer_area_place_holder_->set_parent_owned(false); // Won't be owened. footer_label_ = new views::Label(); footer_label_->SetHorizontalAlignment(views::Label::ALIGN_RIGHT); const gfx::Insets kFooterInsets(2, 2, 2, 4); footer_area_contents_.reset( WrapWithPadding(footer_label_, kFooterInsets)); footer_area_contents_->set_parent_owned(false); // Won't be owened. footer_area_contents_->set_border( views::Border::CreateSolidBorder(1, kFrameColor)); footer_area_contents_->set_background( views::Background::CreateVerticalGradientBackground( kFooterTopColor, kFooterBottomColor)); views::View* footer_area = new views::View; footer_area->SetLayoutManager(new views::FillLayout); // Initialize the footer area with the place holder (i.e. show nothing). footer_area->AddChildView(footer_area_place_holder_.get()); return footer_area; } void CandidateWindowView::SelectCandidateAt(int index_in_page) { const int current_page_index = ComputePageIndex(lookup_table_); if (current_page_index < 0) { LOG(ERROR) << "Invalid lookup_table: " << lookup_table_.ToString(); return; } const int cursor_absolute_index = lookup_table_.page_size * current_page_index + index_in_page; // Ignore click on out of range views. if (cursor_absolute_index < 0 || cursor_absolute_index >= static_cast<int>(lookup_table_.candidates.size())) { return; } // Unselect the currently selected candidate. candidate_views_[selected_candidate_index_in_page_]->Unselect(); // Remember the currently selected candidate index in the current page. selected_candidate_index_in_page_ = index_in_page; // Select the candidate specified by index_in_page. candidate_views_[index_in_page]->Select(); // Update the cursor indexes in the model. lookup_table_.cursor_absolute_index = cursor_absolute_index; } void CandidateWindowView::OnCandidateDragged( const gfx::Point& location) { for (size_t i = 0; i < candidate_views_.size(); ++i) { gfx::Point converted_location = location; views::View::ConvertPointToView(this, candidate_views_[i], &converted_location); if (candidate_views_[i]->HitTest(converted_location)) { SelectCandidateAt(i); break; } } } void CandidateWindowView::CommitCandidate() { // For now, we don't distinguish left and right clicks. const int button = 1; // Left button. const int key_modifilers = 0; FOR_EACH_OBSERVER(Observer, observers_, OnCandidateCommitted(selected_candidate_index_in_page_, button, key_modifilers)); } void CandidateWindowView::ResizeAndMoveParentFrame() { ResizeParentFrame(); MoveParentFrame(); } void CandidateWindowView::ResizeParentFrame() { // Resize the parent frame, with the current candidate window size. gfx::Size size = GetPreferredSize(); gfx::Rect bounds = parent_frame_->GetClientAreaScreenBounds(); // SetBounds() is not cheap. Only call this when the size is changed. if (bounds.size() != size) { bounds.set_size(size); parent_frame_->SetBounds(bounds); } } void CandidateWindowView::MoveParentFrame() { const int x = cursor_location_.x(); const int y = cursor_location_.y(); const int height = cursor_location_.height(); const int horizontal_offset = GetHorizontalOffset(); gfx::Rect frame_bounds = parent_frame_->GetClientAreaScreenBounds(); gfx::Rect screen_bounds = views::Screen::GetMonitorWorkAreaNearestWindow( parent_frame_->GetNativeView()); // The default position. frame_bounds.set_x(x + horizontal_offset); frame_bounds.set_y(y + height); // Handle overflow at the left and the top. frame_bounds.set_x(std::max(frame_bounds.x(), screen_bounds.x())); frame_bounds.set_y(std::max(frame_bounds.y(), screen_bounds.y())); // Handle overflow at the right. const int right_overflow = frame_bounds.right() - screen_bounds.right(); if (right_overflow > 0) { frame_bounds.set_x(frame_bounds.x() - right_overflow); } // Handle overflow at the bottom. const int bottom_overflow = frame_bounds.bottom() - screen_bounds.bottom(); if (bottom_overflow > 0) { frame_bounds.set_y(frame_bounds.y() - height - frame_bounds.height()); } // Move the window per the cursor location. parent_frame_->SetBounds(frame_bounds); } int CandidateWindowView::GetHorizontalOffset() { // Compute the horizontal offset if the lookup table is vertical. if (!candidate_views_.empty() && lookup_table_.orientation == InputMethodLookupTable::kVertical) { return - candidate_views_[0]->GetCandidateLabelPosition().x(); } return 0; } void CandidateWindowView::VisibilityChanged(View* starting_from, bool is_visible) { if (is_visible) { // If the visibility of candidate window is changed, // we should move the frame to the right position. MoveParentFrame(); } } void CandidateWindowView::OnBoundsChanged(const gfx::Rect& previous_bounds) { // If the bounds(size) of candidate window is changed, // we should move the frame to the right position. View::OnBoundsChanged(previous_bounds); MoveParentFrame(); } bool CandidateWindowController::Impl::Init() { // Initialize the input method UI status connection. InputMethodUiStatusMonitorFunctions functions; functions.hide_auxiliary_text = &CandidateWindowController::Impl::OnHideAuxiliaryText; functions.hide_lookup_table = &CandidateWindowController::Impl::OnHideLookupTable; functions.set_cursor_location = &CandidateWindowController::Impl::OnSetCursorLocation; functions.update_auxiliary_text = &CandidateWindowController::Impl::OnUpdateAuxiliaryText; functions.update_lookup_table = &CandidateWindowController::Impl::OnUpdateLookupTable; ui_status_connection_ = MonitorInputMethodUiStatus(functions, this); if (!ui_status_connection_) { LOG(ERROR) << "MonitorInputMethodUiStatus() failed."; return false; } MonitorInputMethodConnection( ui_status_connection_, &CandidateWindowController::Impl::OnConnectionChange); // Create the candidate window view. CreateView(); return true; } void CandidateWindowController::Impl::CreateView() { // Create a non-decorated frame. frame_.reset(views::Widget::CreateWidget( views::Widget::CreateParams(views::Widget::CreateParams::TYPE_POPUP))); // The size is initially zero. frame_->Init(NULL, gfx::Rect(0, 0)); // Create the candidate window. candidate_window_ = new CandidateWindowView(frame_.get()); candidate_window_->Init(); candidate_window_->AddObserver(this); // Put the candidate window view on the frame. The frame is resized // later when the candidate window is shown. views::RootView* root_view = frame_->GetRootView(); // |root_view| owns the |candidate_window_|, thus |frame_| effectively // owns |candidate_window_|. root_view->SetContentsView(candidate_window_); } CandidateWindowController::Impl::Impl() : ui_status_connection_(NULL), frame_(NULL) { } CandidateWindowController::Impl::~Impl() { candidate_window_->RemoveObserver(this); chromeos::DisconnectInputMethodUiStatus(ui_status_connection_); } void CandidateWindowController::Impl::OnHideAuxiliaryText( void* input_method_library) { CandidateWindowController::Impl* controller = static_cast<CandidateWindowController::Impl*>(input_method_library); controller->candidate_window_->HideAuxiliaryText(); controller->candidate_window_->ResizeParentFrame(); } void CandidateWindowController::Impl::OnHideLookupTable( void* input_method_library) { CandidateWindowController::Impl* controller = static_cast<CandidateWindowController::Impl*>(input_method_library); controller->candidate_window_->HideLookupTable(); } void CandidateWindowController::Impl::OnSetCursorLocation( void* input_method_library, int x, int y, int width, int height) { CandidateWindowController::Impl* controller = static_cast<CandidateWindowController::Impl*>(input_method_library); // A workaround for http://crosbug.com/6460. We should ignore very short Y // move to prevent the window from shaking up and down. const int kKeepPositionThreshold = 2; // px const gfx::Rect& last_location = controller->candidate_window_->cursor_location(); const int delta_y = abs(last_location.y() - y); if ((last_location.x() == x) && (delta_y <= kKeepPositionThreshold)) { DLOG(INFO) << "Ignored set_cursor_location signal to prevent window shake"; return; } // Remember the cursor location. controller->candidate_window_->set_cursor_location( gfx::Rect(x, y, width, height)); // Move the window per the cursor location. controller->candidate_window_->MoveParentFrame(); } void CandidateWindowController::Impl::OnUpdateAuxiliaryText( void* input_method_library, const std::string& utf8_text, bool visible) { CandidateWindowController::Impl* controller = static_cast<CandidateWindowController::Impl*>(input_method_library); // If it's not visible, hide the auxiliary text and return. if (!visible) { controller->candidate_window_->HideAuxiliaryText(); return; } controller->candidate_window_->UpdateAuxiliaryText(utf8_text); controller->candidate_window_->ShowAuxiliaryText(); controller->candidate_window_->ResizeParentFrame(); } void CandidateWindowController::Impl::OnUpdateLookupTable( void* input_method_library, const InputMethodLookupTable& lookup_table) { CandidateWindowController::Impl* controller = static_cast<CandidateWindowController::Impl*>(input_method_library); // If it's not visible, hide the window and return. if (!lookup_table.visible) { controller->candidate_window_->HideLookupTable(); return; } controller->candidate_window_->UpdateCandidates(lookup_table); controller->candidate_window_->ResizeParentFrame(); controller->frame_->Show(); } void CandidateWindowController::Impl::OnCandidateCommitted(int index, int button, int flags) { NotifyCandidateClicked(ui_status_connection_, index, button, flags); } void CandidateWindowController::Impl::OnConnectionChange( void* input_method_library, bool connected) { if (!connected) { CandidateWindowController::Impl* controller = static_cast<CandidateWindowController::Impl*>(input_method_library); controller->candidate_window_->HideLookupTable(); } } CandidateWindowController::CandidateWindowController() : impl_(new CandidateWindowController::Impl) { } CandidateWindowController::~CandidateWindowController() { delete impl_; } bool CandidateWindowController::Init() { return impl_->Init(); } } // namespace chromeos