Home | History | Annotate | Download | only in input_method
      1 // Copyright (c) 2012 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 #include "chrome/browser/chromeos/input_method/candidate_window_view.h"
      5 
      6 #include <string>
      7 
      8 #include "ash/shell.h"
      9 #include "base/strings/utf_string_conversions.h"
     10 #include "chrome/browser/chromeos/input_method/candidate_view.h"
     11 #include "chrome/browser/chromeos/input_method/candidate_window_constants.h"
     12 #include "chrome/browser/chromeos/input_method/hidable_area.h"
     13 #include "chromeos/dbus/ibus/ibus_lookup_table.h"
     14 #include "ui/gfx/color_utils.h"
     15 #include "ui/native_theme/native_theme.h"
     16 #include "ui/views/controls/label.h"
     17 #include "ui/views/layout/grid_layout.h"
     18 #include "ui/views/widget/widget.h"
     19 
     20 namespace chromeos {
     21 namespace input_method {
     22 
     23 namespace {
     24 // VerticalCandidateLabel is used for rendering candidate text in
     25 // the vertical candidate window.
     26 class VerticalCandidateLabel : public views::Label {
     27  public:
     28   VerticalCandidateLabel() {}
     29 
     30  private:
     31   virtual ~VerticalCandidateLabel() {}
     32 
     33   // Returns the preferred size, but guarantees that the width has at
     34   // least kMinCandidateLabelWidth pixels.
     35   virtual gfx::Size GetPreferredSize() OVERRIDE {
     36     gfx::Size size = Label::GetPreferredSize();
     37     // Hack. +2 is needed to prevent labels from getting elided like
     38     // "abc..." in some cases. TODO(satorux): Figure out why it's
     39     // necessary.
     40     size.set_width(size.width() + 2);
     41     if (size.width() < kMinCandidateLabelWidth) {
     42       size.set_width(kMinCandidateLabelWidth);
     43     }
     44     if (size.width() > kMaxCandidateLabelWidth) {
     45       size.set_width(kMaxCandidateLabelWidth);
     46     }
     47     return size;
     48   }
     49 
     50   DISALLOW_COPY_AND_ASSIGN(VerticalCandidateLabel);
     51 };
     52 
     53 // Wraps the given view with some padding, and returns it.
     54 views::View* WrapWithPadding(views::View* view, const gfx::Insets& insets) {
     55   views::View* wrapper = new views::View;
     56   // Use GridLayout to give some insets inside.
     57   views::GridLayout* layout = new views::GridLayout(wrapper);
     58   wrapper->SetLayoutManager(layout);  // |wrapper| owns |layout|.
     59   layout->SetInsets(insets);
     60 
     61   views::ColumnSet* column_set = layout->AddColumnSet(0);
     62   column_set->AddColumn(
     63       views::GridLayout::FILL, views::GridLayout::FILL,
     64       1, views::GridLayout::USE_PREF, 0, 0);
     65   layout->StartRow(0, 0);
     66 
     67   // Add the view contents.
     68   layout->AddView(view);  // |view| is owned by |wraper|, not |layout|.
     69   return wrapper;
     70 }
     71 
     72 // Creates shortcut text from the given index and the orientation.
     73 string16 CreateShortcutText(size_t index, const IBusLookupTable& table) {
     74   if (index >= table.candidates().size())
     75     return UTF8ToUTF16("");
     76   std::string shortcut_text = table.candidates()[index].label;
     77   if (!shortcut_text.empty() &&
     78       table.orientation() != IBusLookupTable::VERTICAL)
     79     shortcut_text += '.';
     80   return UTF8ToUTF16(shortcut_text);
     81 }
     82 
     83 // Creates the shortcut label, and returns it (never returns NULL).
     84 // The label text is not set in this function.
     85 views::Label* CreateShortcutLabel(
     86     IBusLookupTable::Orientation orientation, const ui::NativeTheme& theme) {
     87   // Create the shortcut label. The label will be owned by
     88   // |wrapped_shortcut_label|, hence it's deleted when
     89   // |wrapped_shortcut_label| is deleted.
     90   views::Label* shortcut_label = new views::Label;
     91 
     92   if (orientation == IBusLookupTable::VERTICAL) {
     93     shortcut_label->SetFont(
     94         shortcut_label->font().DeriveFont(kFontSizeDelta, gfx::Font::BOLD));
     95   } else {
     96     shortcut_label->SetFont(
     97         shortcut_label->font().DeriveFont(kFontSizeDelta));
     98   }
     99   // TODO(satorux): Maybe we need to use language specific fonts for
    100   // candidate_label, like Chinese font for Chinese input method?
    101   shortcut_label->SetEnabledColor(theme.GetSystemColor(
    102       ui::NativeTheme::kColorId_LabelEnabledColor));
    103   shortcut_label->SetDisabledColor(theme.GetSystemColor(
    104       ui::NativeTheme::kColorId_LabelDisabledColor));
    105 
    106   return shortcut_label;
    107 }
    108 
    109 // Wraps the shortcut label, then decorates wrapped shortcut label
    110 // and returns it (never returns NULL).
    111 // The label text is not set in this function.
    112 views::View* CreateWrappedShortcutLabel(
    113     views::Label* shortcut_label,
    114     IBusLookupTable::Orientation orientation,
    115     const ui::NativeTheme& theme) {
    116   // Wrap it with padding.
    117   const gfx::Insets kVerticalShortcutLabelInsets(1, 6, 1, 6);
    118   const gfx::Insets kHorizontalShortcutLabelInsets(1, 3, 1, 0);
    119   const gfx::Insets insets =
    120       (orientation == IBusLookupTable::VERTICAL ?
    121        kVerticalShortcutLabelInsets :
    122        kHorizontalShortcutLabelInsets);
    123   views::View* wrapped_shortcut_label =
    124       WrapWithPadding(shortcut_label, insets);
    125 
    126   // Add decoration based on the orientation.
    127   if (orientation == IBusLookupTable::VERTICAL) {
    128     // Set the background color.
    129     SkColor blackish = color_utils::AlphaBlend(
    130         SK_ColorBLACK,
    131         theme.GetSystemColor(ui::NativeTheme::kColorId_WindowBackground),
    132         0x40);
    133     SkColor transparent_blakish = color_utils::AlphaBlend(
    134         SK_ColorTRANSPARENT, blackish, 0xE0);
    135     wrapped_shortcut_label->set_background(
    136         views::Background::CreateSolidBackground(transparent_blakish));
    137     shortcut_label->SetBackgroundColor(
    138         wrapped_shortcut_label->background()->get_color());
    139   }
    140 
    141   return wrapped_shortcut_label;
    142 }
    143 
    144 // Creates the candidate label, and returns it (never returns NULL).
    145 // The label text is not set in this function.
    146 views::Label* CreateCandidateLabel(
    147     IBusLookupTable::Orientation orientation) {
    148   views::Label* candidate_label = NULL;
    149 
    150   // Create the candidate label. The label will be added to |this| as a
    151   // child view, hence it's deleted when |this| is deleted.
    152   if (orientation == IBusLookupTable::VERTICAL) {
    153     candidate_label = new VerticalCandidateLabel;
    154   } else {
    155     candidate_label = new views::Label;
    156   }
    157 
    158   // Change the font size.
    159   candidate_label->SetFont(
    160       candidate_label->font().DeriveFont(kFontSizeDelta));
    161   candidate_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    162 
    163   return candidate_label;
    164 }
    165 
    166 // Creates the annotation label, and return it (never returns NULL).
    167 // The label text is not set in this function.
    168 views::Label* CreateAnnotationLabel(
    169     IBusLookupTable::Orientation orientation, const ui::NativeTheme& theme) {
    170   // Create the annotation label.
    171   views::Label* annotation_label = new views::Label;
    172 
    173   // Change the font size and color.
    174   annotation_label->SetFont(
    175       annotation_label->font().DeriveFont(kFontSizeDelta));
    176   annotation_label->SetEnabledColor(theme.GetSystemColor(
    177       ui::NativeTheme::kColorId_LabelDisabledColor));
    178   annotation_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    179 
    180   return annotation_label;
    181 }
    182 
    183 // Computes shortcut column size.
    184 gfx::Size ComputeShortcutColumnSize(
    185     const IBusLookupTable& lookup_table,
    186     const ui::NativeTheme& theme) {
    187   int shortcut_column_width = 0;
    188   int shortcut_column_height = 0;
    189   // Create the shortcut label. The label will be owned by
    190   // |wrapped_shortcut_label|, hence it's deleted when
    191   // |wrapped_shortcut_label| is deleted.
    192   views::Label* shortcut_label = CreateShortcutLabel(
    193       lookup_table.orientation(), theme);
    194   scoped_ptr<views::View> wrapped_shortcut_label(
    195       CreateWrappedShortcutLabel(shortcut_label,
    196                                  lookup_table.orientation(),
    197                                  theme));
    198 
    199   // Compute the max width and height in shortcut labels.
    200   // We'll create temporary shortcut labels, and choose the largest width and
    201   // height.
    202   for (size_t i = 0; i < lookup_table.page_size(); ++i) {
    203     shortcut_label->SetText(CreateShortcutText(i, lookup_table));
    204     gfx::Size text_size = wrapped_shortcut_label->GetPreferredSize();
    205     shortcut_column_width = std::max(shortcut_column_width, text_size.width());
    206     shortcut_column_height = std::max(shortcut_column_height,
    207                                       text_size.height());
    208   }
    209 
    210   return gfx::Size(shortcut_column_width, shortcut_column_height);
    211 }
    212 
    213 // Computes the page index. For instance, if the page size is 9, and the
    214 // cursor is pointing to 13th candidate, the page index will be 1 (2nd
    215 // page, as the index is zero-origin). Returns -1 on error.
    216 int ComputePageIndex(const IBusLookupTable& lookup_table) {
    217   if (lookup_table.page_size() > 0)
    218     return lookup_table.cursor_position() / lookup_table.page_size();
    219   return -1;
    220 }
    221 
    222 // Computes candidate column size.
    223 gfx::Size ComputeCandidateColumnSize(
    224     const IBusLookupTable& lookup_table) {
    225   int candidate_column_width = 0;
    226   int candidate_column_height = 0;
    227   scoped_ptr<views::Label> candidate_label(
    228       CreateCandidateLabel(lookup_table.orientation()));
    229 
    230   // Compute the start index of |lookup_table_|.
    231   const int current_page_index = ComputePageIndex(lookup_table);
    232   if (current_page_index < 0)
    233     return gfx::Size(0, 0);
    234   const size_t start_from = current_page_index * lookup_table.page_size();
    235 
    236   // Compute the max width and height in candidate labels.
    237   // We'll create temporary candidate labels, and choose the largest width and
    238   // height.
    239   for (size_t i = 0; i + start_from < lookup_table.candidates().size(); ++i) {
    240     const size_t index = start_from + i;
    241 
    242     candidate_label->SetText(
    243         UTF8ToUTF16(lookup_table.candidates()[index].value));
    244     gfx::Size text_size = candidate_label->GetPreferredSize();
    245     candidate_column_width = std::max(candidate_column_width,
    246                                       text_size.width());
    247     candidate_column_height = std::max(candidate_column_height,
    248                                        text_size.height());
    249   }
    250 
    251   return gfx::Size(candidate_column_width, candidate_column_height);
    252 }
    253 
    254 // Computes annotation column size.
    255 gfx::Size ComputeAnnotationColumnSize(
    256     const IBusLookupTable& lookup_table, const ui::NativeTheme& theme) {
    257   int annotation_column_width = 0;
    258   int annotation_column_height = 0;
    259   scoped_ptr<views::Label> annotation_label(
    260       CreateAnnotationLabel(lookup_table.orientation(), theme));
    261 
    262   // Compute the start index of |lookup_table_|.
    263   const int current_page_index = ComputePageIndex(lookup_table);
    264   if (current_page_index < 0)
    265     return gfx::Size(0, 0);
    266   const size_t start_from = current_page_index * lookup_table.page_size();
    267 
    268   // Compute max width and height in annotation labels.
    269   // We'll create temporary annotation labels, and choose the largest width and
    270   // height.
    271   for (size_t i = 0; i + start_from < lookup_table.candidates().size(); ++i) {
    272     const size_t index = start_from + i;
    273 
    274     annotation_label->SetText(
    275         UTF8ToUTF16(lookup_table.candidates()[index].annotation));
    276     gfx::Size text_size = annotation_label->GetPreferredSize();
    277     annotation_column_width = std::max(annotation_column_width,
    278                                        text_size.width());
    279     annotation_column_height = std::max(annotation_column_height,
    280                                         text_size.height());
    281   }
    282 
    283   return gfx::Size(annotation_column_width, annotation_column_height);
    284 }
    285 
    286 }  // namespace
    287 
    288 // InformationTextArea is a HidableArea having a single Label in it.
    289 class InformationTextArea : public HidableArea {
    290  public:
    291   // Specify the alignment and initialize the control.
    292   InformationTextArea(gfx::HorizontalAlignment align, int minWidth)
    293       : minWidth_(minWidth) {
    294     label_ = new views::Label;
    295     label_->SetHorizontalAlignment(align);
    296 
    297     const gfx::Insets kInsets(2, 2, 2, 4);
    298     views::View* contents = WrapWithPadding(label_, kInsets);
    299     SetContents(contents);
    300     contents->set_border(views::Border::CreateSolidBorder(
    301         1,
    302         GetNativeTheme()->GetSystemColor(
    303             ui::NativeTheme::kColorId_MenuBorderColor)));
    304     contents->set_background(views::Background::CreateSolidBackground(
    305         color_utils::AlphaBlend(SK_ColorBLACK,
    306                                 GetNativeTheme()->GetSystemColor(
    307                                     ui::NativeTheme::kColorId_WindowBackground),
    308                                 0x10)));
    309     label_->SetBackgroundColor(contents->background()->get_color());
    310   }
    311 
    312   // Set the displayed text.
    313   void SetText(const std::string& utf8_text) {
    314     label_->SetText(UTF8ToUTF16(utf8_text));
    315   }
    316 
    317  protected:
    318   virtual gfx::Size GetPreferredSize() OVERRIDE {
    319     gfx::Size size = HidableArea::GetPreferredSize();
    320     // Hack. +2 is needed as the same reason as in VerticalCandidateLabel
    321     size.set_width(size.width() + 2);
    322     if (size.width() < minWidth_) {
    323       size.set_width(minWidth_);
    324     }
    325     return size;
    326   }
    327 
    328  private:
    329   views::Label* label_;
    330   int minWidth_;
    331 
    332   DISALLOW_COPY_AND_ASSIGN(InformationTextArea);
    333 };
    334 
    335 CandidateView::CandidateView(
    336     CandidateWindowView* parent_candidate_window,
    337     int index_in_page,
    338     IBusLookupTable::Orientation orientation)
    339     : index_in_page_(index_in_page),
    340       orientation_(orientation),
    341       parent_candidate_window_(parent_candidate_window),
    342       shortcut_label_(NULL),
    343       candidate_label_(NULL),
    344       annotation_label_(NULL),
    345       infolist_icon_(NULL),
    346       infolist_icon_enabled_(false) {
    347 }
    348 
    349 void CandidateView::Init(int shortcut_column_width,
    350                          int candidate_column_width,
    351                          int annotation_column_width,
    352                          int column_height) {
    353   views::GridLayout* layout = new views::GridLayout(this);
    354   SetLayoutManager(layout);  // |this| owns |layout|.
    355 
    356   // Create Labels.
    357   const ui::NativeTheme& theme = *GetNativeTheme();
    358   shortcut_label_ = CreateShortcutLabel(orientation_, theme);
    359   views::View* wrapped_shortcut_label =
    360       CreateWrappedShortcutLabel(shortcut_label_, orientation_, theme);
    361   candidate_label_ = CreateCandidateLabel(orientation_);
    362   annotation_label_ = CreateAnnotationLabel(orientation_, theme);
    363 
    364   // Initialize the column set with three columns.
    365   views::ColumnSet* column_set = layout->AddColumnSet(0);
    366 
    367   // If orientation is vertical, each column width is fixed.
    368   // Otherwise the width is resizable.
    369   const views::GridLayout::SizeType column_type =
    370       orientation_ == IBusLookupTable::VERTICAL ?
    371           views::GridLayout::FIXED : views::GridLayout::USE_PREF;
    372 
    373   const int padding_column_width =
    374       orientation_ == IBusLookupTable::VERTICAL ? 4 : 6;
    375 
    376   // Set shortcut column type and width.
    377   column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
    378                         0, column_type, shortcut_column_width, 0);
    379   column_set->AddPaddingColumn(0, padding_column_width);
    380 
    381   // Set candidate column type and width.
    382   column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
    383                         1, views::GridLayout::USE_PREF, 0,
    384                         orientation_ == IBusLookupTable::VERTICAL ?
    385                         candidate_column_width : 0);
    386   column_set->AddPaddingColumn(0, padding_column_width);
    387 
    388   // Set annotation column type and width.
    389   column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
    390                         0, column_type, annotation_column_width, 0);
    391 
    392   if (orientation_ == IBusLookupTable::VERTICAL) {
    393     column_set->AddPaddingColumn(0, 1);
    394     column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 0,
    395                           views::GridLayout::FIXED, kInfolistIndicatorIconWidth,
    396                           0);
    397     column_set->AddPaddingColumn(0, 2);
    398   } else {
    399     column_set->AddPaddingColumn(0, padding_column_width);
    400   }
    401 
    402   // Add the shortcut label, the candidate label, and annotation label.
    403   layout->StartRow(0, 0);
    404   // |wrapped_shortcut_label|, |candidate_label_|, and |annotation_label_|
    405   // will be owned by |this|.
    406   layout->AddView(wrapped_shortcut_label,
    407                   1,  // Column span.
    408                   1,  // Row span.
    409                   views::GridLayout::FILL,  // Horizontal alignment.
    410                   views::GridLayout::FILL,  // Vertical alignment.
    411                   -1,  // Preferred width, not specified.
    412                   column_height);  // Preferred height.
    413   layout->AddView(candidate_label_,
    414                   1,  // Column span.
    415                   1,  // Row span.
    416                   views::GridLayout::FILL,  // Horizontal alignment.
    417                   views::GridLayout::FILL,  // Vertical alignment.
    418                   -1,  // Preferred width, not specified.
    419                   column_height);  // Preferred height.
    420   layout->AddView(annotation_label_,
    421                   1,  // Column span.
    422                   1,  // Row span.
    423                   views::GridLayout::FILL,  // Horizontal alignment.
    424                   views::GridLayout::FILL,  // Vertical alignemnt.
    425                   -1,  // Preferred width, not specified.
    426                   column_height);  // Preferred height.
    427   if (orientation_ == IBusLookupTable::VERTICAL) {
    428     infolist_icon_ = new views::View;
    429     views::View* infolist_icon_wrapper = new views::View;
    430     views::GridLayout* infolist_icon_layout =
    431         new views::GridLayout(infolist_icon_wrapper);
    432     // |infolist_icon_layout| is owned by |infolist_icon_wrapper|.
    433     infolist_icon_wrapper->SetLayoutManager(infolist_icon_layout);
    434     infolist_icon_layout->AddColumnSet(0)->AddColumn(
    435         views::GridLayout::FILL, views::GridLayout::FILL,
    436         0, views::GridLayout::FIXED, kInfolistIndicatorIconWidth, 0);
    437     infolist_icon_layout->AddPaddingRow(0, kInfolistIndicatorIconPadding);
    438     infolist_icon_layout->StartRow(1.0, 0);  // infolist_icon_ is resizable.
    439     // |infolist_icon_| is owned by |infolist_icon_wrapper|.
    440     infolist_icon_layout->AddView(infolist_icon_);
    441     infolist_icon_layout->AddPaddingRow(0, kInfolistIndicatorIconPadding);
    442     // |infolist_icon_wrapper| is owned by |this|.
    443     layout->AddView(infolist_icon_wrapper);
    444   }
    445   UpdateLabelBackgroundColors();
    446 }
    447 
    448 void CandidateView::SetCandidateText(const string16& text) {
    449   candidate_label_->SetText(text);
    450 }
    451 
    452 void CandidateView::SetShortcutText(const string16& text) {
    453   shortcut_label_->SetText(text);
    454 }
    455 
    456 void CandidateView::SetAnnotationText(const string16& text) {
    457   annotation_label_->SetText(text);
    458 }
    459 
    460 void CandidateView::SetInfolistIcon(bool enable) {
    461   if (!infolist_icon_ || (infolist_icon_enabled_ == enable))
    462     return;
    463   infolist_icon_enabled_ = enable;
    464   infolist_icon_->set_background(
    465       enable ?
    466       views::Background::CreateSolidBackground(GetNativeTheme()->GetSystemColor(
    467           ui::NativeTheme::kColorId_FocusedBorderColor)) :
    468       NULL);
    469   UpdateLabelBackgroundColors();
    470   SchedulePaint();
    471 }
    472 
    473 void CandidateView::Select() {
    474   set_background(
    475       views::Background::CreateSolidBackground(GetNativeTheme()->GetSystemColor(
    476           ui::NativeTheme::kColorId_TextfieldSelectionBackgroundFocused)));
    477   set_border(views::Border::CreateSolidBorder(
    478       1, GetNativeTheme()->GetSystemColor(
    479           ui::NativeTheme::kColorId_FocusedBorderColor)));
    480   UpdateLabelBackgroundColors();
    481   // Need to call SchedulePaint() for background and border color changes.
    482   SchedulePaint();
    483 }
    484 
    485 void CandidateView::Unselect() {
    486   set_background(NULL);
    487   set_border(NULL);
    488   UpdateLabelBackgroundColors();
    489   SchedulePaint();  // See comments at Select().
    490 }
    491 
    492 void CandidateView::SetRowEnabled(bool enabled) {
    493   shortcut_label_->SetEnabled(enabled);
    494 }
    495 
    496 gfx::Point CandidateView::GetCandidateLabelPosition() const {
    497   return candidate_label_->GetMirroredPosition();
    498 }
    499 
    500 bool CandidateView::OnMousePressed(const ui::MouseEvent& event) {
    501   // TODO(kinaba): On Windows and MacOS, candidate windows typically commits a
    502   // candidate at OnMouseReleased event. We have chosen OnMousePressed here for
    503   // working around several obstacle rising from views implementation over GTK.
    504   // See: http://crosbug.com/11423#c11. Since we have moved from GTK to Aura,
    505   // the reasoning should have became obsolete. We might want to reconsider
    506   // implementing mouse-up selection.
    507   SelectCandidateAt(event.location());
    508   return false;
    509 }
    510 
    511 void CandidateView::OnGestureEvent(ui::GestureEvent* event) {
    512   if (event->type() == ui::ET_GESTURE_TAP) {
    513     SelectCandidateAt(event->location());
    514     event->SetHandled();
    515     return;
    516   }
    517   View::OnGestureEvent(event);
    518 }
    519 
    520 void CandidateView::SelectCandidateAt(const gfx::Point& location) {
    521   gfx::Point location_in_candidate_window = location;
    522   views::View::ConvertPointToTarget(this, parent_candidate_window_,
    523                                     &location_in_candidate_window);
    524   parent_candidate_window_->OnCandidatePressed(location_in_candidate_window);
    525   parent_candidate_window_->CommitCandidate();
    526 }
    527 
    528 void CandidateView::UpdateLabelBackgroundColors() {
    529   SkColor color = background() ?
    530       background()->get_color() :
    531       GetNativeTheme()->GetSystemColor(
    532           ui::NativeTheme::kColorId_WindowBackground);
    533   if (orientation_ != IBusLookupTable::VERTICAL)
    534     shortcut_label_->SetBackgroundColor(color);
    535   candidate_label_->SetBackgroundColor(color);
    536   annotation_label_->SetBackgroundColor(color);
    537 }
    538 
    539 CandidateWindowView::CandidateWindowView(views::Widget* parent_frame)
    540     : selected_candidate_index_in_page_(-1),
    541       parent_frame_(parent_frame),
    542       preedit_area_(NULL),
    543       header_area_(NULL),
    544       candidate_area_(NULL),
    545       footer_area_(NULL),
    546       previous_shortcut_column_size_(0, 0),
    547       previous_candidate_column_size_(0, 0),
    548       previous_annotation_column_size_(0, 0),
    549       should_show_at_composition_head_(false),
    550       should_show_upper_side_(false),
    551       was_candidate_window_open_(false) {
    552 }
    553 
    554 CandidateWindowView::~CandidateWindowView() {
    555 }
    556 
    557 void CandidateWindowView::Init() {
    558   // Set the background and the border of the view.
    559   set_background(
    560       views::Background::CreateSolidBackground(GetNativeTheme()->GetSystemColor(
    561           ui::NativeTheme::kColorId_WindowBackground)));
    562   set_border(views::Border::CreateSolidBorder(
    563       1, GetNativeTheme()->GetSystemColor(
    564           ui::NativeTheme::kColorId_MenuBorderColor)));
    565 
    566   // Create areas.
    567   preedit_area_ = new InformationTextArea(gfx::ALIGN_LEFT,
    568                                           kMinPreeditAreaWidth);
    569   header_area_ = new InformationTextArea(gfx::ALIGN_LEFT, 0);
    570   candidate_area_ = new HidableArea;
    571   candidate_area_->SetContents(new views::View);
    572   footer_area_ = new InformationTextArea(gfx::ALIGN_RIGHT, 0);
    573 
    574   // Set the window layout of the view
    575   views::GridLayout* layout = new views::GridLayout(this);
    576   SetLayoutManager(layout);  // |this| owns |layout|.
    577   views::ColumnSet* column_set = layout->AddColumnSet(0);
    578   column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
    579                         0, views::GridLayout::USE_PREF, 0, 0);
    580 
    581   // Add the preedit area
    582   layout->StartRow(0, 0);
    583   layout->AddView(preedit_area_);  // |preedit_area_| is owned by |this|.
    584 
    585   // Add the header area.
    586   layout->StartRow(0, 0);
    587   layout->AddView(header_area_);  // |header_area_| is owned by |this|.
    588 
    589   // Add the candidate area.
    590   layout->StartRow(0, 0);
    591   layout->AddView(candidate_area_);  // |candidate_area_| is owned by |this|.
    592 
    593   // Add the footer area.
    594   layout->StartRow(0, 0);
    595   layout->AddView(footer_area_);  // |footer_area_| is owned by |this|.
    596 }
    597 
    598 void CandidateWindowView::HideAll() {
    599   parent_frame_->Hide();
    600   NotifyIfCandidateWindowOpenedOrClosed();
    601 }
    602 
    603 void CandidateWindowView::UpdateParentArea() {
    604   if (candidate_area_->IsShown() ||
    605       header_area_->IsShown() ||
    606       footer_area_->IsShown() ||
    607       preedit_area_->IsShown()) {
    608     ResizeAndMoveParentFrame();
    609     parent_frame_->Show();
    610   } else {
    611     parent_frame_->Hide();
    612   }
    613   NotifyIfCandidateWindowOpenedOrClosed();
    614 }
    615 
    616 void CandidateWindowView::HideLookupTable() {
    617   candidate_area_->Hide();
    618   UpdateParentArea();
    619 }
    620 
    621 void CandidateWindowView::HideAuxiliaryText() {
    622   header_area_->Hide();
    623   footer_area_->Hide();
    624   UpdateParentArea();
    625 }
    626 
    627 void CandidateWindowView::ShowAuxiliaryText() {
    628   // If candidate_area is not shown, shows auxiliary text at header_area.
    629   // We expect both header_area_ and footer_area_ contain same value.
    630   if (!candidate_area_->IsShown()) {
    631     header_area_->Show();
    632     footer_area_->Hide();
    633   } else {
    634     // If candidate_area is shown, shows auxiliary text with orientation.
    635     if (lookup_table_.orientation() == IBusLookupTable::HORIZONTAL) {
    636       header_area_->Show();
    637       footer_area_->Hide();
    638     } else {
    639       footer_area_->Show();
    640       header_area_->Hide();
    641     }
    642   }
    643   UpdateParentArea();
    644 }
    645 
    646 void CandidateWindowView::UpdateAuxiliaryText(const std::string& utf8_text) {
    647   header_area_->SetText(utf8_text);
    648   footer_area_->SetText(utf8_text);
    649   ShowAuxiliaryText();
    650 }
    651 
    652 void CandidateWindowView::HidePreeditText() {
    653   preedit_area_->Hide();
    654   UpdateParentArea();
    655 }
    656 
    657 void CandidateWindowView::ShowPreeditText() {
    658   preedit_area_->Show();
    659   UpdateParentArea();
    660 }
    661 
    662 void CandidateWindowView::UpdatePreeditText(const std::string& utf8_text) {
    663   preedit_area_->SetText(utf8_text);
    664 }
    665 
    666 void CandidateWindowView::ShowLookupTable() {
    667   if (!candidate_area_->IsShown())
    668     should_show_upper_side_ = false;
    669   candidate_area_->Show();
    670   UpdateParentArea();
    671 }
    672 
    673 void CandidateWindowView::NotifyIfCandidateWindowOpenedOrClosed() {
    674   bool is_open = IsCandidateWindowOpen();
    675   if (!was_candidate_window_open_ && is_open) {
    676     FOR_EACH_OBSERVER(Observer, observers_, OnCandidateWindowOpened());
    677   } else if (was_candidate_window_open_ && !is_open) {
    678     FOR_EACH_OBSERVER(Observer, observers_, OnCandidateWindowClosed());
    679   }
    680   was_candidate_window_open_ = is_open;
    681 }
    682 
    683 bool CandidateWindowView::ShouldUpdateCandidateViews(
    684     const IBusLookupTable& old_table,
    685     const IBusLookupTable& new_table) {
    686   return !old_table.IsEqual(new_table);
    687 }
    688 
    689 void CandidateWindowView::UpdateCandidates(
    690     const IBusLookupTable& new_lookup_table) {
    691   const bool should_update = ShouldUpdateCandidateViews(lookup_table_,
    692                                                         new_lookup_table);
    693   // Updating the candidate views is expensive. We'll skip this if possible.
    694   if (should_update) {
    695     // Initialize candidate views if necessary.
    696     MaybeInitializeCandidateViews(new_lookup_table);
    697 
    698     should_show_at_composition_head_
    699         = new_lookup_table.show_window_at_composition();
    700     // Compute the index of the current page.
    701     const int current_page_index = ComputePageIndex(new_lookup_table);
    702     if (current_page_index < 0) {
    703       return;
    704     }
    705 
    706     // Update the candidates in the current page.
    707     const size_t start_from = current_page_index * new_lookup_table.page_size();
    708 
    709     // In some cases, engines send empty shortcut labels. For instance,
    710     // ibus-mozc sends empty labels when they show suggestions. In this
    711     // case, we should not show shortcut labels.
    712     bool no_shortcut_mode = true;
    713     for (size_t i = 0; i < new_lookup_table.candidates().size(); ++i) {
    714       if (!new_lookup_table.candidates()[i].label.empty()) {
    715         no_shortcut_mode = false;
    716         break;
    717       }
    718     }
    719 
    720     for (size_t i = 0; i < candidate_views_.size(); ++i) {
    721       const size_t index_in_page = i;
    722       const size_t candidate_index = start_from + index_in_page;
    723       CandidateView* candidate_view = candidate_views_[index_in_page];
    724       // Set the shortcut text.
    725       if (no_shortcut_mode) {
    726         candidate_view->SetShortcutText(string16());
    727       } else {
    728         // At this moment, we don't use labels sent from engines for UX
    729         // reasons. First, we want to show shortcut labels in empty rows
    730         // (ex. show 6, 7, 8, ... in empty rows when the number of
    731         // candidates is 5). Second, we want to add a period after each
    732         // shortcut label when the candidate window is horizontal.
    733         candidate_view->SetShortcutText(
    734             CreateShortcutText(i, new_lookup_table));
    735       }
    736       // Set the candidate text.
    737        if (candidate_index < new_lookup_table.candidates().size()) {
    738          const IBusLookupTable::Entry& entry =
    739              new_lookup_table.candidates()[candidate_index];
    740          candidate_view->SetCandidateText(UTF8ToUTF16(entry.value));
    741          candidate_view->SetAnnotationText(UTF8ToUTF16(entry.annotation));
    742          candidate_view->SetRowEnabled(true);
    743          candidate_view->SetInfolistIcon(!entry.description_title.empty());
    744       } else {
    745         // Disable the empty row.
    746         candidate_view->SetCandidateText(string16());
    747         candidate_view->SetAnnotationText(string16());
    748         candidate_view->SetRowEnabled(false);
    749         candidate_view->SetInfolistIcon(false);
    750       }
    751     }
    752   }
    753   // Update the current lookup table. We'll use lookup_table_ from here.
    754   // Note that SelectCandidateAt() uses lookup_table_.
    755   lookup_table_.CopyFrom(new_lookup_table);
    756 
    757   // Select the current candidate in the page.
    758   if (lookup_table_.is_cursor_visible()) {
    759     if (lookup_table_.page_size()) {
    760       const int current_candidate_in_page =
    761           lookup_table_.cursor_position() % lookup_table_.page_size();
    762       SelectCandidateAt(current_candidate_in_page);
    763     }
    764   } else {
    765     // Unselect the currently selected candidate.
    766     if (0 <= selected_candidate_index_in_page_ &&
    767         static_cast<size_t>(selected_candidate_index_in_page_) <
    768         candidate_views_.size()) {
    769       candidate_views_[selected_candidate_index_in_page_]->Unselect();
    770       selected_candidate_index_in_page_ = -1;
    771     }
    772   }
    773 }
    774 
    775 void CandidateWindowView::MaybeInitializeCandidateViews(
    776     const IBusLookupTable& lookup_table) {
    777   const IBusLookupTable::Orientation orientation =
    778       lookup_table.orientation();
    779   const int page_size = lookup_table.page_size();
    780   views::View* candidate_area_contents = candidate_area_->contents();
    781 
    782   // Current column width.
    783   gfx::Size shortcut_column_size(0, 0);
    784   gfx::Size candidate_column_size(0,0);
    785   gfx::Size annotation_column_size(0, 0);
    786 
    787   // If orientation is horizontal, don't need to compute width,
    788   // because each label is left aligned.
    789   if (orientation == IBusLookupTable::VERTICAL) {
    790     const ui::NativeTheme& theme = *GetNativeTheme();
    791     shortcut_column_size = ComputeShortcutColumnSize(lookup_table, theme);
    792     candidate_column_size = ComputeCandidateColumnSize(lookup_table);
    793     annotation_column_size = ComputeAnnotationColumnSize(lookup_table, theme);
    794   }
    795 
    796   // If the requested number of views matches the number of current views, and
    797   // previous and current column width are same, just reuse these.
    798   //
    799   // Note that the early exit logic is not only useful for improving
    800   // performance, but also necessary for the horizontal candidate window
    801   // to be redrawn properly. If we get rid of the logic, the horizontal
    802   // candidate window won't get redrawn properly for some reason when
    803   // there is no size change. You can test this by removing "return" here
    804   // and type "ni" with Pinyin input method.
    805   if (static_cast<int>(candidate_views_.size()) == page_size &&
    806       lookup_table_.orientation() == orientation &&
    807       previous_shortcut_column_size_ == shortcut_column_size &&
    808       previous_candidate_column_size_ == candidate_column_size &&
    809       previous_annotation_column_size_ == annotation_column_size) {
    810     return;
    811   }
    812 
    813   // Update the previous column widths.
    814   previous_shortcut_column_size_ = shortcut_column_size;
    815   previous_candidate_column_size_ = candidate_column_size;
    816   previous_annotation_column_size_ = annotation_column_size;
    817 
    818   // Clear the existing candidate_views if any.
    819   for (size_t i = 0; i < candidate_views_.size(); ++i) {
    820     candidate_area_contents->RemoveChildView(candidate_views_[i]);
    821     // Delete the view after getting out the current message loop iteration.
    822     base::MessageLoop::current()->DeleteSoon(FROM_HERE, candidate_views_[i]);
    823   }
    824   candidate_views_.clear();
    825   selected_candidate_index_in_page_ = -1;  // Invalidates the index.
    826 
    827   views::GridLayout* layout = new views::GridLayout(candidate_area_contents);
    828   // |candidate_area_contents| owns |layout|.
    829   candidate_area_contents->SetLayoutManager(layout);
    830   // Initialize the column set.
    831   views::ColumnSet* column_set = layout->AddColumnSet(0);
    832   if (orientation == IBusLookupTable::VERTICAL) {
    833     column_set->AddColumn(views::GridLayout::FILL,
    834                           views::GridLayout::FILL,
    835                           1, views::GridLayout::USE_PREF, 0, 0);
    836   } else {
    837     for (int i = 0; i < page_size; ++i) {
    838       column_set->AddColumn(views::GridLayout::FILL,
    839                             views::GridLayout::FILL,
    840                             0, views::GridLayout::USE_PREF, 0, 0);
    841     }
    842   }
    843 
    844   // Set insets so the border of the selected candidate is drawn inside of
    845   // the border of the main candidate window, but we don't have the inset
    846   // at the top and the bottom as we have the borders of the header and
    847   // footer areas.
    848   const gfx::Insets kCandidateAreaInsets(0, 1, 0, 1);
    849   layout->SetInsets(kCandidateAreaInsets.top(),
    850                     kCandidateAreaInsets.left(),
    851                     kCandidateAreaInsets.bottom(),
    852                     kCandidateAreaInsets.right());
    853 
    854   // Use maximum height for all rows in candidate area.
    855   const int kColumnHeight = std::max(shortcut_column_size.height(),
    856                                      std::max(candidate_column_size.height(),
    857                                               annotation_column_size.height()));
    858 
    859   // Add views to the candidate area.
    860   if (orientation == IBusLookupTable::HORIZONTAL) {
    861     layout->StartRow(0, 0);
    862   }
    863 
    864   for (int i = 0; i < page_size; ++i) {
    865     CandidateView* candidate_row = new CandidateView(this, i, orientation);
    866     candidate_row->Init(shortcut_column_size.width(),
    867                         candidate_column_size.width(),
    868                         annotation_column_size.width(),
    869                         kColumnHeight);
    870     candidate_views_.push_back(candidate_row);
    871     if (orientation == IBusLookupTable::VERTICAL) {
    872       layout->StartRow(0, 0);
    873     }
    874     // |candidate_row| will be owned by |candidate_area_contents|.
    875     layout->AddView(candidate_row,
    876                     1,  // Column span.
    877                     1,  // Row span.
    878                     // Horizontal alignment.
    879                     orientation == IBusLookupTable::VERTICAL ?
    880                     views::GridLayout::FILL : views::GridLayout::CENTER,
    881                     views::GridLayout::CENTER,  // Vertical alignment.
    882                     -1,  // Preferred width, not specified.
    883                     kColumnHeight);  // Preferred height.
    884   }
    885 
    886   // Compute views size in |layout|.
    887   // If we don't call this function, GetHorizontalOffset() often
    888   // returns invalid value (returns 0), then candidate window
    889   // moves right from the correct position in ResizeAndMoveParentFrame().
    890   // TODO(nhiroki): Figure out why it returns invalid value.
    891   // It seems that the x-position of the candidate labels is not set.
    892   layout->Layout(candidate_area_contents);
    893 }
    894 
    895 bool CandidateWindowView::IsCandidateWindowOpen() const {
    896   return !should_show_at_composition_head_ &&
    897       candidate_area_->visible() && candidate_area_->IsShown();
    898 }
    899 
    900 void CandidateWindowView::SelectCandidateAt(int index_in_page) {
    901   const int current_page_index = ComputePageIndex(lookup_table_);
    902   if (current_page_index < 0) {
    903     return;
    904   }
    905 
    906   const int cursor_absolute_index =
    907       lookup_table_.page_size() * current_page_index + index_in_page;
    908   // Ignore click on out of range views.
    909   if (cursor_absolute_index < 0 ||
    910       lookup_table_.candidates().size() <=
    911       static_cast<size_t>(cursor_absolute_index)) {
    912     return;
    913   }
    914 
    915   // Unselect the currently selected candidate.
    916   if (0 <= selected_candidate_index_in_page_ &&
    917       static_cast<size_t>(selected_candidate_index_in_page_) <
    918       candidate_views_.size()) {
    919     candidate_views_[selected_candidate_index_in_page_]->Unselect();
    920   }
    921   // Remember the currently selected candidate index in the current page.
    922   selected_candidate_index_in_page_ = index_in_page;
    923 
    924   // Select the candidate specified by index_in_page.
    925   candidate_views_[index_in_page]->Select();
    926 
    927   // Update the cursor indexes in the model.
    928   lookup_table_.set_cursor_position(cursor_absolute_index);
    929 }
    930 
    931 void CandidateWindowView::OnCandidatePressed(
    932     const gfx::Point& location) {
    933   for (size_t i = 0; i < candidate_views_.size(); ++i) {
    934     gfx::Point converted_location = location;
    935     views::View::ConvertPointToTarget(this, candidate_views_[i],
    936                                       &converted_location);
    937     if (candidate_views_[i]->HitTestPoint(converted_location)) {
    938       SelectCandidateAt(i);
    939       break;
    940     }
    941   }
    942 }
    943 
    944 void CandidateWindowView::CommitCandidate() {
    945   if (!(0 <= selected_candidate_index_in_page_ &&
    946         static_cast<size_t>(selected_candidate_index_in_page_) <
    947         candidate_views_.size())) {
    948     return;  // Out of range, do nothing.
    949   }
    950 
    951   // For now, we don't distinguish left and right clicks.
    952   const int button = 1;  // Left button.
    953   const int key_modifilers = 0;
    954   FOR_EACH_OBSERVER(Observer, observers_,
    955                     OnCandidateCommitted(selected_candidate_index_in_page_,
    956                                          button,
    957                                          key_modifilers));
    958 }
    959 
    960 void CandidateWindowView::ResizeAndMoveParentFrame() {
    961   // If rendering operation comes from mozc-engine, uses mozc specific location,
    962   // otherwise lookup table is shown under the cursor.
    963   const int x = should_show_at_composition_head_?
    964       composition_head_location_.x() : cursor_location_.x();
    965   // To avoid lookup-table overlapping, uses maximum y-position of mozc specific
    966   // location and cursor location, because mozc-engine does not consider about
    967   // multi-line composition.
    968   const int y = should_show_at_composition_head_?
    969       std::max(composition_head_location_.y(), cursor_location_.y()) :
    970       cursor_location_.y();
    971   const int height = cursor_location_.height();
    972   const int horizontal_offset = GetHorizontalOffset();
    973 
    974   gfx::Rect old_bounds = parent_frame_->GetClientAreaBoundsInScreen();
    975   gfx::Rect screen_bounds = ash::Shell::GetScreen()->GetDisplayMatching(
    976       cursor_location_).work_area();
    977   // The size.
    978   gfx::Rect frame_bounds = old_bounds;
    979   frame_bounds.set_size(GetPreferredSize());
    980 
    981   // The default position.
    982   frame_bounds.set_x(x + horizontal_offset);
    983   frame_bounds.set_y(y + height);
    984 
    985   // Handle overflow at the left and the top.
    986   frame_bounds.set_x(std::max(frame_bounds.x(), screen_bounds.x()));
    987   frame_bounds.set_y(std::max(frame_bounds.y(), screen_bounds.y()));
    988 
    989   // Handle overflow at the right.
    990   const int right_overflow = frame_bounds.right() - screen_bounds.right();
    991   if (right_overflow > 0) {
    992     frame_bounds.set_x(frame_bounds.x() - right_overflow);
    993   }
    994 
    995   // Handle overflow at the bottom.
    996   const int bottom_overflow = frame_bounds.bottom() - screen_bounds.bottom();
    997 
    998   // To avoid flickering window position, the candidate window should be shown
    999   // on upper side of composition string if it was shown there.
   1000   if (should_show_upper_side_ || bottom_overflow > 0) {
   1001     frame_bounds.set_y(frame_bounds.y() - height - frame_bounds.height());
   1002     should_show_upper_side_ = true;
   1003   }
   1004 
   1005   // TODO(nona): check top_overflow here.
   1006 
   1007   // Move the window per the cursor location.
   1008   // SetBounds() is not cheap. Only call this when it is really changed.
   1009   if (frame_bounds != old_bounds)
   1010     parent_frame_->SetBounds(frame_bounds);
   1011 }
   1012 
   1013 int CandidateWindowView::GetHorizontalOffset() {
   1014   // Compute the horizontal offset if the lookup table is vertical.
   1015   if (!candidate_views_.empty() &&
   1016       lookup_table_.orientation() == IBusLookupTable::VERTICAL) {
   1017     return - candidate_views_[0]->GetCandidateLabelPosition().x();
   1018   }
   1019   return 0;
   1020 }
   1021 
   1022 void CandidateWindowView::VisibilityChanged(View* starting_from,
   1023                                             bool is_visible) {
   1024   if (is_visible) {
   1025     // If the visibility of candidate window is changed,
   1026     // we should move the frame to the right position.
   1027     ResizeAndMoveParentFrame();
   1028   }
   1029 }
   1030 
   1031 void CandidateWindowView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
   1032   // If the bounds(size) of candidate window is changed,
   1033   // we should move the frame to the right position.
   1034   View::OnBoundsChanged(previous_bounds);
   1035   ResizeAndMoveParentFrame();
   1036 }
   1037 
   1038 }  // namespace input_method
   1039 }  // namespace chromeos
   1040