Home | History | Annotate | Download | only in ime
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "ash/ime/candidate_window_view.h"
      6 
      7 #include <string>
      8 
      9 #include "ash/ime/candidate_view.h"
     10 #include "ash/ime/candidate_window_constants.h"
     11 #include "base/strings/utf_string_conversions.h"
     12 #include "ui/gfx/color_utils.h"
     13 #include "ui/gfx/screen.h"
     14 #include "ui/native_theme/native_theme.h"
     15 #include "ui/views/background.h"
     16 #include "ui/views/border.h"
     17 #include "ui/views/bubble/bubble_frame_view.h"
     18 #include "ui/views/controls/label.h"
     19 #include "ui/views/layout/box_layout.h"
     20 #include "ui/views/layout/fill_layout.h"
     21 #include "ui/wm/core/window_animations.h"
     22 
     23 namespace ash {
     24 namespace ime {
     25 
     26 namespace {
     27 
     28 class CandidateWindowBorder : public views::BubbleBorder {
     29  public:
     30   explicit CandidateWindowBorder(gfx::NativeView parent)
     31       : views::BubbleBorder(views::BubbleBorder::TOP_CENTER,
     32                             views::BubbleBorder::NO_SHADOW,
     33                             SK_ColorTRANSPARENT),
     34         parent_(parent),
     35         offset_(0) {
     36     set_paint_arrow(views::BubbleBorder::PAINT_NONE);
     37   }
     38   virtual ~CandidateWindowBorder() {}
     39 
     40   void set_offset(int offset) { offset_ = offset; }
     41 
     42  private:
     43   // Overridden from views::BubbleBorder:
     44   virtual gfx::Rect GetBounds(const gfx::Rect& anchor_rect,
     45                               const gfx::Size& content_size) const OVERRIDE {
     46     gfx::Rect bounds(content_size);
     47     bounds.set_origin(gfx::Point(
     48         anchor_rect.x() - offset_,
     49         is_arrow_on_top(arrow()) ?
     50         anchor_rect.bottom() : anchor_rect.y() - content_size.height()));
     51 
     52     // It cannot use the normal logic of arrow offset for horizontal offscreen,
     53     // because the arrow must be in the content's edge. But CandidateWindow has
     54     // to be visible even when |anchor_rect| is out of the screen.
     55     gfx::Rect work_area = gfx::Screen::GetNativeScreen()->
     56         GetDisplayNearestWindow(parent_).work_area();
     57     if (bounds.right() > work_area.right())
     58       bounds.set_x(work_area.right() - bounds.width());
     59     if (bounds.x() < work_area.x())
     60       bounds.set_x(work_area.x());
     61 
     62     return bounds;
     63   }
     64 
     65   virtual gfx::Insets GetInsets() const OVERRIDE {
     66     return gfx::Insets();
     67   }
     68 
     69   gfx::NativeView parent_;
     70   int offset_;
     71 
     72   DISALLOW_COPY_AND_ASSIGN(CandidateWindowBorder);
     73 };
     74 
     75 // Computes the page index. For instance, if the page size is 9, and the
     76 // cursor is pointing to 13th candidate, the page index will be 1 (2nd
     77 // page, as the index is zero-origin). Returns -1 on error.
     78 int ComputePageIndex(const ui::CandidateWindow& candidate_window) {
     79   if (candidate_window.page_size() > 0)
     80     return candidate_window.cursor_position() / candidate_window.page_size();
     81   return -1;
     82 }
     83 
     84 }  // namespace
     85 
     86 class InformationTextArea : public views::View {
     87  public:
     88   // InformationTextArea's border is drawn as a separator, it should appear
     89   // at either top or bottom.
     90   enum BorderPosition {
     91     TOP,
     92     BOTTOM
     93   };
     94 
     95   // Specify the alignment and initialize the control.
     96   InformationTextArea(gfx::HorizontalAlignment align, int min_width)
     97       : min_width_(min_width) {
     98     label_ = new views::Label;
     99     label_->SetHorizontalAlignment(align);
    100     label_->SetBorder(views::Border::CreateEmptyBorder(2, 2, 2, 4));
    101 
    102     SetLayoutManager(new views::FillLayout());
    103     AddChildView(label_);
    104     set_background(views::Background::CreateSolidBackground(
    105         color_utils::AlphaBlend(SK_ColorBLACK,
    106                                 GetNativeTheme()->GetSystemColor(
    107                                     ui::NativeTheme::kColorId_WindowBackground),
    108                                 0x10)));
    109   }
    110 
    111   // Sets the text alignment.
    112   void SetAlignment(gfx::HorizontalAlignment alignment) {
    113     label_->SetHorizontalAlignment(alignment);
    114   }
    115 
    116   // Sets the displayed text.
    117   void SetText(const base::string16& text) {
    118     label_->SetText(text);
    119   }
    120 
    121   // Sets the border thickness for top/bottom.
    122   void SetBorderFromPosition(BorderPosition position) {
    123     SetBorder(views::Border::CreateSolidSidedBorder(
    124         (position == TOP) ? 1 : 0,
    125         0,
    126         (position == BOTTOM) ? 1 : 0,
    127         0,
    128         GetNativeTheme()->GetSystemColor(
    129             ui::NativeTheme::kColorId_MenuBorderColor)));
    130   }
    131 
    132  protected:
    133   virtual gfx::Size GetPreferredSize() const OVERRIDE {
    134     gfx::Size size = views::View::GetPreferredSize();
    135     size.SetToMax(gfx::Size(min_width_, 0));
    136     return size;
    137   }
    138 
    139  private:
    140   views::Label* label_;
    141   int min_width_;
    142 
    143   DISALLOW_COPY_AND_ASSIGN(InformationTextArea);
    144 };
    145 
    146 CandidateWindowView::CandidateWindowView(gfx::NativeView parent)
    147     : selected_candidate_index_in_page_(-1),
    148       should_show_at_composition_head_(false),
    149       should_show_upper_side_(false),
    150       was_candidate_window_open_(false) {
    151   set_use_focusless(true);
    152   set_parent_window(parent);
    153   set_margins(gfx::Insets());
    154 
    155   // Set the background and the border of the view.
    156   ui::NativeTheme* theme = GetNativeTheme();
    157   set_background(
    158       views::Background::CreateSolidBackground(theme->GetSystemColor(
    159           ui::NativeTheme::kColorId_WindowBackground)));
    160   SetBorder(views::Border::CreateSolidBorder(
    161       1, theme->GetSystemColor(ui::NativeTheme::kColorId_MenuBorderColor)));
    162 
    163   SetLayoutManager(new views::BoxLayout(views::BoxLayout::kVertical, 0, 0, 0));
    164   auxiliary_text_ = new InformationTextArea(gfx::ALIGN_RIGHT, 0);
    165   preedit_ = new InformationTextArea(gfx::ALIGN_LEFT, kMinPreeditAreaWidth);
    166   candidate_area_ = new views::View;
    167   auxiliary_text_->SetVisible(false);
    168   preedit_->SetVisible(false);
    169   candidate_area_->SetVisible(false);
    170   preedit_->SetBorderFromPosition(InformationTextArea::BOTTOM);
    171   if (candidate_window_.orientation() == ui::CandidateWindow::VERTICAL) {
    172     AddChildView(preedit_);
    173     AddChildView(candidate_area_);
    174     AddChildView(auxiliary_text_);
    175     auxiliary_text_->SetBorderFromPosition(InformationTextArea::TOP);
    176     candidate_area_->SetLayoutManager(new views::BoxLayout(
    177         views::BoxLayout::kVertical, 0, 0, 0));
    178   } else {
    179     AddChildView(preedit_);
    180     AddChildView(auxiliary_text_);
    181     AddChildView(candidate_area_);
    182     auxiliary_text_->SetAlignment(gfx::ALIGN_LEFT);
    183     auxiliary_text_->SetBorderFromPosition(InformationTextArea::BOTTOM);
    184     candidate_area_->SetLayoutManager(new views::BoxLayout(
    185         views::BoxLayout::kHorizontal, 0, 0, 0));
    186   }
    187 }
    188 
    189 CandidateWindowView::~CandidateWindowView() {
    190 }
    191 
    192 views::Widget* CandidateWindowView::InitWidget() {
    193   views::Widget* widget = BubbleDelegateView::CreateBubble(this);
    194 
    195   wm::SetWindowVisibilityAnimationType(
    196       widget->GetNativeView(),
    197       wm::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE);
    198 
    199   GetBubbleFrameView()->SetBubbleBorder(scoped_ptr<views::BubbleBorder>(
    200       new CandidateWindowBorder(parent_window())));
    201   return widget;
    202 }
    203 
    204 void CandidateWindowView::UpdateVisibility() {
    205   if (candidate_area_->visible() || auxiliary_text_->visible() ||
    206       preedit_->visible()) {
    207     SizeToContents();
    208   } else {
    209     GetWidget()->Close();
    210   }
    211 }
    212 
    213 void CandidateWindowView::HideLookupTable() {
    214   candidate_area_->SetVisible(false);
    215   auxiliary_text_->SetVisible(false);
    216   UpdateVisibility();
    217 }
    218 
    219 void CandidateWindowView::HidePreeditText() {
    220   preedit_->SetVisible(false);
    221   UpdateVisibility();
    222 }
    223 
    224 void CandidateWindowView::ShowPreeditText() {
    225   preedit_->SetVisible(true);
    226   UpdateVisibility();
    227 }
    228 
    229 void CandidateWindowView::UpdatePreeditText(const base::string16& text) {
    230   preedit_->SetText(text);
    231 }
    232 
    233 void CandidateWindowView::ShowLookupTable() {
    234   candidate_area_->SetVisible(true);
    235   auxiliary_text_->SetVisible(candidate_window_.is_auxiliary_text_visible());
    236   UpdateVisibility();
    237 }
    238 
    239 void CandidateWindowView::UpdateCandidates(
    240     const ui::CandidateWindow& new_candidate_window) {
    241   // Updating the candidate views is expensive. We'll skip this if possible.
    242   if (!candidate_window_.IsEqual(new_candidate_window)) {
    243     if (candidate_window_.orientation() != new_candidate_window.orientation()) {
    244       // If the new layout is vertical, the aux text should appear at the
    245       // bottom. If horizontal, it should appear between preedit and candidates.
    246       if (new_candidate_window.orientation() == ui::CandidateWindow::VERTICAL) {
    247         ReorderChildView(auxiliary_text_, -1);
    248         auxiliary_text_->SetAlignment(gfx::ALIGN_RIGHT);
    249         auxiliary_text_->SetBorderFromPosition(InformationTextArea::TOP);
    250         candidate_area_->SetLayoutManager(new views::BoxLayout(
    251             views::BoxLayout::kVertical, 0, 0, 0));
    252       } else {
    253         ReorderChildView(auxiliary_text_, 1);
    254         auxiliary_text_->SetAlignment(gfx::ALIGN_LEFT);
    255         auxiliary_text_->SetBorderFromPosition(InformationTextArea::BOTTOM);
    256         candidate_area_->SetLayoutManager(new views::BoxLayout(
    257             views::BoxLayout::kHorizontal, 0, 0, 0));
    258       }
    259     }
    260 
    261     // Initialize candidate views if necessary.
    262     MaybeInitializeCandidateViews(new_candidate_window);
    263 
    264     should_show_at_composition_head_
    265         = new_candidate_window.show_window_at_composition();
    266     // Compute the index of the current page.
    267     const int current_page_index = ComputePageIndex(new_candidate_window);
    268     if (current_page_index < 0)
    269       return;
    270 
    271     // Update the candidates in the current page.
    272     const size_t start_from =
    273         current_page_index * new_candidate_window.page_size();
    274 
    275     int max_shortcut_width = 0;
    276     int max_candidate_width = 0;
    277     for (size_t i = 0; i < candidate_views_.size(); ++i) {
    278       const size_t index_in_page = i;
    279       const size_t candidate_index = start_from + index_in_page;
    280       CandidateView* candidate_view = candidate_views_[index_in_page];
    281       // Set the candidate text.
    282       if (candidate_index < new_candidate_window.candidates().size()) {
    283         const ui::CandidateWindow::Entry& entry =
    284             new_candidate_window.candidates()[candidate_index];
    285         candidate_view->SetEntry(entry);
    286         candidate_view->SetEnabled(true);
    287         candidate_view->SetInfolistIcon(!entry.description_title.empty());
    288       } else {
    289         // Disable the empty row.
    290         candidate_view->SetEntry(ui::CandidateWindow::Entry());
    291         candidate_view->SetEnabled(false);
    292         candidate_view->SetInfolistIcon(false);
    293       }
    294       if (new_candidate_window.orientation() == ui::CandidateWindow::VERTICAL) {
    295         int shortcut_width = 0;
    296         int candidate_width = 0;
    297         candidate_views_[i]->GetPreferredWidths(
    298             &shortcut_width, &candidate_width);
    299         max_shortcut_width = std::max(max_shortcut_width, shortcut_width);
    300         max_candidate_width = std::max(max_candidate_width, candidate_width);
    301       }
    302     }
    303     if (new_candidate_window.orientation() == ui::CandidateWindow::VERTICAL) {
    304       for (size_t i = 0; i < candidate_views_.size(); ++i)
    305         candidate_views_[i]->SetWidths(max_shortcut_width, max_candidate_width);
    306     }
    307 
    308     CandidateWindowBorder* border = static_cast<CandidateWindowBorder*>(
    309         GetBubbleFrameView()->bubble_border());
    310     if (new_candidate_window.orientation() == ui::CandidateWindow::VERTICAL)
    311       border->set_offset(max_shortcut_width);
    312     else
    313       border->set_offset(0);
    314   }
    315   // Update the current candidate window. We'll use candidate_window_ from here.
    316   // Note that SelectCandidateAt() uses candidate_window_.
    317   candidate_window_.CopyFrom(new_candidate_window);
    318 
    319   // Select the current candidate in the page.
    320   if (candidate_window_.is_cursor_visible()) {
    321     if (candidate_window_.page_size()) {
    322       const int current_candidate_in_page =
    323           candidate_window_.cursor_position() % candidate_window_.page_size();
    324       SelectCandidateAt(current_candidate_in_page);
    325     }
    326   } else {
    327     // Unselect the currently selected candidate.
    328     if (0 <= selected_candidate_index_in_page_ &&
    329         static_cast<size_t>(selected_candidate_index_in_page_) <
    330         candidate_views_.size()) {
    331       candidate_views_[selected_candidate_index_in_page_]->SetHighlighted(
    332           false);
    333       selected_candidate_index_in_page_ = -1;
    334     }
    335   }
    336 
    337   // Updates auxiliary text
    338   auxiliary_text_->SetVisible(candidate_window_.is_auxiliary_text_visible());
    339   auxiliary_text_->SetText(base::UTF8ToUTF16(
    340       candidate_window_.auxiliary_text()));
    341 }
    342 
    343 void CandidateWindowView::SetCursorBounds(const gfx::Rect& cursor_bounds,
    344                                           const gfx::Rect& composition_head) {
    345   if (candidate_window_.show_window_at_composition())
    346     SetAnchorRect(composition_head);
    347   else
    348     SetAnchorRect(cursor_bounds);
    349 }
    350 
    351 void CandidateWindowView::MaybeInitializeCandidateViews(
    352     const ui::CandidateWindow& candidate_window) {
    353   const ui::CandidateWindow::Orientation orientation =
    354       candidate_window.orientation();
    355   const size_t page_size = candidate_window.page_size();
    356 
    357   // Reset all candidate_views_ when orientation changes.
    358   if (orientation != candidate_window_.orientation())
    359     STLDeleteElements(&candidate_views_);
    360 
    361   while (page_size < candidate_views_.size()) {
    362     delete candidate_views_.back();
    363     candidate_views_.pop_back();
    364   }
    365   while (page_size > candidate_views_.size()) {
    366     CandidateView* new_candidate = new CandidateView(this, orientation);
    367     candidate_area_->AddChildView(new_candidate);
    368     candidate_views_.push_back(new_candidate);
    369   }
    370 }
    371 
    372 void CandidateWindowView::SelectCandidateAt(int index_in_page) {
    373   const int current_page_index = ComputePageIndex(candidate_window_);
    374   if (current_page_index < 0) {
    375     return;
    376   }
    377 
    378   const int cursor_absolute_index =
    379       candidate_window_.page_size() * current_page_index + index_in_page;
    380   // Ignore click on out of range views.
    381   if (cursor_absolute_index < 0 ||
    382       candidate_window_.candidates().size() <=
    383       static_cast<size_t>(cursor_absolute_index)) {
    384     return;
    385   }
    386 
    387   // Remember the currently selected candidate index in the current page.
    388   selected_candidate_index_in_page_ = index_in_page;
    389 
    390   // Select the candidate specified by index_in_page.
    391   candidate_views_[index_in_page]->SetHighlighted(true);
    392 
    393   // Update the cursor indexes in the model.
    394   candidate_window_.set_cursor_position(cursor_absolute_index);
    395 }
    396 
    397 void CandidateWindowView::ButtonPressed(views::Button* sender,
    398                                         const ui::Event& event) {
    399   for (size_t i = 0; i < candidate_views_.size(); ++i) {
    400     if (sender == candidate_views_[i]) {
    401       FOR_EACH_OBSERVER(Observer, observers_, OnCandidateCommitted(i));
    402       return;
    403     }
    404   }
    405 }
    406 
    407 }  // namespace ime
    408 }  // namespace ash
    409