| // 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_view.h" |
| |
| #include <string> |
| |
| #include "ash/shell.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/chromeos/input_method/candidate_view.h" |
| #include "chrome/browser/chromeos/input_method/candidate_window_constants.h" |
| #include "chrome/browser/chromeos/input_method/hidable_area.h" |
| #include "chromeos/ime/candidate_window.h" |
| #include "ui/gfx/color_utils.h" |
| #include "ui/native_theme/native_theme.h" |
| #include "ui/views/background.h" |
| #include "ui/views/border.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/layout/grid_layout.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace chromeos { |
| namespace input_method { |
| |
| namespace { |
| // 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() OVERRIDE { |
| 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(size_t index, |
| const CandidateWindow& candidate_window) { |
| if (index >= candidate_window.candidates().size()) |
| return UTF8ToUTF16(""); |
| std::string shortcut_text = candidate_window.candidates()[index].label; |
| if (!shortcut_text.empty() && |
| candidate_window.orientation() != CandidateWindow::VERTICAL) |
| 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( |
| CandidateWindow::Orientation orientation, const ui::NativeTheme& theme) { |
| // 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 == CandidateWindow::VERTICAL) { |
| 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(theme.GetSystemColor( |
| ui::NativeTheme::kColorId_LabelEnabledColor)); |
| shortcut_label->SetDisabledColor(theme.GetSystemColor( |
| ui::NativeTheme::kColorId_LabelDisabledColor)); |
| |
| 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, |
| CandidateWindow::Orientation orientation, |
| const ui::NativeTheme& theme) { |
| // 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 == CandidateWindow::VERTICAL ? |
| kVerticalShortcutLabelInsets : |
| kHorizontalShortcutLabelInsets); |
| views::View* wrapped_shortcut_label = |
| WrapWithPadding(shortcut_label, insets); |
| |
| // Add decoration based on the orientation. |
| if (orientation == CandidateWindow::VERTICAL) { |
| // Set the background color. |
| SkColor blackish = color_utils::AlphaBlend( |
| SK_ColorBLACK, |
| theme.GetSystemColor(ui::NativeTheme::kColorId_WindowBackground), |
| 0x40); |
| SkColor transparent_blakish = color_utils::AlphaBlend( |
| SK_ColorTRANSPARENT, blackish, 0xE0); |
| wrapped_shortcut_label->set_background( |
| views::Background::CreateSolidBackground(transparent_blakish)); |
| 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( |
| CandidateWindow::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 == CandidateWindow::VERTICAL) { |
| 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(gfx::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( |
| CandidateWindow::Orientation orientation, const ui::NativeTheme& theme) { |
| // 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(theme.GetSystemColor( |
| ui::NativeTheme::kColorId_LabelDisabledColor)); |
| annotation_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| |
| return annotation_label; |
| } |
| |
| // Computes shortcut column size. |
| gfx::Size ComputeShortcutColumnSize( |
| const CandidateWindow& candidate_window, |
| const ui::NativeTheme& theme) { |
| 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( |
| candidate_window.orientation(), theme); |
| scoped_ptr<views::View> wrapped_shortcut_label( |
| CreateWrappedShortcutLabel(shortcut_label, |
| candidate_window.orientation(), |
| theme)); |
| |
| // Compute the max width and height in shortcut labels. |
| // We'll create temporary shortcut labels, and choose the largest width and |
| // height. |
| for (size_t i = 0; i < candidate_window.page_size(); ++i) { |
| shortcut_label->SetText(CreateShortcutText(i, candidate_window)); |
| 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 CandidateWindow& candidate_window) { |
| if (candidate_window.page_size() > 0) |
| return candidate_window.cursor_position() / candidate_window.page_size(); |
| return -1; |
| } |
| |
| // Computes candidate column size. |
| gfx::Size ComputeCandidateColumnSize( |
| const CandidateWindow& candidate_window) { |
| int candidate_column_width = 0; |
| int candidate_column_height = 0; |
| scoped_ptr<views::Label> candidate_label( |
| CreateCandidateLabel(candidate_window.orientation())); |
| |
| // Compute the start index of |candidate_window_|. |
| const int current_page_index = ComputePageIndex(candidate_window); |
| if (current_page_index < 0) |
| return gfx::Size(0, 0); |
| const size_t start_from = current_page_index * candidate_window.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 < candidate_window.candidates().size(); |
| ++i) { |
| const size_t index = start_from + i; |
| |
| candidate_label->SetText( |
| UTF8ToUTF16(candidate_window.candidates()[index].value)); |
| 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 CandidateWindow& candidate_window, const ui::NativeTheme& theme) { |
| int annotation_column_width = 0; |
| int annotation_column_height = 0; |
| scoped_ptr<views::Label> annotation_label( |
| CreateAnnotationLabel(candidate_window.orientation(), theme)); |
| |
| // Compute the start index of |candidate_window_|. |
| const int current_page_index = ComputePageIndex(candidate_window); |
| if (current_page_index < 0) |
| return gfx::Size(0, 0); |
| const size_t start_from = current_page_index * candidate_window.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 < candidate_window.candidates().size(); |
| ++i) { |
| const size_t index = start_from + i; |
| |
| annotation_label->SetText( |
| UTF8ToUTF16(candidate_window.candidates()[index].annotation)); |
| 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 |
| |
| // InformationTextArea is a HidableArea having a single Label in it. |
| class InformationTextArea : public HidableArea { |
| public: |
| // Specify the alignment and initialize the control. |
| InformationTextArea(gfx::HorizontalAlignment 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, |
| GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_MenuBorderColor))); |
| contents->set_background(views::Background::CreateSolidBackground( |
| color_utils::AlphaBlend(SK_ColorBLACK, |
| GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_WindowBackground), |
| 0x10))); |
| 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); |
| }; |
| |
| CandidateView::CandidateView( |
| CandidateWindowView* parent_candidate_window, |
| int index_in_page, |
| CandidateWindow::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. |
| const ui::NativeTheme& theme = *GetNativeTheme(); |
| shortcut_label_ = CreateShortcutLabel(orientation_, theme); |
| views::View* wrapped_shortcut_label = |
| CreateWrappedShortcutLabel(shortcut_label_, orientation_, theme); |
| candidate_label_ = CreateCandidateLabel(orientation_); |
| annotation_label_ = CreateAnnotationLabel(orientation_, theme); |
| |
| // 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_ == CandidateWindow::VERTICAL ? |
| views::GridLayout::FIXED : views::GridLayout::USE_PREF; |
| |
| const int padding_column_width = |
| orientation_ == CandidateWindow::VERTICAL ? 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, |
| 1, views::GridLayout::USE_PREF, 0, |
| orientation_ == CandidateWindow::VERTICAL ? |
| 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_ == CandidateWindow::VERTICAL) { |
| 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_ == CandidateWindow::VERTICAL) { |
| 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 base::string16& text) { |
| candidate_label_->SetText(text); |
| } |
| |
| void CandidateView::SetShortcutText(const base::string16& text) { |
| shortcut_label_->SetText(text); |
| } |
| |
| void CandidateView::SetAnnotationText(const base::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(GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_FocusedBorderColor)) : |
| NULL); |
| UpdateLabelBackgroundColors(); |
| SchedulePaint(); |
| } |
| |
| void CandidateView::Select() { |
| set_background( |
| views::Background::CreateSolidBackground(GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_TextfieldSelectionBackgroundFocused))); |
| set_border(views::Border::CreateSolidBorder( |
| 1, GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_FocusedBorderColor))); |
| 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): On Windows and MacOS, candidate windows typically commits a |
| // candidate at OnMouseReleased event. We have chosen OnMousePressed here for |
| // working around several obstacle rising from views implementation over GTK. |
| // See: http://crosbug.com/11423#c11. Since we have moved from GTK to Aura, |
| // the reasoning should have became obsolete. We might want to reconsider |
| // implementing mouse-up selection. |
| SelectCandidateAt(event.location()); |
| return false; |
| } |
| |
| void CandidateView::OnGestureEvent(ui::GestureEvent* event) { |
| if (event->type() == ui::ET_GESTURE_TAP) { |
| SelectCandidateAt(event->location()); |
| event->SetHandled(); |
| return; |
| } |
| View::OnGestureEvent(event); |
| } |
| |
| void CandidateView::SelectCandidateAt(const gfx::Point& location) { |
| gfx::Point location_in_candidate_window = location; |
| views::View::ConvertPointToTarget(this, parent_candidate_window_, |
| &location_in_candidate_window); |
| parent_candidate_window_->OnCandidatePressed(location_in_candidate_window); |
| parent_candidate_window_->CommitCandidate(); |
| } |
| |
| void CandidateView::UpdateLabelBackgroundColors() { |
| SkColor color = background() ? |
| background()->get_color() : |
| GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_WindowBackground); |
| if (orientation_ != CandidateWindow::VERTICAL) |
| shortcut_label_->SetBackgroundColor(color); |
| candidate_label_->SetBackgroundColor(color); |
| annotation_label_->SetBackgroundColor(color); |
| } |
| |
| CandidateWindowView::CandidateWindowView(views::Widget* parent_frame) |
| : selected_candidate_index_in_page_(-1), |
| 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(GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_WindowBackground))); |
| set_border(views::Border::CreateSolidBorder( |
| 1, GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_MenuBorderColor))); |
| |
| // Create areas. |
| preedit_area_ = new InformationTextArea(gfx::ALIGN_LEFT, |
| kMinPreeditAreaWidth); |
| header_area_ = new InformationTextArea(gfx::ALIGN_LEFT, 0); |
| candidate_area_ = new HidableArea; |
| candidate_area_->SetContents(new views::View); |
| footer_area_ = new InformationTextArea(gfx::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 (candidate_window_.orientation() == CandidateWindow::HORIZONTAL) { |
| 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 CandidateWindow& old_candidate_window, |
| const CandidateWindow& new_candidate_window) { |
| return !old_candidate_window.IsEqual(new_candidate_window); |
| } |
| |
| void CandidateWindowView::UpdateCandidates( |
| const CandidateWindow& new_candidate_window) { |
| const bool should_update = ShouldUpdateCandidateViews(candidate_window_, |
| new_candidate_window); |
| // Updating the candidate views is expensive. We'll skip this if possible. |
| if (should_update) { |
| // Initialize candidate views if necessary. |
| MaybeInitializeCandidateViews(new_candidate_window); |
| |
| should_show_at_composition_head_ |
| = new_candidate_window.show_window_at_composition(); |
| // Compute the index of the current page. |
| const int current_page_index = ComputePageIndex(new_candidate_window); |
| if (current_page_index < 0) { |
| return; |
| } |
| |
| // Update the candidates in the current page. |
| const size_t start_from = |
| current_page_index * new_candidate_window.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. |
| bool no_shortcut_mode = true; |
| for (size_t i = 0; i < new_candidate_window.candidates().size(); ++i) { |
| if (!new_candidate_window.candidates()[i].label.empty()) { |
| no_shortcut_mode = false; |
| break; |
| } |
| } |
| |
| 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_candidate_window)); |
| } |
| // Set the candidate text. |
| if (candidate_index < new_candidate_window.candidates().size()) { |
| const CandidateWindow::Entry& entry = |
| new_candidate_window.candidates()[candidate_index]; |
| candidate_view->SetCandidateText(UTF8ToUTF16(entry.value)); |
| candidate_view->SetAnnotationText(UTF8ToUTF16(entry.annotation)); |
| candidate_view->SetRowEnabled(true); |
| candidate_view->SetInfolistIcon(!entry.description_title.empty()); |
| } else { |
| // Disable the empty row. |
| candidate_view->SetCandidateText(string16()); |
| candidate_view->SetAnnotationText(string16()); |
| candidate_view->SetRowEnabled(false); |
| candidate_view->SetInfolistIcon(false); |
| } |
| } |
| } |
| // Update the current candidate window. We'll use candidate_window_ from here. |
| // Note that SelectCandidateAt() uses candidate_window_. |
| candidate_window_.CopyFrom(new_candidate_window); |
| |
| // Select the current candidate in the page. |
| if (candidate_window_.is_cursor_visible()) { |
| if (candidate_window_.page_size()) { |
| const int current_candidate_in_page = |
| candidate_window_.cursor_position() % candidate_window_.page_size(); |
| SelectCandidateAt(current_candidate_in_page); |
| } |
| } else { |
| // Unselect the currently selected candidate. |
| if (0 <= selected_candidate_index_in_page_ && |
| static_cast<size_t>(selected_candidate_index_in_page_) < |
| candidate_views_.size()) { |
| candidate_views_[selected_candidate_index_in_page_]->Unselect(); |
| selected_candidate_index_in_page_ = -1; |
| } |
| } |
| } |
| |
| void CandidateWindowView::MaybeInitializeCandidateViews( |
| const CandidateWindow& candidate_window) { |
| const CandidateWindow::Orientation orientation = |
| candidate_window.orientation(); |
| const int page_size = candidate_window.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 == CandidateWindow::VERTICAL) { |
| const ui::NativeTheme& theme = *GetNativeTheme(); |
| shortcut_column_size = ComputeShortcutColumnSize(candidate_window, theme); |
| candidate_column_size = ComputeCandidateColumnSize(candidate_window); |
| annotation_column_size = ComputeAnnotationColumnSize(candidate_window, |
| theme); |
| } |
| |
| // 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 && |
| candidate_window_.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. |
| base::MessageLoop::current()->DeleteSoon(FROM_HERE, candidate_views_[i]); |
| } |
| candidate_views_.clear(); |
| selected_candidate_index_in_page_ = -1; // Invalidates the index. |
| |
| 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 == CandidateWindow::VERTICAL) { |
| column_set->AddColumn(views::GridLayout::FILL, |
| views::GridLayout::FILL, |
| 1, 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 == CandidateWindow::HORIZONTAL) { |
| 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 == CandidateWindow::VERTICAL) { |
| layout->StartRow(0, 0); |
| } |
| // |candidate_row| will be owned by |candidate_area_contents|. |
| layout->AddView(candidate_row, |
| 1, // Column span. |
| 1, // Row span. |
| // Horizontal alignment. |
| orientation == CandidateWindow::VERTICAL ? |
| views::GridLayout::FILL : views::GridLayout::CENTER, |
| 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(candidate_window_); |
| if (current_page_index < 0) { |
| return; |
| } |
| |
| const int cursor_absolute_index = |
| candidate_window_.page_size() * current_page_index + index_in_page; |
| // Ignore click on out of range views. |
| if (cursor_absolute_index < 0 || |
| candidate_window_.candidates().size() <= |
| static_cast<size_t>(cursor_absolute_index)) { |
| return; |
| } |
| |
| // Unselect the currently selected candidate. |
| if (0 <= selected_candidate_index_in_page_ && |
| static_cast<size_t>(selected_candidate_index_in_page_) < |
| candidate_views_.size()) { |
| 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. |
| candidate_window_.set_cursor_position(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() { |
| if (!(0 <= selected_candidate_index_in_page_ && |
| static_cast<size_t>(selected_candidate_index_in_page_) < |
| candidate_views_.size())) { |
| return; // Out of range, do nothing. |
| } |
| |
| FOR_EACH_OBSERVER(Observer, observers_, |
| OnCandidateCommitted(selected_candidate_index_in_page_)); |
| } |
| |
| void CandidateWindowView::ResizeAndMoveParentFrame() { |
| // If rendering operation comes from mozc-engine, uses mozc specific bounds, |
| // otherwise candidate window is shown under the cursor. |
| const int x = should_show_at_composition_head_? |
| composition_head_bounds_.x() : cursor_bounds_.x(); |
| // To avoid candidate-window overlapping, uses maximum y-position of mozc |
| // specific bounds and cursor bounds, because mozc-engine does not |
| // consider about multi-line composition. |
| const int y = should_show_at_composition_head_? |
| std::max(composition_head_bounds_.y(), cursor_bounds_.y()) : |
| cursor_bounds_.y(); |
| const int height = cursor_bounds_.height(); |
| const int horizontal_offset = GetHorizontalOffset(); |
| |
| gfx::Rect old_bounds = parent_frame_->GetClientAreaBoundsInScreen(); |
| gfx::Rect screen_bounds = ash::Shell::GetScreen()->GetDisplayMatching( |
| cursor_bounds_).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 bounds. |
| // 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 candidate window is vertical. |
| if (!candidate_views_.empty() && |
| candidate_window_.orientation() == CandidateWindow::VERTICAL) { |
| 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(); |
| } |
| |
| } // namespace input_method |
| } // namespace chromeos |