// 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 #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_can_activate(false); 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( 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( 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(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(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