| // Copyright 2014 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 "ash/ime/candidate_window_view.h" |
| |
| #include <string> |
| |
| #include "ash/ime/candidate_view.h" |
| #include "ash/ime/candidate_window_constants.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "ui/gfx/color_utils.h" |
| #include "ui/gfx/screen.h" |
| #include "ui/native_theme/native_theme.h" |
| #include "ui/views/background.h" |
| #include "ui/views/border.h" |
| #include "ui/views/bubble/bubble_frame_view.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/wm/core/window_animations.h" |
| |
| namespace ash { |
| namespace ime { |
| |
| namespace { |
| |
| class CandidateWindowBorder : public views::BubbleBorder { |
| public: |
| explicit CandidateWindowBorder(gfx::NativeView parent) |
| : views::BubbleBorder(views::BubbleBorder::TOP_CENTER, |
| views::BubbleBorder::NO_SHADOW, |
| SK_ColorTRANSPARENT), |
| parent_(parent), |
| offset_(0) { |
| set_paint_arrow(views::BubbleBorder::PAINT_NONE); |
| } |
| virtual ~CandidateWindowBorder() {} |
| |
| void set_offset(int offset) { offset_ = offset; } |
| |
| private: |
| // Overridden from views::BubbleBorder: |
| virtual gfx::Rect GetBounds(const gfx::Rect& anchor_rect, |
| const gfx::Size& content_size) const OVERRIDE { |
| gfx::Rect bounds(content_size); |
| bounds.set_origin(gfx::Point( |
| anchor_rect.x() - offset_, |
| is_arrow_on_top(arrow()) ? |
| anchor_rect.bottom() : anchor_rect.y() - content_size.height())); |
| |
| // It cannot use the normal logic of arrow offset for horizontal offscreen, |
| // because the arrow must be in the content's edge. But CandidateWindow has |
| // to be visible even when |anchor_rect| is out of the screen. |
| gfx::Rect work_area = gfx::Screen::GetNativeScreen()-> |
| GetDisplayNearestWindow(parent_).work_area(); |
| if (bounds.right() > work_area.right()) |
| bounds.set_x(work_area.right() - bounds.width()); |
| if (bounds.x() < work_area.x()) |
| bounds.set_x(work_area.x()); |
| |
| return bounds; |
| } |
| |
| virtual gfx::Insets GetInsets() const OVERRIDE { |
| return gfx::Insets(); |
| } |
| |
| gfx::NativeView parent_; |
| int offset_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CandidateWindowBorder); |
| }; |
| |
| // 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 ui::CandidateWindow& candidate_window) { |
| if (candidate_window.page_size() > 0) |
| return candidate_window.cursor_position() / candidate_window.page_size(); |
| return -1; |
| } |
| |
| } // namespace |
| |
| class InformationTextArea : public views::View { |
| public: |
| // InformationTextArea's border is drawn as a separator, it should appear |
| // at either top or bottom. |
| enum BorderPosition { |
| TOP, |
| BOTTOM |
| }; |
| |
| // Specify the alignment and initialize the control. |
| InformationTextArea(gfx::HorizontalAlignment align, int min_width) |
| : min_width_(min_width) { |
| label_ = new views::Label; |
| label_->SetHorizontalAlignment(align); |
| label_->SetBorder(views::Border::CreateEmptyBorder(2, 2, 2, 4)); |
| |
| SetLayoutManager(new views::FillLayout()); |
| AddChildView(label_); |
| set_background(views::Background::CreateSolidBackground( |
| color_utils::AlphaBlend(SK_ColorBLACK, |
| GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_WindowBackground), |
| 0x10))); |
| } |
| |
| // Sets the text alignment. |
| void SetAlignment(gfx::HorizontalAlignment alignment) { |
| label_->SetHorizontalAlignment(alignment); |
| } |
| |
| // Sets the displayed text. |
| void SetText(const base::string16& text) { |
| label_->SetText(text); |
| } |
| |
| // Sets the border thickness for top/bottom. |
| void SetBorderFromPosition(BorderPosition position) { |
| SetBorder(views::Border::CreateSolidSidedBorder( |
| (position == TOP) ? 1 : 0, |
| 0, |
| (position == BOTTOM) ? 1 : 0, |
| 0, |
| GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_MenuBorderColor))); |
| } |
| |
| protected: |
| virtual gfx::Size GetPreferredSize() const OVERRIDE { |
| gfx::Size size = views::View::GetPreferredSize(); |
| size.SetToMax(gfx::Size(min_width_, 0)); |
| return size; |
| } |
| |
| private: |
| views::Label* label_; |
| int min_width_; |
| |
| DISALLOW_COPY_AND_ASSIGN(InformationTextArea); |
| }; |
| |
| CandidateWindowView::CandidateWindowView(gfx::NativeView parent) |
| : selected_candidate_index_in_page_(-1), |
| should_show_at_composition_head_(false), |
| should_show_upper_side_(false), |
| was_candidate_window_open_(false) { |
| set_use_focusless(true); |
| set_parent_window(parent); |
| set_margins(gfx::Insets()); |
| |
| // Set the background and the border of the view. |
| ui::NativeTheme* theme = GetNativeTheme(); |
| set_background( |
| views::Background::CreateSolidBackground(theme->GetSystemColor( |
| ui::NativeTheme::kColorId_WindowBackground))); |
| SetBorder(views::Border::CreateSolidBorder( |
| 1, theme->GetSystemColor(ui::NativeTheme::kColorId_MenuBorderColor))); |
| |
| SetLayoutManager(new views::BoxLayout(views::BoxLayout::kVertical, 0, 0, 0)); |
| auxiliary_text_ = new InformationTextArea(gfx::ALIGN_RIGHT, 0); |
| preedit_ = new InformationTextArea(gfx::ALIGN_LEFT, kMinPreeditAreaWidth); |
| candidate_area_ = new views::View; |
| auxiliary_text_->SetVisible(false); |
| preedit_->SetVisible(false); |
| candidate_area_->SetVisible(false); |
| preedit_->SetBorderFromPosition(InformationTextArea::BOTTOM); |
| if (candidate_window_.orientation() == ui::CandidateWindow::VERTICAL) { |
| AddChildView(preedit_); |
| AddChildView(candidate_area_); |
| AddChildView(auxiliary_text_); |
| auxiliary_text_->SetBorderFromPosition(InformationTextArea::TOP); |
| candidate_area_->SetLayoutManager(new views::BoxLayout( |
| views::BoxLayout::kVertical, 0, 0, 0)); |
| } else { |
| AddChildView(preedit_); |
| AddChildView(auxiliary_text_); |
| AddChildView(candidate_area_); |
| auxiliary_text_->SetAlignment(gfx::ALIGN_LEFT); |
| auxiliary_text_->SetBorderFromPosition(InformationTextArea::BOTTOM); |
| candidate_area_->SetLayoutManager(new views::BoxLayout( |
| views::BoxLayout::kHorizontal, 0, 0, 0)); |
| } |
| } |
| |
| CandidateWindowView::~CandidateWindowView() { |
| } |
| |
| views::Widget* CandidateWindowView::InitWidget() { |
| views::Widget* widget = BubbleDelegateView::CreateBubble(this); |
| |
| wm::SetWindowVisibilityAnimationType( |
| widget->GetNativeView(), |
| wm::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE); |
| |
| GetBubbleFrameView()->SetBubbleBorder(scoped_ptr<views::BubbleBorder>( |
| new CandidateWindowBorder(parent_window()))); |
| return widget; |
| } |
| |
| void CandidateWindowView::UpdateVisibility() { |
| if (candidate_area_->visible() || auxiliary_text_->visible() || |
| preedit_->visible()) { |
| SizeToContents(); |
| } else { |
| GetWidget()->Close(); |
| } |
| } |
| |
| void CandidateWindowView::HideLookupTable() { |
| candidate_area_->SetVisible(false); |
| auxiliary_text_->SetVisible(false); |
| UpdateVisibility(); |
| } |
| |
| void CandidateWindowView::HidePreeditText() { |
| preedit_->SetVisible(false); |
| UpdateVisibility(); |
| } |
| |
| void CandidateWindowView::ShowPreeditText() { |
| preedit_->SetVisible(true); |
| UpdateVisibility(); |
| } |
| |
| void CandidateWindowView::UpdatePreeditText(const base::string16& text) { |
| preedit_->SetText(text); |
| } |
| |
| void CandidateWindowView::ShowLookupTable() { |
| candidate_area_->SetVisible(true); |
| auxiliary_text_->SetVisible(candidate_window_.is_auxiliary_text_visible()); |
| UpdateVisibility(); |
| } |
| |
| void CandidateWindowView::UpdateCandidates( |
| const ui::CandidateWindow& new_candidate_window) { |
| // Updating the candidate views is expensive. We'll skip this if possible. |
| if (!candidate_window_.IsEqual(new_candidate_window)) { |
| if (candidate_window_.orientation() != new_candidate_window.orientation()) { |
| // If the new layout is vertical, the aux text should appear at the |
| // bottom. If horizontal, it should appear between preedit and candidates. |
| if (new_candidate_window.orientation() == ui::CandidateWindow::VERTICAL) { |
| ReorderChildView(auxiliary_text_, -1); |
| auxiliary_text_->SetAlignment(gfx::ALIGN_RIGHT); |
| auxiliary_text_->SetBorderFromPosition(InformationTextArea::TOP); |
| candidate_area_->SetLayoutManager(new views::BoxLayout( |
| views::BoxLayout::kVertical, 0, 0, 0)); |
| } else { |
| ReorderChildView(auxiliary_text_, 1); |
| auxiliary_text_->SetAlignment(gfx::ALIGN_LEFT); |
| auxiliary_text_->SetBorderFromPosition(InformationTextArea::BOTTOM); |
| candidate_area_->SetLayoutManager(new views::BoxLayout( |
| views::BoxLayout::kHorizontal, 0, 0, 0)); |
| } |
| } |
| |
| // 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(); |
| |
| int max_shortcut_width = 0; |
| int max_candidate_width = 0; |
| 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 candidate text. |
| if (candidate_index < new_candidate_window.candidates().size()) { |
| const ui::CandidateWindow::Entry& entry = |
| new_candidate_window.candidates()[candidate_index]; |
| candidate_view->SetEntry(entry); |
| candidate_view->SetEnabled(true); |
| candidate_view->SetInfolistIcon(!entry.description_title.empty()); |
| } else { |
| // Disable the empty row. |
| candidate_view->SetEntry(ui::CandidateWindow::Entry()); |
| candidate_view->SetEnabled(false); |
| candidate_view->SetInfolistIcon(false); |
| } |
| if (new_candidate_window.orientation() == ui::CandidateWindow::VERTICAL) { |
| int shortcut_width = 0; |
| int candidate_width = 0; |
| candidate_views_[i]->GetPreferredWidths( |
| &shortcut_width, &candidate_width); |
| max_shortcut_width = std::max(max_shortcut_width, shortcut_width); |
| max_candidate_width = std::max(max_candidate_width, candidate_width); |
| } |
| } |
| if (new_candidate_window.orientation() == ui::CandidateWindow::VERTICAL) { |
| for (size_t i = 0; i < candidate_views_.size(); ++i) |
| candidate_views_[i]->SetWidths(max_shortcut_width, max_candidate_width); |
| } |
| |
| CandidateWindowBorder* border = static_cast<CandidateWindowBorder*>( |
| GetBubbleFrameView()->bubble_border()); |
| if (new_candidate_window.orientation() == ui::CandidateWindow::VERTICAL) |
| border->set_offset(max_shortcut_width); |
| else |
| border->set_offset(0); |
| } |
| // 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_]->SetHighlighted( |
| false); |
| selected_candidate_index_in_page_ = -1; |
| } |
| } |
| |
| // Updates auxiliary text |
| auxiliary_text_->SetVisible(candidate_window_.is_auxiliary_text_visible()); |
| auxiliary_text_->SetText(base::UTF8ToUTF16( |
| candidate_window_.auxiliary_text())); |
| } |
| |
| void CandidateWindowView::SetCursorBounds(const gfx::Rect& cursor_bounds, |
| const gfx::Rect& composition_head) { |
| if (candidate_window_.show_window_at_composition()) |
| SetAnchorRect(composition_head); |
| else |
| SetAnchorRect(cursor_bounds); |
| } |
| |
| void CandidateWindowView::MaybeInitializeCandidateViews( |
| const ui::CandidateWindow& candidate_window) { |
| const ui::CandidateWindow::Orientation orientation = |
| candidate_window.orientation(); |
| const size_t page_size = candidate_window.page_size(); |
| |
| // Reset all candidate_views_ when orientation changes. |
| if (orientation != candidate_window_.orientation()) |
| STLDeleteElements(&candidate_views_); |
| |
| while (page_size < candidate_views_.size()) { |
| delete candidate_views_.back(); |
| candidate_views_.pop_back(); |
| } |
| while (page_size > candidate_views_.size()) { |
| CandidateView* new_candidate = new CandidateView(this, orientation); |
| candidate_area_->AddChildView(new_candidate); |
| candidate_views_.push_back(new_candidate); |
| } |
| } |
| |
| 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; |
| } |
| |
| // 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]->SetHighlighted(true); |
| |
| // Update the cursor indexes in the model. |
| candidate_window_.set_cursor_position(cursor_absolute_index); |
| } |
| |
| void CandidateWindowView::ButtonPressed(views::Button* sender, |
| const ui::Event& event) { |
| for (size_t i = 0; i < candidate_views_.size(); ++i) { |
| if (sender == candidate_views_[i]) { |
| FOR_EACH_OBSERVER(Observer, observers_, OnCandidateCommitted(i)); |
| return; |
| } |
| } |
| } |
| |
| } // namespace ime |
| } // namespace ash |