| // Copyright (c) 2012 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/message_loop.h" |
| #include "base/observer_list.h" |
| #include "base/string_util.h" |
| #include "base/stringprintf.h" |
| #include "base/timer.h" |
| #include "base/utf_string_conversions.h" |
| #include "chrome/browser/chromeos/input_method/candidate_view.h" |
| #include "chrome/browser/chromeos/input_method/candidate_window_view.h" |
| #include "chrome/browser/chromeos/input_method/ibus_ui_controller.h" |
| #include "grit/generated_resources.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/font.h" |
| #include "ui/gfx/screen.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/controls/textfield/textfield.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/layout/grid_layout.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/window/non_client_view.h" |
| |
| #if defined(USE_ASH) |
| #include "ash/shell.h" |
| #include "ash/shell_window_ids.h" |
| #include "ash/wm/window_animations.h" |
| #endif // USE_ASH |
| |
| namespace chromeos { |
| namespace input_method { |
| |
| 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); |
| const SkColor kSelectedInfolistRowBackgroundColor = |
| SkColorSetRGB(0xee, 0xee, 0xee); |
| const SkColor kSelectedInfolistRowFrameColor = SkColorSetRGB(0xcc, 0xcc, 0xcc); |
| const SkColor kInfolistTitleBackgroundColor = SkColorSetRGB(0xdd, 0xdd, 0xdd); |
| |
| // We'll use a bigger font size, so Chinese characters are more readable |
| // in the candidate window. |
| const int kFontSizeDelta = 2; |
| |
| // Currently the infolist window only supports Japanese font. |
| #if defined(GOOGLE_CHROME_BUILD) |
| const char kJapaneseFontName[] = "MotoyaG04Gothic"; |
| #else |
| const char kJapaneseFontName[] = "IPAPGothic"; |
| #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; |
| // The minimum width of preedit area. We use this value to prevent the |
| // candidate window from being too narrow when candidate lists are not shown. |
| const int kMinPreeditAreaWidth = 134; |
| |
| // The milliseconds of the delay to show the infolist window. |
| const int kInfolistShowDelayMilliSeconds = 500; |
| // The milliseconds of the delay to hide the infolist window. |
| const int kInfolistHideDelayMilliSeconds = 500; |
| // The width of the infolist indicator icon in the candidate window. |
| const int kInfolistIndicatorIconWidth = 4; |
| // The padding size of the infolist indicator icon in the candidate window. |
| const int kInfolistIndicatorIconPadding = 2; |
| |
| // VerticalCandidateLabel is used for rendering candidate text in |
| // the vertical candidate window. |
| class VerticalCandidateLabel : public views::Label { |
| public: |
| VerticalCandidateLabel() {} |
| |
| private: |
| 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; |
| } |
| |
| DISALLOW_COPY_AND_ASSIGN(VerticalCandidateLabel); |
| }; |
| |
| // 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. |
| string16 CreateShortcutText(int index, |
| const InputMethodLookupTable& table) { |
| // Choose the character used for the shortcut label. |
| const char kShortcutCharacters[] = "1234567890ABCDEF"; |
| // The default character should not be used but just in case. |
| std::string shortcut_text = " "; |
| if (table.labels.empty()) { |
| // -1 to exclude the null character at the end. |
| if (index < static_cast<int>(arraysize(kShortcutCharacters) - 1)) |
| shortcut_text = std::string(1, kShortcutCharacters[index]); |
| } else { |
| if (index < static_cast<int>(table.labels.size())) |
| shortcut_text = table.labels[index]; |
| } |
| |
| if (table.orientation != InputMethodLookupTable::kVertical) |
| shortcut_text += '.'; |
| return UTF8ToUTF16(shortcut_text); |
| } |
| |
| // Creates the shortcut label, and returns it (never returns NULL). |
| // The label text is not set in this function. |
| views::Label* CreateShortcutLabel( |
| 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 == 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->SetEnabledColor(kShortcutColor); |
| shortcut_label->SetDisabledColor(kDisabledShortcutColor); |
| |
| 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, |
| 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 == InputMethodLookupTable::kVertical ? |
| kVerticalShortcutLabelInsets : |
| kHorizontalShortcutLabelInsets); |
| views::View* wrapped_shortcut_label = |
| WrapWithPadding(shortcut_label, insets); |
| |
| // Add decoration based on the orientation. |
| if (orientation == InputMethodLookupTable::kVertical) { |
| // Set the background color. |
| wrapped_shortcut_label->set_background( |
| views::Background::CreateSolidBackground( |
| kShortcutBackgroundColor)); |
| shortcut_label->SetBackgroundColor( |
| wrapped_shortcut_label->background()->get_color()); |
| } |
| |
| 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( |
| 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 == 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( |
| 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->SetEnabledColor(kAnnotationColor); |
| annotation_label->SetHorizontalAlignment(views::Label::ALIGN_LEFT); |
| |
| return annotation_label; |
| } |
| |
| // Computes shortcut column size. |
| gfx::Size ComputeShortcutColumnSize( |
| const InputMethodLookupTable& lookup_table) { |
| int shortcut_column_width = 0; |
| int shortcut_column_height = 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 and height in shortcut labels. |
| // We'll create temporary shortcut labels, and choose the largest width and |
| // height. |
| for (int i = 0; i < lookup_table.page_size; ++i) { |
| shortcut_label->SetText(CreateShortcutText(i, lookup_table)); |
| gfx::Size text_size = wrapped_shortcut_label->GetPreferredSize(); |
| shortcut_column_width = std::max(shortcut_column_width, text_size.width()); |
| shortcut_column_height = std::max(shortcut_column_height, |
| text_size.height()); |
| } |
| |
| return gfx::Size(shortcut_column_width, shortcut_column_height); |
| } |
| |
| // 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 InputMethodLookupTable& lookup_table) { |
| if (lookup_table.page_size > 0) |
| return lookup_table.cursor_absolute_index / lookup_table.page_size; |
| return -1; |
| } |
| |
| // Computes candidate column size. |
| gfx::Size ComputeCandidateColumnSize( |
| const InputMethodLookupTable& lookup_table) { |
| int candidate_column_width = 0; |
| int candidate_column_height = 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 gfx::Size(0, 0); |
| const size_t start_from = current_page_index * lookup_table.page_size; |
| |
| // Compute the max width and height in candidate labels. |
| // We'll create temporary candidate labels, and choose the largest width and |
| // height. |
| for (size_t i = 0; i + start_from < lookup_table.candidates.size(); ++i) { |
| const size_t index = start_from + i; |
| |
| candidate_label->SetText( |
| UTF8ToUTF16(lookup_table.candidates[index])); |
| gfx::Size text_size = candidate_label->GetPreferredSize(); |
| candidate_column_width = std::max(candidate_column_width, |
| text_size.width()); |
| candidate_column_height = std::max(candidate_column_height, |
| text_size.height()); |
| } |
| |
| return gfx::Size(candidate_column_width, candidate_column_height); |
| } |
| |
| // Computes annotation column size. |
| gfx::Size ComputeAnnotationColumnSize( |
| const InputMethodLookupTable& lookup_table) { |
| int annotation_column_width = 0; |
| int annotation_column_height = 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 gfx::Size(0, 0); |
| const size_t start_from = current_page_index * lookup_table.page_size; |
| |
| // Compute max width and height in annotation labels. |
| // We'll create temporary annotation labels, and choose the largest width and |
| // height. |
| for (size_t i = 0; i + start_from < lookup_table.annotations.size(); ++i) { |
| const size_t index = start_from + i; |
| |
| annotation_label->SetText( |
| UTF8ToUTF16(lookup_table.annotations[index])); |
| gfx::Size text_size = annotation_label->GetPreferredSize(); |
| annotation_column_width = std::max(annotation_column_width, |
| text_size.width()); |
| annotation_column_height = std::max(annotation_column_height, |
| text_size.height()); |
| } |
| |
| return gfx::Size(annotation_column_width, annotation_column_height); |
| } |
| |
| } // namespace |
| |
| // HidableArea is used as an area to place optional information that can be |
| // turned displaying off if it is unnecessary. |
| class HidableArea : public views::View { |
| public: |
| HidableArea() { |
| // |place_holder_| will be deleted by scoped_ptr, rather than |
| // the standard owning relation of views::View. |
| // |
| // This is because we swap the contents of HidableArea between |
| // |place_holder_| (to show nothing) and |contents_| (to show something). |
| // In other words, the HidableArea only contains one of the two views |
| // hence cannot own the two views at the same time. |
| place_holder_.reset(new views::View); |
| place_holder_->set_owned_by_client(); // Won't own |
| |
| // Initially show nothing. |
| SetLayoutManager(new views::FillLayout); |
| AddChildView(place_holder_.get()); |
| } |
| |
| // Sets the content view. |
| void SetContents(views::View* contents) { |
| contents_.reset(contents); |
| contents_->set_owned_by_client(); // Won't own |
| } |
| |
| // Shows the content. |
| void Show() { |
| if (contents_.get() && contents_->parent() != this) { |
| RemoveAllChildViews(false); // Don't delete child views. |
| AddChildView(contents_.get()); |
| } |
| } |
| |
| // Hides the content. |
| void Hide() { |
| if (IsShown()) { |
| RemoveAllChildViews(false); // Don't delete child views. |
| AddChildView(place_holder_.get()); |
| } |
| } |
| |
| // Returns whether the content is already set and shown. |
| bool IsShown() const { |
| return contents_.get() && contents_->parent() == this; |
| } |
| |
| // Returns the content. |
| views::View* contents() { |
| return contents_.get(); |
| } |
| |
| private: |
| scoped_ptr<views::View> contents_; |
| scoped_ptr<views::View> place_holder_; |
| |
| DISALLOW_COPY_AND_ASSIGN(HidableArea); |
| }; |
| |
| // InformationTextArea is a HidableArea having a single Label in it. |
| class InformationTextArea : public HidableArea { |
| public: |
| // Specify the alignment and initialize the control. |
| InformationTextArea(views::Label::Alignment align, int minWidth) |
| : minWidth_(minWidth) { |
| label_ = new views::Label; |
| label_->SetHorizontalAlignment(align); |
| |
| const gfx::Insets kInsets(2, 2, 2, 4); |
| views::View* contents = WrapWithPadding(label_, kInsets); |
| SetContents(contents); |
| contents->set_border( |
| views::Border::CreateSolidBorder(1, kFrameColor)); |
| contents->set_background( |
| views::Background::CreateVerticalGradientBackground( |
| kFooterTopColor, |
| kFooterBottomColor)); |
| label_->SetBackgroundColor(contents->background()->get_color()); |
| } |
| |
| // Set the displayed text. |
| void SetText(const std::string& utf8_text) { |
| label_->SetText(UTF8ToUTF16(utf8_text)); |
| } |
| |
| protected: |
| virtual gfx::Size GetPreferredSize() OVERRIDE { |
| gfx::Size size = HidableArea::GetPreferredSize(); |
| // Hack. +2 is needed as the same reason as in VerticalCandidateLabel |
| size.set_width(size.width() + 2); |
| if (size.width() < minWidth_) { |
| size.set_width(minWidth_); |
| } |
| return size; |
| } |
| |
| private: |
| views::Label* label_; |
| int minWidth_; |
| |
| DISALLOW_COPY_AND_ASSIGN(InformationTextArea); |
| }; |
| |
| // InfolistRow renderes a row of a infolist. |
| class InfolistView : public views::View { |
| public: |
| explicit InfolistView(InfolistWindowView* parent_infolist_window); |
| virtual ~InfolistView() {} |
| |
| void Init(); |
| |
| // Sets title text and description text. |
| void SetTitleAndDescriptionText(const string16& title, |
| const string16& description); |
| |
| // Selects the infolist row. Changes the appearance to make it look |
| // like a selected candidate. |
| void Select(); |
| |
| // Unselects the infolist row. Changes the appearance to make it look |
| // like an unselected candidate. |
| void Unselect(); |
| |
| protected: |
| virtual gfx::Size GetPreferredSize() OVERRIDE { |
| return title_and_description_size_; |
| } |
| |
| private: |
| // Notifies labels of their new background colors. Called whenever the view's |
| // background color changes. |
| void UpdateLabelBackgroundColors(); |
| |
| // The parent candidate window that contains this view. |
| InfolistWindowView* parent_infolist_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 title label. |
| views::Label* title_label_; |
| // The description label. |
| views::Label* description_label_; |
| // Whether the item is selected. |
| bool selected_; |
| // The size of the area which contains the title and the description. |
| gfx::Size title_and_description_size_; |
| |
| DISALLOW_COPY_AND_ASSIGN(InfolistView); |
| }; |
| |
| // The implementation of CandidateWindowController. |
| // CandidateWindowController controls the CandidateWindow. |
| class CandidateWindowControllerImpl : public CandidateWindowController, |
| public CandidateWindowView::Observer, |
| public IBusUiController::Observer { |
| public: |
| CandidateWindowControllerImpl(); |
| virtual ~CandidateWindowControllerImpl(); |
| |
| // Initializes the candidate window. Returns true on success. |
| virtual bool Init() OVERRIDE; |
| |
| virtual void AddObserver( |
| CandidateWindowController::Observer* observer) OVERRIDE; |
| virtual void RemoveObserver( |
| CandidateWindowController::Observer* observer) OVERRIDE; |
| |
| private: |
| // CandidateWindowView::Observer implementation. |
| virtual void OnCandidateCommitted(int index, |
| int button, |
| int flags); |
| virtual void OnCandidateWindowOpened(); |
| virtual void OnCandidateWindowClosed(); |
| |
| // Creates the candidate window view. |
| void CreateView(); |
| |
| // IBusUiController::Observer overrides. |
| virtual void OnHideAuxiliaryText(); |
| virtual void OnHideLookupTable(); |
| virtual void OnHidePreeditText(); |
| virtual void OnSetCursorLocation(const gfx::Rect& cursor_position, |
| const gfx::Rect& composition_head); |
| virtual void OnUpdateAuxiliaryText(const std::string& utf8_text, |
| bool visible); |
| virtual void OnUpdateLookupTable(const InputMethodLookupTable& lookup_table); |
| virtual void OnUpdatePreeditText(const std::string& utf8_text, |
| unsigned int cursor, bool visible); |
| virtual void OnConnectionChange(bool connected); |
| |
| // The controller is used for communicating with the IBus daemon. |
| scoped_ptr<IBusUiController> ibus_ui_controller_; |
| |
| // 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_; |
| |
| // The infolist window view. |
| InfolistWindowView* infolist_window_; |
| |
| // This is the outer frame of the infolist window view. The frame will |
| // own |infolist_window_|. |
| scoped_ptr<views::Widget> infolist_frame_; |
| |
| ObserverList<CandidateWindowController::Observer> observers_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CandidateWindowControllerImpl); |
| }; |
| |
| 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), |
| infolist_icon_(NULL), |
| infolist_icon_enabled_(false) { |
| } |
| |
| void CandidateView::Init(int shortcut_column_width, |
| int candidate_column_width, |
| int annotation_column_width, |
| int column_height) { |
| 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); |
| |
| if (orientation_ == InputMethodLookupTable::kVertical) { |
| column_set->AddPaddingColumn(0, 1); |
| column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 0, |
| views::GridLayout::FIXED, kInfolistIndicatorIconWidth, |
| 0); |
| column_set->AddPaddingColumn(0, 2); |
| } else { |
| 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, |
| 1, // Column span. |
| 1, // Row span. |
| views::GridLayout::FILL, // Horizontal alignment. |
| views::GridLayout::FILL, // Vertical alignment. |
| -1, // Preferred width, not specified. |
| column_height); // Preferred height. |
| layout->AddView(candidate_label_, |
| 1, // Column span. |
| 1, // Row span. |
| views::GridLayout::FILL, // Horizontal alignment. |
| views::GridLayout::FILL, // Vertical alignment. |
| -1, // Preferred width, not specified. |
| column_height); // Preferred height. |
| layout->AddView(annotation_label_, |
| 1, // Column span. |
| 1, // Row span. |
| views::GridLayout::FILL, // Horizontal alignment. |
| views::GridLayout::FILL, // Vertical alignemnt. |
| -1, // Preferred width, not specified. |
| column_height); // Preferred height. |
| if (orientation_ == InputMethodLookupTable::kVertical) { |
| infolist_icon_ = new views::View; |
| views::View* infolist_icon_wrapper = new views::View; |
| views::GridLayout* infolist_icon_layout = |
| new views::GridLayout(infolist_icon_wrapper); |
| // |infolist_icon_layout| is owned by |infolist_icon_wrapper|. |
| infolist_icon_wrapper->SetLayoutManager(infolist_icon_layout); |
| infolist_icon_layout->AddColumnSet(0)->AddColumn( |
| views::GridLayout::FILL, views::GridLayout::FILL, |
| 0, views::GridLayout::FIXED, kInfolistIndicatorIconWidth, 0); |
| infolist_icon_layout->AddPaddingRow(0, kInfolistIndicatorIconPadding); |
| infolist_icon_layout->StartRow(1.0, 0); // infolist_icon_ is resizable. |
| // |infolist_icon_| is owned by |infolist_icon_wrapper|. |
| infolist_icon_layout->AddView(infolist_icon_); |
| infolist_icon_layout->AddPaddingRow(0, kInfolistIndicatorIconPadding); |
| // |infolist_icon_wrapper| is owned by |this|. |
| layout->AddView(infolist_icon_wrapper); |
| } |
| UpdateLabelBackgroundColors(); |
| } |
| |
| void CandidateView::SetCandidateText(const string16& text) { |
| candidate_label_->SetText(text); |
| } |
| |
| void CandidateView::SetShortcutText(const string16& text) { |
| shortcut_label_->SetText(text); |
| } |
| |
| void CandidateView::SetAnnotationText(const string16& text) { |
| annotation_label_->SetText(text); |
| } |
| |
| void CandidateView::SetInfolistIcon(bool enable) { |
| if (!infolist_icon_ || (infolist_icon_enabled_ == enable)) |
| return; |
| infolist_icon_enabled_ = enable; |
| infolist_icon_->set_background(enable ? |
| views::Background::CreateSolidBackground(kSelectedRowFrameColor) : NULL); |
| UpdateLabelBackgroundColors(); |
| SchedulePaint(); |
| } |
| |
| void CandidateView::Select() { |
| set_background( |
| views::Background::CreateSolidBackground(kSelectedRowBackgroundColor)); |
| set_border(views::Border::CreateSolidBorder(1, kSelectedRowFrameColor)); |
| UpdateLabelBackgroundColors(); |
| // Need to call SchedulePaint() for background and border color changes. |
| SchedulePaint(); |
| } |
| |
| void CandidateView::Unselect() { |
| set_background(NULL); |
| set_border(NULL); |
| UpdateLabelBackgroundColors(); |
| SchedulePaint(); // See comments at Select(). |
| } |
| |
| void CandidateView::SetRowEnabled(bool enabled) { |
| shortcut_label_->SetEnabled(enabled); |
| } |
| |
| gfx::Point CandidateView::GetCandidateLabelPosition() const { |
| return candidate_label_->GetMirroredPosition(); |
| } |
| |
| bool CandidateView::OnMousePressed(const ui::MouseEvent& event) { |
| // TODO(kinaba): investigate a way to delay the commit until OnMouseReleased. |
| // Mouse-down selection is a temporally workaround for crosbug.com/11423. |
| // |
| // Typical Windows/Mac input methods select candidates at the point of mouse- |
| // up event. This would be implemented in our CandidateWindow like this: |
| // 1. Return true form CandidateView::OnMousePressed, to indicate that we |
| // need to capture mouse and to receive drag/mouse-up events. |
| // 2. In response to the drag events (OnMouseDragged()), we update our |
| // selection by calling parent_candidate_window_->OOnCandidatePressed(). |
| // 3. In response to the mouse-up event (OnMouseReleased()), we commit the |
| // selection by parent_candidate_window_->CommitCandidate(). |
| // |
| // The unfortunate thing is that before the step 2 and 3... |
| // 1.1. The mouse is captured by gtk_grab_add() inside the views framework. |
| // 1.2. The render widget watches the grab via the callback function |
| // RenderWidgetHostViewGtkWidget::OnGrabNotify(), and, even though |
| // the candidate window itself does not steal focus (since it is a |
| // popup widget), the render widget explicitly regards the grab as |
| // a signal of focus-out and calls im_context_->OnFocusOut(). |
| // 1.3. It forces the input method to fully commit the composition. |
| // Hence, the composition is committed before the user do any selection. |
| // |
| // The step 1.1 is somehow unavoidable, and the step 1.2 looks like an |
| // intended behavior, though it is not pleasant for an in-process candidate |
| // window (note that grab-notify is triggered only when a window in the |
| // same application took a grab, which explains why we didn't see the issue |
| // before r72934). So, for now, we give up the mouse-up selection and use |
| // mouse-down selection, which doen't require grabbing. |
| // |
| // Moreover, there seems to be another issue when grabbing windows is hidden |
| // http://crosbug.com/11422. |
| // TODO(yusukes): investigate if we could fix Views so it always releases grab |
| // when a popup window gets hidden. http://crosbug.com/11422 |
| |
| gfx::Point location_in_candidate_window = event.location(); |
| views::View::ConvertPointToTarget(this, parent_candidate_window_, |
| &location_in_candidate_window); |
| parent_candidate_window_->OnCandidatePressed(location_in_candidate_window); |
| parent_candidate_window_->CommitCandidate(); |
| return false; |
| } |
| |
| void CandidateView::UpdateLabelBackgroundColors() { |
| SkColor color = background() ? |
| background()->get_color() : kDefaultBackgroundColor; |
| if (orientation_ != InputMethodLookupTable::kVertical) |
| shortcut_label_->SetBackgroundColor(color); |
| candidate_label_->SetBackgroundColor(color); |
| annotation_label_->SetBackgroundColor(color); |
| } |
| |
| CandidateWindowView::CandidateWindowView(views::Widget* parent_frame) |
| : selected_candidate_index_in_page_(0), |
| parent_frame_(parent_frame), |
| preedit_area_(NULL), |
| header_area_(NULL), |
| candidate_area_(NULL), |
| footer_area_(NULL), |
| previous_shortcut_column_size_(0, 0), |
| previous_candidate_column_size_(0, 0), |
| previous_annotation_column_size_(0, 0), |
| should_show_at_composition_head_(false), |
| should_show_upper_side_(false), |
| was_candidate_window_open_(false) { |
| } |
| |
| CandidateWindowView::~CandidateWindowView() { |
| } |
| |
| 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 areas. |
| preedit_area_ = new InformationTextArea(views::Label::ALIGN_LEFT, |
| kMinPreeditAreaWidth); |
| header_area_ = new InformationTextArea(views::Label::ALIGN_LEFT, 0); |
| candidate_area_ = new HidableArea; |
| candidate_area_->SetContents(new views::View); |
| footer_area_ = new InformationTextArea(views::Label::ALIGN_RIGHT, 0); |
| |
| // 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 preedit area |
| layout->StartRow(0, 0); |
| layout->AddView(preedit_area_); // |preedit_area_| is owned by |this|. |
| |
| // 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::HideAll() { |
| parent_frame_->Hide(); |
| NotifyIfCandidateWindowOpenedOrClosed(); |
| } |
| |
| void CandidateWindowView::UpdateParentArea() { |
| if (candidate_area_->IsShown() || |
| header_area_->IsShown() || |
| footer_area_->IsShown() || |
| preedit_area_->IsShown()) { |
| ResizeAndMoveParentFrame(); |
| parent_frame_->Show(); |
| } else { |
| parent_frame_->Hide(); |
| } |
| NotifyIfCandidateWindowOpenedOrClosed(); |
| } |
| |
| void CandidateWindowView::HideLookupTable() { |
| candidate_area_->Hide(); |
| UpdateParentArea(); |
| } |
| |
| void CandidateWindowView::HideAuxiliaryText() { |
| header_area_->Hide(); |
| footer_area_->Hide(); |
| UpdateParentArea(); |
| } |
| |
| void CandidateWindowView::ShowAuxiliaryText() { |
| // If candidate_area is not shown, shows auxiliary text at header_area. |
| // We expect both header_area_ and footer_area_ contain same value. |
| if (!candidate_area_->IsShown()) { |
| header_area_->Show(); |
| footer_area_->Hide(); |
| } else { |
| // If candidate_area is shown, shows auxiliary text with orientation. |
| if (lookup_table_.orientation == InputMethodLookupTable::kHorizontal) { |
| header_area_->Show(); |
| footer_area_->Hide(); |
| } else { |
| footer_area_->Show(); |
| header_area_->Hide(); |
| } |
| } |
| UpdateParentArea(); |
| } |
| |
| void CandidateWindowView::UpdateAuxiliaryText(const std::string& utf8_text) { |
| header_area_->SetText(utf8_text); |
| footer_area_->SetText(utf8_text); |
| ShowAuxiliaryText(); |
| } |
| |
| void CandidateWindowView::HidePreeditText() { |
| preedit_area_->Hide(); |
| UpdateParentArea(); |
| } |
| |
| void CandidateWindowView::ShowPreeditText() { |
| preedit_area_->Show(); |
| UpdateParentArea(); |
| } |
| |
| void CandidateWindowView::UpdatePreeditText(const std::string& utf8_text) { |
| preedit_area_->SetText(utf8_text); |
| } |
| |
| void CandidateWindowView::ShowLookupTable() { |
| if (!candidate_area_->IsShown()) |
| should_show_upper_side_ = false; |
| candidate_area_->Show(); |
| UpdateParentArea(); |
| } |
| |
| void CandidateWindowView::NotifyIfCandidateWindowOpenedOrClosed() { |
| bool is_open = IsCandidateWindowOpen(); |
| if (!was_candidate_window_open_ && is_open) { |
| FOR_EACH_OBSERVER(Observer, observers_, OnCandidateWindowOpened()); |
| } else if (was_candidate_window_open_ && !is_open) { |
| FOR_EACH_OBSERVER(Observer, observers_, OnCandidateWindowClosed()); |
| } |
| was_candidate_window_open_ = is_open; |
| } |
| |
| bool CandidateWindowView::ShouldUpdateCandidateViews( |
| const InputMethodLookupTable& old_table, |
| const InputMethodLookupTable& new_table) { |
| |
| // Check only candidate category because other fields are not used in |
| // CandidateWindowView. |
| // TODO(nona): Remove mozc_candidates(crbug.com/129403). |
| if (old_table.mozc_candidates.has_category() || |
| new_table.mozc_candidates.has_category()) { |
| if (old_table.mozc_candidates.category() != |
| new_table.mozc_candidates.category()) |
| return true; |
| } |
| |
| // 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); |
| |
| if (new_lookup_table.mozc_candidates.has_category() && |
| new_lookup_table.mozc_candidates.category() == |
| mozc::commands::SUGGESTION) { |
| should_show_at_composition_head_ = true; |
| } else { |
| should_show_at_composition_head_ = false; |
| } |
| |
| // Compute the index of the current page. |
| const int current_page_index = ComputePageIndex(new_lookup_table); |
| if (current_page_index < 0) { |
| DVLOG(1) << "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].empty()); |
| 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(string16()); |
| } 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)); |
| } |
| // Set the candidate text. |
| if (candidate_index < new_lookup_table.candidates.size() && |
| candidate_index < new_lookup_table.annotations.size()) { |
| candidate_view->SetCandidateText( |
| UTF8ToUTF16(new_lookup_table.candidates[candidate_index])); |
| candidate_view->SetAnnotationText( |
| UTF8ToUTF16(new_lookup_table.annotations[candidate_index])); |
| candidate_view->SetRowEnabled(true); |
| |
| if ((new_lookup_table.mozc_candidates.candidate_size() > |
| static_cast<int>(i)) && |
| (new_lookup_table.mozc_candidates. |
| candidate(i).has_information_id())) { |
| candidate_view->SetInfolistIcon(true); |
| } else { |
| candidate_view->SetInfolistIcon(false); |
| } |
| } else { |
| // Disable the empty row. |
| candidate_view->SetCandidateText(string16()); |
| candidate_view->SetAnnotationText(string16()); |
| candidate_view->SetRowEnabled(false); |
| candidate_view->SetInfolistIcon(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; |
| views::View* candidate_area_contents = candidate_area_->contents(); |
| |
| // Current column width. |
| gfx::Size shortcut_column_size(0, 0); |
| gfx::Size candidate_column_size(0,0); |
| gfx::Size annotation_column_size(0, 0); |
| |
| // If orientation is horizontal, don't need to compute width, |
| // because each label is left aligned. |
| if (orientation == InputMethodLookupTable::kVertical) { |
| shortcut_column_size = ComputeShortcutColumnSize(lookup_table); |
| candidate_column_size = ComputeCandidateColumnSize(lookup_table); |
| annotation_column_size = ComputeAnnotationColumnSize(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_size_ == shortcut_column_size && |
| previous_candidate_column_size_ == candidate_column_size && |
| previous_annotation_column_size_ == annotation_column_size) { |
| return; |
| } |
| |
| // Update the previous column widths. |
| previous_shortcut_column_size_ = shortcut_column_size; |
| previous_candidate_column_size_ = candidate_column_size; |
| previous_annotation_column_size_ = annotation_column_size; |
| |
| // Clear the existing candidate_views if any. |
| for (size_t i = 0; i < candidate_views_.size(); ++i) { |
| candidate_area_contents->RemoveChildView(candidate_views_[i]); |
| // Delete the view after getting out the current message loop iteration. |
| MessageLoop::current()->DeleteSoon(FROM_HERE, candidate_views_[i]); |
| } |
| candidate_views_.clear(); |
| |
| views::GridLayout* layout = new views::GridLayout(candidate_area_contents); |
| // |candidate_area_contents| owns |layout|. |
| candidate_area_contents->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()); |
| |
| // Use maximum height for all rows in candidate area. |
| const int kColumnHeight = std::max(shortcut_column_size.height(), |
| std::max(candidate_column_size.height(), |
| annotation_column_size.height())); |
| |
| // 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_size.width(), |
| candidate_column_size.width(), |
| annotation_column_size.width(), |
| kColumnHeight); |
| candidate_views_.push_back(candidate_row); |
| if (orientation == InputMethodLookupTable::kVertical) { |
| layout->StartRow(0, 0); |
| } |
| // |candidate_row| will be owned by |candidate_area_contents|. |
| layout->AddView(candidate_row, |
| 1, // Column span. |
| 1, // Row span. |
| views::GridLayout::CENTER, // Horizontal alignment. |
| views::GridLayout::CENTER, // Vertical alignment. |
| -1, // Preferred width, not specified. |
| kColumnHeight); // Preferred height. |
| } |
| |
| // 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 ResizeAndMoveParentFrame(). |
| // 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_contents); |
| } |
| |
| bool CandidateWindowView::IsCandidateWindowOpen() const { |
| return !should_show_at_composition_head_ && |
| candidate_area_->visible() && candidate_area_->IsShown(); |
| } |
| |
| void CandidateWindowView::SelectCandidateAt(int index_in_page) { |
| const int current_page_index = ComputePageIndex(lookup_table_); |
| if (current_page_index < 0) { |
| DVLOG(1) << "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::OnCandidatePressed( |
| const gfx::Point& location) { |
| for (size_t i = 0; i < candidate_views_.size(); ++i) { |
| gfx::Point converted_location = location; |
| views::View::ConvertPointToTarget(this, candidate_views_[i], |
| &converted_location); |
| if (candidate_views_[i]->HitTestPoint(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() { |
| // If rendering operation comes from mozc-engine, uses mozc specific location, |
| // otherwise lookup table is shown under the cursor. |
| const int x = should_show_at_composition_head_? |
| composition_head_location_.x() : cursor_location_.x(); |
| // To avoid lookup-table overlapping, uses maximum y-position of mozc specific |
| // location and cursor location, because mozc-engine does not consider about |
| // multi-line composition. |
| const int y = should_show_at_composition_head_? |
| std::max(composition_head_location_.y(), cursor_location_.y()) : |
| cursor_location_.y(); |
| const int height = cursor_location_.height(); |
| const int horizontal_offset = GetHorizontalOffset(); |
| |
| gfx::Rect old_bounds = parent_frame_->GetClientAreaBoundsInScreen(); |
| gfx::Rect screen_bounds = |
| gfx::Screen::GetDisplayMatching(cursor_location_).work_area(); |
| // The size. |
| gfx::Rect frame_bounds = old_bounds; |
| frame_bounds.set_size(GetPreferredSize()); |
| |
| // 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(); |
| |
| // To avoid flickering window position, the candidate window should be shown |
| // on upper side of composition string if it was shown there. |
| if (should_show_upper_side_ || bottom_overflow > 0) { |
| frame_bounds.set_y(frame_bounds.y() - height - frame_bounds.height()); |
| should_show_upper_side_ = true; |
| } |
| |
| // TODO(nona): check top_overflow here. |
| |
| // Move the window per the cursor location. |
| // SetBounds() is not cheap. Only call this when it is really changed. |
| if (frame_bounds != old_bounds) |
| 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. |
| ResizeAndMoveParentFrame(); |
| } |
| } |
| |
| 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); |
| ResizeAndMoveParentFrame(); |
| } |
| |
| |
| InfolistView::InfolistView( |
| InfolistWindowView* parent_infolist_window) |
| : parent_infolist_window_(parent_infolist_window), |
| title_label_(NULL), |
| description_label_(NULL), |
| selected_(false) { |
| } |
| |
| void InfolistView::Init() { |
| title_label_ = new views::Label; |
| title_label_->SetPosition(gfx::Point(0, 0)); |
| title_label_->SetFont(parent_infolist_window_->GetTitleFont()); |
| title_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); |
| title_label_->set_border( |
| views::Border::CreateEmptyBorder(4, 7, 2, 4)); |
| |
| description_label_ = new views::Label; |
| description_label_->SetPosition(gfx::Point(0, 0)); |
| description_label_->SetFont(parent_infolist_window_->GetDescriptionFont()); |
| description_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); |
| description_label_->SetMultiLine(true); |
| description_label_->set_border( |
| views::Border::CreateEmptyBorder(2, 17, 4, 4)); |
| AddChildView(title_label_); |
| AddChildView(description_label_); |
| UpdateLabelBackgroundColors(); |
| } |
| |
| void InfolistView::SetTitleAndDescriptionText(const string16& title, |
| const string16& description) { |
| if ((title_label_->text() == title) && |
| (description_label_->text() == description)) { |
| return; |
| } |
| |
| title_label_->SetText(title); |
| const gfx::Size title_size = title_label_->GetPreferredSize(); |
| title_label_->SetSize(title_size); |
| |
| description_label_->SetText(description); |
| description_label_->SizeToFit(200); |
| const gfx::Size description_size = description_label_->size(); |
| description_label_->SetPosition(gfx::Point(0, title_size.height())); |
| |
| title_and_description_size_ = |
| gfx::Size(200, description_size.height() + title_size.height()); |
| } |
| |
| void InfolistView::Select() { |
| if (selected_) { |
| return; |
| } |
| selected_ = true; |
| set_background(views::Background::CreateSolidBackground( |
| kSelectedInfolistRowBackgroundColor)); |
| set_border( |
| views::Border::CreateSolidBorder(1, kSelectedInfolistRowFrameColor)); |
| UpdateLabelBackgroundColors(); |
| // Need to call SchedulePaint() for background and border color changes. |
| SchedulePaint(); |
| } |
| |
| void InfolistView::Unselect() { |
| if (!selected_) { |
| return; |
| } |
| selected_ = false; |
| set_background(NULL); |
| set_border(views::Border::CreateEmptyBorder(1, 1, 1, 1)); |
| UpdateLabelBackgroundColors(); |
| SchedulePaint(); // See comments at Select(). |
| } |
| |
| void InfolistView::UpdateLabelBackgroundColors() { |
| SkColor color = background() ? |
| background()->get_color() : kDefaultBackgroundColor; |
| title_label_->SetBackgroundColor(color); |
| description_label_->SetBackgroundColor(color); |
| } |
| |
| InfolistWindowView::InfolistWindowView(views::Widget* parent_frame, |
| views::Widget* candidate_window_frame) |
| : usages_(new mozc::commands::InformationList), |
| parent_frame_(parent_frame), |
| candidate_window_frame_(candidate_window_frame), |
| infolist_area_(NULL), |
| visible_(false), |
| title_font_(new gfx::Font(kJapaneseFontName, kFontSizeDelta + 15)), |
| description_font_(new gfx::Font(kJapaneseFontName, kFontSizeDelta + 11)) { |
| } |
| |
| InfolistWindowView::~InfolistWindowView() { |
| if (infolist_area_ != NULL) { |
| infolist_area_->RemoveAllChildViews(false); |
| } |
| |
| for (size_t i = 0; i < infolist_views_.size(); ++i) { |
| delete infolist_views_[i]; |
| } |
| } |
| |
| void InfolistWindowView::Init() { |
| set_background( |
| views::Background::CreateSolidBackground(kDefaultBackgroundColor)); |
| set_border(views::Border::CreateSolidBorder(1, kFrameColor)); |
| infolist_area_ = new views::View; |
| |
| views::BoxLayout* layout = new views::BoxLayout(views::BoxLayout::kVertical, |
| 0, 0, 0); |
| SetLayoutManager(layout); // |this| owns |layout|. |
| |
| views::Label* caption_label = new views::Label; |
| caption_label->SetFont(caption_label->font().DeriveFont(kFontSizeDelta - 2)); |
| caption_label->SetHorizontalAlignment(views::Label::ALIGN_LEFT); |
| caption_label->SetText( |
| l10n_util::GetStringUTF16(IDS_INPUT_METHOD_INFOLIST_WINDOW_TITLE)); |
| caption_label->set_border( |
| views::Border::CreateEmptyBorder(2, 2, 2, 2)); |
| caption_label->set_background( |
| views::Background::CreateSolidBackground(kInfolistTitleBackgroundColor)); |
| caption_label->SetBackgroundColor( |
| caption_label->background()->get_color()); |
| |
| AddChildView(caption_label); |
| AddChildView(infolist_area_); |
| } |
| |
| void InfolistWindowView::Hide() { |
| visible_ = false; |
| show_hide_timer_.Stop(); |
| parent_frame_->Hide(); |
| } |
| |
| void InfolistWindowView::DelayHide(unsigned int milliseconds) { |
| visible_ = false; |
| show_hide_timer_.Stop(); |
| show_hide_timer_.Start(FROM_HERE, |
| base::TimeDelta::FromMilliseconds(milliseconds), |
| this, |
| &InfolistWindowView::OnShowHideTimer); |
| } |
| |
| void InfolistWindowView::Show() { |
| visible_ = true; |
| show_hide_timer_.Stop(); |
| ResizeAndMoveParentFrame(); |
| parent_frame_->Show(); |
| } |
| |
| void InfolistWindowView::DelayShow(unsigned int milliseconds) { |
| visible_ = true; |
| show_hide_timer_.Stop(); |
| show_hide_timer_.Start(FROM_HERE, |
| base::TimeDelta::FromMilliseconds(milliseconds), |
| this, |
| &InfolistWindowView::OnShowHideTimer); |
| } |
| |
| void InfolistWindowView::OnShowHideTimer() { |
| if (visible_) { |
| Show(); |
| } else { |
| Hide(); |
| } |
| } |
| |
| bool InfolistWindowView::ShouldUpdateView( |
| const mozc::commands::InformationList* old_usages, |
| const mozc::commands::InformationList* new_usages) { |
| if (old_usages->information_size() != |
| new_usages->information_size()) { |
| return true; |
| } |
| for (int i = 0; i < old_usages->information_size(); ++i) { |
| if ((old_usages->information(i).title() != |
| new_usages->information(i).title()) || |
| (old_usages->information(i).description() != |
| new_usages->information(i).description())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void InfolistWindowView::UpdateCandidates( |
| const InputMethodLookupTable& new_lookup_table) { |
| if (!new_lookup_table.mozc_candidates.has_usages()) { |
| return; |
| } |
| const mozc::commands::InformationList& new_usages = |
| new_lookup_table.mozc_candidates.usages(); |
| const bool should_update = ShouldUpdateView(usages_.get(), |
| &new_usages); |
| if (should_update) { |
| usages_->CopyFrom(new_usages); |
| for (int i = infolist_views_.size(); i < usages_->information_size(); ++i) { |
| InfolistView* infolist_row = new InfolistView(this); |
| infolist_row->Init(); |
| infolist_views_.push_back(infolist_row); |
| } |
| infolist_area_->RemoveAllChildViews(false); |
| |
| views::BoxLayout* layout = new views::BoxLayout(views::BoxLayout::kVertical, |
| 0, 0, 0); |
| // |infolist_area_| owns |layout|. |
| infolist_area_->SetLayoutManager(layout); |
| |
| for (int i = 0; i < usages_->information_size(); ++i) { |
| InfolistView* infolist_row = infolist_views_[i]; |
| infolist_row->SetTitleAndDescriptionText( |
| UTF8ToUTF16(usages_->information(i).title()), |
| UTF8ToUTF16(usages_->information(i).description())); |
| infolist_row->Unselect(); |
| infolist_area_->AddChildView(infolist_row); |
| } |
| } |
| |
| for (int i = 0; i < new_usages.information_size(); ++i) { |
| InfolistView* infolist_row = infolist_views_[i]; |
| if (new_usages.has_focused_index() && |
| (static_cast<int>(new_usages.focused_index()) == i)) { |
| infolist_row->Select(); |
| } else { |
| infolist_row->Unselect(); |
| } |
| } |
| } |
| |
| void InfolistWindowView::ResizeAndMoveParentFrame() { |
| int x, y; |
| gfx::Rect old_bounds = parent_frame_->GetClientAreaBoundsInScreen(); |
| gfx::Rect screen_bounds = gfx::Screen::GetDisplayNearestWindow( |
| parent_frame_->GetNativeView()).work_area(); |
| // The size. |
| gfx::Rect frame_bounds = old_bounds; |
| gfx::Size size = GetPreferredSize(); |
| frame_bounds.set_size(size); |
| |
| gfx::Rect candidatewindow_bounds; |
| if (candidate_window_frame_ != NULL) { |
| candidatewindow_bounds = |
| candidate_window_frame_->GetClientAreaBoundsInScreen(); |
| } |
| |
| if (screen_bounds.height() == 0 || screen_bounds.width() == 0) { |
| x = candidatewindow_bounds.right(); |
| y = candidatewindow_bounds.y(); |
| } |
| if (candidatewindow_bounds.right() + frame_bounds.width() > |
| screen_bounds.right()) { |
| x = candidatewindow_bounds.x() - frame_bounds.width(); |
| } else { |
| x = candidatewindow_bounds.right(); |
| } |
| if (candidatewindow_bounds.y() + frame_bounds.height() > |
| screen_bounds.bottom()) { |
| y = screen_bounds.bottom() - frame_bounds.height(); |
| } else { |
| y = candidatewindow_bounds.y(); |
| } |
| |
| frame_bounds.set_x(x); |
| frame_bounds.set_y(y); |
| |
| // Move the window per the cursor location. |
| // SetBounds() is not cheap. Only call this when it is really changed. |
| if (frame_bounds != old_bounds) |
| parent_frame_->SetBounds(frame_bounds); |
| } |
| |
| void InfolistWindowView::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. |
| ResizeAndMoveParentFrame(); |
| } |
| } |
| |
| void InfolistWindowView::OnBoundsChanged(const gfx::Rect& previous_bounds) { |
| View::OnBoundsChanged(previous_bounds); |
| ResizeAndMoveParentFrame(); |
| } |
| |
| gfx::Font InfolistWindowView::GetTitleFont() const { |
| return *title_font_.get(); |
| } |
| |
| gfx::Font InfolistWindowView::GetDescriptionFont() const { |
| return *description_font_.get(); |
| } |
| |
| bool CandidateWindowControllerImpl::Init() { |
| // Create the candidate window view. |
| CreateView(); |
| |
| // The observer should be added before Connect() so we can capture the |
| // initial connection change. |
| ibus_ui_controller_->AddObserver(this); |
| ibus_ui_controller_->Connect(); |
| return true; |
| } |
| |
| void CandidateWindowControllerImpl::CreateView() { |
| // Create a non-decorated frame. |
| frame_.reset(new views::Widget); |
| // The size is initially zero. |
| views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); |
| // |frame_| and |infolist_frame_| are owned by controller impl so |
| // they should use WIDGET_OWNS_NATIVE_WIDGET ownership. |
| params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| // Show the candidate window always on top |
| #if defined(USE_ASH) |
| params.parent = ash::Shell::GetContainer( |
| ash::Shell::GetActiveRootWindow(), |
| ash::internal::kShellWindowId_InputMethodContainer); |
| #else |
| params.keep_on_top = true; |
| #endif |
| frame_->Init(params); |
| #if defined(USE_ASH) |
| ash::SetWindowVisibilityAnimationType( |
| frame_->GetNativeView(), |
| ash::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE); |
| #endif // USE_ASH |
| |
| // Create the candidate window. |
| candidate_window_ = new CandidateWindowView(frame_.get()); |
| candidate_window_->Init(); |
| candidate_window_->AddObserver(this); |
| |
| frame_->SetContentsView(candidate_window_); |
| |
| |
| // Create the infolist window. |
| infolist_frame_.reset(new views::Widget); |
| infolist_frame_->Init(params); |
| #if defined(USE_ASH) |
| ash::SetWindowVisibilityAnimationType( |
| infolist_frame_->GetNativeView(), |
| ash::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE); |
| #endif // USE_ASH |
| |
| infolist_window_ = new InfolistWindowView( |
| infolist_frame_.get(), frame_.get()); |
| infolist_window_->Init(); |
| infolist_frame_->SetContentsView(infolist_window_); |
| } |
| |
| CandidateWindowControllerImpl::CandidateWindowControllerImpl() |
| : ibus_ui_controller_(IBusUiController::Create()), |
| candidate_window_(NULL), |
| infolist_window_(NULL) { |
| } |
| |
| CandidateWindowControllerImpl::~CandidateWindowControllerImpl() { |
| ibus_ui_controller_->RemoveObserver(this); |
| candidate_window_->RemoveObserver(this); |
| // ibus_ui_controller_'s destructor will close the connection. |
| } |
| |
| void CandidateWindowControllerImpl::OnHideAuxiliaryText() { |
| candidate_window_->HideAuxiliaryText(); |
| } |
| |
| void CandidateWindowControllerImpl::OnHideLookupTable() { |
| candidate_window_->HideLookupTable(); |
| infolist_window_->Hide(); |
| } |
| |
| void CandidateWindowControllerImpl::OnHidePreeditText() { |
| candidate_window_->HidePreeditText(); |
| } |
| |
| void CandidateWindowControllerImpl::OnSetCursorLocation( |
| const gfx::Rect& cursor_location, |
| const gfx::Rect& composition_head) { |
| // 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 = |
| candidate_window_->cursor_location(); |
| const int delta_y = abs(last_location.y() - cursor_location.y()); |
| if ((last_location.x() == cursor_location.x()) && |
| (delta_y <= kKeepPositionThreshold)) { |
| DVLOG(1) << "Ignored set_cursor_location signal to prevent window shake"; |
| return; |
| } |
| |
| // Remember the cursor location. |
| candidate_window_->set_cursor_location(cursor_location); |
| candidate_window_->set_composition_head_location(composition_head); |
| // Move the window per the cursor location. |
| candidate_window_->ResizeAndMoveParentFrame(); |
| infolist_window_->ResizeAndMoveParentFrame(); |
| } |
| |
| void CandidateWindowControllerImpl::OnUpdateAuxiliaryText( |
| const std::string& utf8_text, |
| bool visible) { |
| // If it's not visible, hide the auxiliary text and return. |
| if (!visible) { |
| candidate_window_->HideAuxiliaryText(); |
| return; |
| } |
| candidate_window_->UpdateAuxiliaryText(utf8_text); |
| candidate_window_->ShowAuxiliaryText(); |
| } |
| |
| void CandidateWindowControllerImpl::OnUpdateLookupTable( |
| const InputMethodLookupTable& lookup_table) { |
| // If it's not visible, hide the lookup table and return. |
| if (!lookup_table.visible) { |
| candidate_window_->HideLookupTable(); |
| infolist_window_->Hide(); |
| return; |
| } |
| |
| candidate_window_->UpdateCandidates(lookup_table); |
| candidate_window_->ShowLookupTable(); |
| |
| const mozc::commands::Candidates& candidates = lookup_table.mozc_candidates; |
| |
| if (lookup_table.mozc_candidates.has_usages() && |
| lookup_table.mozc_candidates.usages().information_size() > 0) { |
| infolist_window_->UpdateCandidates(lookup_table); |
| infolist_window_->ResizeAndMoveParentFrame(); |
| if (candidates.has_focused_index() && candidates.candidate_size() > 0) { |
| const int focused_row = |
| candidates.focused_index() - candidates.candidate(0).index(); |
| if (candidates.candidate_size() >= focused_row && |
| candidates.candidate(focused_row).has_information_id()) { |
| infolist_window_->DelayShow(kInfolistShowDelayMilliSeconds); |
| } else { |
| infolist_window_->DelayHide(kInfolistHideDelayMilliSeconds); |
| } |
| } else { |
| infolist_window_->DelayHide(kInfolistHideDelayMilliSeconds); |
| } |
| } else { |
| infolist_window_->Hide(); |
| } |
| } |
| |
| void CandidateWindowControllerImpl::OnUpdatePreeditText( |
| const std::string& utf8_text, unsigned int cursor, bool visible) { |
| // If it's not visible, hide the preedit text and return. |
| if (!visible || utf8_text.empty()) { |
| candidate_window_->HidePreeditText(); |
| return; |
| } |
| candidate_window_->UpdatePreeditText(utf8_text); |
| candidate_window_->ShowPreeditText(); |
| } |
| |
| void CandidateWindowControllerImpl::OnCandidateCommitted(int index, |
| int button, |
| int flags) { |
| ibus_ui_controller_->NotifyCandidateClicked(index, button, flags); |
| } |
| |
| void CandidateWindowControllerImpl::OnCandidateWindowOpened() { |
| FOR_EACH_OBSERVER(CandidateWindowController::Observer, observers_, |
| CandidateWindowOpened()); |
| } |
| |
| void CandidateWindowControllerImpl::OnCandidateWindowClosed() { |
| FOR_EACH_OBSERVER(CandidateWindowController::Observer, observers_, |
| CandidateWindowClosed()); |
| } |
| |
| void CandidateWindowControllerImpl::AddObserver( |
| CandidateWindowController::Observer* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void CandidateWindowControllerImpl::RemoveObserver( |
| CandidateWindowController::Observer* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void CandidateWindowControllerImpl::OnConnectionChange(bool connected) { |
| if (!connected) { |
| candidate_window_->HideAll(); |
| infolist_window_->Hide(); |
| } |
| } |
| |
| // static |
| CandidateWindowController* |
| CandidateWindowController::CreateCandidateWindowController() { |
| return new CandidateWindowControllerImpl; |
| } |
| |
| } // namespace input_method |
| } // namespace chromeos |