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