Home | History | Annotate | Download | only in views
      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 
      5 #include "ui/app_list/views/search_result_view.h"
      6 
      7 #include <algorithm>
      8 
      9 #include "ui/app_list/app_list_constants.h"
     10 #include "ui/app_list/search_result.h"
     11 #include "ui/app_list/views/progress_bar_view.h"
     12 #include "ui/app_list/views/search_result_actions_view.h"
     13 #include "ui/app_list/views/search_result_list_view.h"
     14 #include "ui/gfx/canvas.h"
     15 #include "ui/gfx/font.h"
     16 #include "ui/gfx/image/image_skia_operations.h"
     17 #include "ui/gfx/render_text.h"
     18 #include "ui/views/controls/button/image_button.h"
     19 #include "ui/views/controls/image_view.h"
     20 #include "ui/views/controls/menu/menu_runner.h"
     21 
     22 namespace app_list {
     23 
     24 namespace {
     25 
     26 const int kPreferredWidth = 300;
     27 const int kPreferredHeight = 52;
     28 const int kIconPadding = 14;
     29 const int kTextTrailPadding = kIconPadding;
     30 const int kBorderSize = 1;
     31 
     32 // Extra margin at the right of the rightmost action icon.
     33 const int kActionButtonRightMargin = 8;
     34 
     35 int GetIconViewWidth() {
     36   return kListIconSize + 2 * kIconPadding;
     37 }
     38 
     39 // Creates a RenderText of given |text| and |styles|. Caller takes ownership
     40 // of returned RenderText.
     41 gfx::RenderText* CreateRenderText(const base::string16& text,
     42                                   const SearchResult::Tags& tags) {
     43   gfx::RenderText* render_text = gfx::RenderText::CreateInstance();
     44   render_text->SetText(text);
     45   render_text->SetColor(kResultDefaultTextColor);
     46 
     47   for (SearchResult::Tags::const_iterator it = tags.begin();
     48        it != tags.end();
     49        ++it) {
     50     // NONE means default style so do nothing.
     51     if (it->styles == SearchResult::Tag::NONE)
     52       continue;
     53 
     54     if (it->styles & SearchResult::Tag::MATCH)
     55       render_text->ApplyStyle(gfx::BOLD, true, it->range);
     56     if (it->styles & SearchResult::Tag::DIM)
     57       render_text->ApplyColor(kResultDimmedTextColor, it->range);
     58     else if (it->styles & SearchResult::Tag::URL)
     59       render_text->ApplyColor(kResultURLTextColor, it->range);
     60   }
     61 
     62   return render_text;
     63 }
     64 
     65 }  // namespace
     66 
     67 // static
     68 const char SearchResultView::kViewClassName[] = "ui/app_list/SearchResultView";
     69 
     70 SearchResultView::SearchResultView(SearchResultListView* list_view)
     71     : views::CustomButton(this),
     72       result_(NULL),
     73       list_view_(list_view),
     74       icon_(new views::ImageView),
     75       actions_view_(new SearchResultActionsView(this)),
     76       progress_bar_(new ProgressBarView) {
     77   icon_->set_interactive(false);
     78 
     79   AddChildView(icon_);
     80   AddChildView(actions_view_);
     81   AddChildView(progress_bar_);
     82   set_context_menu_controller(this);
     83 }
     84 
     85 SearchResultView::~SearchResultView() {
     86   ClearResultNoRepaint();
     87 }
     88 
     89 void SearchResultView::SetResult(SearchResult* result) {
     90   ClearResultNoRepaint();
     91 
     92   result_ = result;
     93   if (result_)
     94     result_->AddObserver(this);
     95 
     96   OnIconChanged();
     97   OnActionsChanged();
     98   UpdateTitleText();
     99   UpdateDetailsText();
    100   OnIsInstallingChanged();
    101   OnPercentDownloadedChanged();
    102   SchedulePaint();
    103 }
    104 
    105 void SearchResultView::ClearResultNoRepaint() {
    106   if (result_)
    107     result_->RemoveObserver(this);
    108   result_ = NULL;
    109 }
    110 
    111 void SearchResultView::ClearSelectedAction() {
    112   actions_view_->SetSelectedAction(-1);
    113 }
    114 
    115 void SearchResultView::UpdateTitleText() {
    116   if (!result_ || result_->title().empty()) {
    117     title_text_.reset();
    118     SetAccessibleName(base::string16());
    119   } else {
    120     title_text_.reset(CreateRenderText(result_->title(),
    121                                        result_->title_tags()));
    122     SetAccessibleName(result_->title());
    123   }
    124 }
    125 
    126 void SearchResultView::UpdateDetailsText() {
    127   if (!result_ || result_->details().empty()) {
    128     details_text_.reset();
    129   } else {
    130     details_text_.reset(CreateRenderText(result_->details(),
    131                                          result_->details_tags()));
    132   }
    133 }
    134 
    135 const char* SearchResultView::GetClassName() const {
    136   return kViewClassName;
    137 }
    138 
    139 gfx::Size SearchResultView::GetPreferredSize() const {
    140   return gfx::Size(kPreferredWidth, kPreferredHeight);
    141 }
    142 
    143 void SearchResultView::Layout() {
    144   gfx::Rect rect(GetContentsBounds());
    145   if (rect.IsEmpty())
    146     return;
    147 
    148   gfx::Rect icon_bounds(rect);
    149   icon_bounds.set_width(GetIconViewWidth());
    150   icon_bounds.Inset(kIconPadding, (rect.height() - kListIconSize) / 2);
    151   icon_bounds.Intersect(rect);
    152   icon_->SetBoundsRect(icon_bounds);
    153 
    154   const int max_actions_width =
    155       (rect.right() - kActionButtonRightMargin - icon_bounds.right()) / 2;
    156   int actions_width = std::min(max_actions_width,
    157                                actions_view_->GetPreferredSize().width());
    158 
    159   gfx::Rect actions_bounds(rect);
    160   actions_bounds.set_x(rect.right() - kActionButtonRightMargin - actions_width);
    161   actions_bounds.set_width(actions_width);
    162   actions_view_->SetBoundsRect(actions_bounds);
    163 
    164   const int progress_width = rect.width() / 5;
    165   const int progress_height = progress_bar_->GetPreferredSize().height();
    166   const gfx::Rect progress_bounds(
    167       rect.right() - kActionButtonRightMargin - progress_width,
    168       rect.y() + (rect.height() - progress_height) / 2,
    169       progress_width,
    170       progress_height);
    171   progress_bar_->SetBoundsRect(progress_bounds);
    172 }
    173 
    174 bool SearchResultView::OnKeyPressed(const ui::KeyEvent& event) {
    175   // |result_| could be NULL when result list is changing.
    176   if (!result_)
    177     return false;
    178 
    179   switch (event.key_code()) {
    180     case ui::VKEY_TAB: {
    181       int new_selected = actions_view_->selected_action()
    182           + (event.IsShiftDown() ? -1 : 1);
    183       actions_view_->SetSelectedAction(new_selected);
    184       return actions_view_->IsValidActionIndex(new_selected);
    185     }
    186     case ui::VKEY_RETURN: {
    187       int selected = actions_view_->selected_action();
    188       if (actions_view_->IsValidActionIndex(selected)) {
    189         OnSearchResultActionActivated(selected, event.flags());
    190       } else {
    191         list_view_->SearchResultActivated(this, event.flags());
    192       }
    193       return true;
    194     }
    195     default:
    196       break;
    197   }
    198 
    199   return false;
    200 }
    201 
    202 void SearchResultView::ChildPreferredSizeChanged(views::View* child) {
    203   Layout();
    204 }
    205 
    206 void SearchResultView::OnPaint(gfx::Canvas* canvas) {
    207   gfx::Rect rect(GetContentsBounds());
    208   if (rect.IsEmpty())
    209     return;
    210 
    211   gfx::Rect content_rect(rect);
    212   content_rect.set_height(rect.height() - kBorderSize);
    213 
    214   const bool selected = list_view_->IsResultViewSelected(this);
    215   const bool hover = state() == STATE_HOVERED || state() == STATE_PRESSED;
    216   if (selected)
    217     canvas->FillRect(content_rect, kSelectedColor);
    218   else if (hover)
    219     canvas->FillRect(content_rect, kHighlightedColor);
    220   else
    221     canvas->FillRect(content_rect, kContentsBackgroundColor);
    222 
    223   gfx::Rect border_bottom = gfx::SubtractRects(rect, content_rect);
    224   canvas->FillRect(border_bottom, kResultBorderColor);
    225 
    226   gfx::Rect text_bounds(rect);
    227   text_bounds.set_x(GetIconViewWidth());
    228   if (actions_view_->visible()) {
    229     text_bounds.set_width(
    230         rect.width() - GetIconViewWidth() - kTextTrailPadding -
    231         actions_view_->bounds().width() -
    232         (actions_view_->has_children() ? kActionButtonRightMargin : 0));
    233   } else {
    234     text_bounds.set_width(rect.width() - GetIconViewWidth() -
    235                           kTextTrailPadding - progress_bar_->bounds().width() -
    236                           kActionButtonRightMargin);
    237   }
    238   text_bounds.set_x(GetMirroredXWithWidthInView(text_bounds.x(),
    239                                                 text_bounds.width()));
    240 
    241   if (title_text_ && details_text_) {
    242     gfx::Size title_size(text_bounds.width(),
    243                          title_text_->GetStringSize().height());
    244     gfx::Size details_size(text_bounds.width(),
    245                            details_text_->GetStringSize().height());
    246     int total_height = title_size.height() + + details_size.height();
    247     int y = text_bounds.y() + (text_bounds.height() - total_height) / 2;
    248 
    249     title_text_->SetDisplayRect(gfx::Rect(gfx::Point(text_bounds.x(), y),
    250                                           title_size));
    251     title_text_->Draw(canvas);
    252 
    253     y += title_size.height();
    254     details_text_->SetDisplayRect(gfx::Rect(gfx::Point(text_bounds.x(), y),
    255                                             details_size));
    256     details_text_->Draw(canvas);
    257   } else if (title_text_) {
    258     gfx::Size title_size(text_bounds.width(),
    259                          title_text_->GetStringSize().height());
    260     gfx::Rect centered_title_rect(text_bounds);
    261     centered_title_rect.ClampToCenteredSize(title_size);
    262     title_text_->SetDisplayRect(centered_title_rect);
    263     title_text_->Draw(canvas);
    264   }
    265 }
    266 
    267 void SearchResultView::ButtonPressed(views::Button* sender,
    268                                      const ui::Event& event) {
    269   DCHECK(sender == this);
    270 
    271   list_view_->SearchResultActivated(this, event.flags());
    272 }
    273 
    274 void SearchResultView::OnIconChanged() {
    275   gfx::ImageSkia image(result_ ? result_->icon() : gfx::ImageSkia());
    276   // Note this might leave the view with an old icon. But it is needed to avoid
    277   // flash when a SearchResult's icon is loaded asynchronously. In this case, it
    278   // looks nicer to keep the stale icon for a little while on screen instead of
    279   // clearing it out. It should work correctly as long as the SearchResult does
    280   // not forget to SetIcon when it's ready.
    281   if (image.isNull())
    282     return;
    283 
    284   // Scales down big icons but leave small ones unchanged.
    285   if (image.width() > kListIconSize || image.height() > kListIconSize) {
    286     image = gfx::ImageSkiaOperations::CreateResizedImage(
    287         image,
    288         skia::ImageOperations::RESIZE_BEST,
    289         gfx::Size(kListIconSize, kListIconSize));
    290   } else {
    291     icon_->ResetImageSize();
    292   }
    293 
    294   // Set the image to an empty image before we reset the image because
    295   // since we're using the same backing store for our images, sometimes
    296   // ImageView won't detect that we have a new image set due to the pixel
    297   // buffer pointers remaining the same despite the image changing.
    298   icon_->SetImage(gfx::ImageSkia());
    299   icon_->SetImage(image);
    300 }
    301 
    302 void SearchResultView::OnActionsChanged() {
    303   actions_view_->SetActions(result_ ? result_->actions()
    304                                     : SearchResult::Actions());
    305 }
    306 
    307 void SearchResultView::OnIsInstallingChanged() {
    308   const bool is_installing = result_ && result_->is_installing();
    309   actions_view_->SetVisible(!is_installing);
    310   progress_bar_->SetVisible(is_installing);
    311 }
    312 
    313 void SearchResultView::OnPercentDownloadedChanged() {
    314   progress_bar_->SetValue(result_ ? result_->percent_downloaded() / 100.0 : 0);
    315 }
    316 
    317 void SearchResultView::OnItemInstalled() {
    318   list_view_->OnSearchResultInstalled(this);
    319 }
    320 
    321 void SearchResultView::OnItemUninstalled() {
    322   list_view_->OnSearchResultUninstalled(this);
    323 }
    324 
    325 void SearchResultView::OnSearchResultActionActivated(size_t index,
    326                                                      int event_flags) {
    327   // |result_| could be NULL when result list is changing.
    328   if (!result_)
    329     return;
    330 
    331   DCHECK_LT(index, result_->actions().size());
    332 
    333   list_view_->SearchResultActionActivated(this, index, event_flags);
    334 }
    335 
    336 void SearchResultView::ShowContextMenuForView(views::View* source,
    337                                               const gfx::Point& point,
    338                                               ui::MenuSourceType source_type) {
    339   // |result_| could be NULL when result list is changing.
    340   if (!result_)
    341     return;
    342 
    343   ui::MenuModel* menu_model = result_->GetContextMenuModel();
    344   if (!menu_model)
    345     return;
    346 
    347   context_menu_runner_.reset(
    348       new views::MenuRunner(menu_model, views::MenuRunner::HAS_MNEMONICS));
    349   if (context_menu_runner_->RunMenuAt(GetWidget(),
    350                                       NULL,
    351                                       gfx::Rect(point, gfx::Size()),
    352                                       views::MENU_ANCHOR_TOPLEFT,
    353                                       source_type) ==
    354       views::MenuRunner::MENU_DELETED)
    355     return;
    356 }
    357 
    358 }  // namespace app_list
    359