Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / ui / app_list / views / search_result_view.cc
blob909a05f2200c3c676fc3f2fca07213426682ef19
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.
5 #include "ui/app_list/views/search_result_view.h"
7 #include <algorithm>
9 #include "base/strings/utf_string_conversions.h"
10 #include "ui/app_list/app_list_constants.h"
11 #include "ui/app_list/app_list_switches.h"
12 #include "ui/app_list/search_result.h"
13 #include "ui/app_list/views/progress_bar_view.h"
14 #include "ui/app_list/views/search_result_actions_view.h"
15 #include "ui/app_list/views/search_result_list_view.h"
16 #include "ui/gfx/canvas.h"
17 #include "ui/gfx/font.h"
18 #include "ui/gfx/image/image_skia_operations.h"
19 #include "ui/gfx/render_text.h"
20 #include "ui/views/controls/button/image_button.h"
21 #include "ui/views/controls/image_view.h"
22 #include "ui/views/controls/menu/menu_runner.h"
24 namespace app_list {
26 namespace {
28 const int kPreferredWidth = 300;
29 const int kPreferredHeight = 56;
30 const int kIconLeftPadding = 16;
31 const int kIconRightPadding = 24;
32 const int kTextTrailPadding = 16;
33 const int kSeparatorPadding = 62;
34 const int kBorderSize = 1;
35 const SkColor kSeparatorColor = SkColorSetRGB(0xE1, 0xE1, 0xE1);
37 // Extra margin at the right of the rightmost action icon.
38 const int kActionButtonRightMargin = 8;
40 int GetIconViewWidth() {
41 return kListIconSize + kIconLeftPadding + kIconRightPadding;
44 // Creates a RenderText of given |text| and |styles|. Caller takes ownership
45 // of returned RenderText.
46 gfx::RenderText* CreateRenderText(const base::string16& text,
47 const SearchResult::Tags& tags) {
48 gfx::RenderText* render_text = gfx::RenderText::CreateInstance();
49 render_text->SetText(text);
50 render_text->SetColor(kResultDefaultTextColor);
52 for (SearchResult::Tags::const_iterator it = tags.begin();
53 it != tags.end();
54 ++it) {
55 // NONE means default style so do nothing.
56 if (it->styles == SearchResult::Tag::NONE)
57 continue;
59 if (it->styles & SearchResult::Tag::MATCH)
60 render_text->ApplyStyle(gfx::BOLD, true, it->range);
61 if (it->styles & SearchResult::Tag::DIM)
62 render_text->ApplyColor(kResultDimmedTextColor, it->range);
63 else if (it->styles & SearchResult::Tag::URL)
64 render_text->ApplyColor(kResultURLTextColor, it->range);
67 return render_text;
70 } // namespace
72 // static
73 const char SearchResultView::kViewClassName[] = "ui/app_list/SearchResultView";
75 SearchResultView::SearchResultView(SearchResultListView* list_view)
76 : views::CustomButton(this),
77 result_(NULL),
78 is_last_result_(false),
79 list_view_(list_view),
80 icon_(new views::ImageView),
81 badge_icon_(new views::ImageView),
82 actions_view_(new SearchResultActionsView(this)),
83 progress_bar_(new ProgressBarView) {
84 icon_->set_interactive(false);
85 badge_icon_->set_interactive(false);
87 AddChildView(icon_);
88 AddChildView(badge_icon_);
89 AddChildView(actions_view_);
90 AddChildView(progress_bar_);
91 set_context_menu_controller(this);
94 SearchResultView::~SearchResultView() {
95 ClearResultNoRepaint();
98 void SearchResultView::SetResult(SearchResult* result) {
99 ClearResultNoRepaint();
101 result_ = result;
102 if (result_)
103 result_->AddObserver(this);
105 OnIconChanged();
106 OnBadgeIconChanged();
107 OnActionsChanged();
108 UpdateTitleText();
109 UpdateDetailsText();
110 OnIsInstallingChanged();
111 OnPercentDownloadedChanged();
112 SchedulePaint();
115 void SearchResultView::ClearResultNoRepaint() {
116 if (result_)
117 result_->RemoveObserver(this);
118 result_ = NULL;
121 void SearchResultView::ClearSelectedAction() {
122 actions_view_->SetSelectedAction(-1);
125 void SearchResultView::UpdateTitleText() {
126 if (!result_ || result_->title().empty()) {
127 title_text_.reset();
128 } else {
129 title_text_.reset(CreateRenderText(result_->title(),
130 result_->title_tags()));
133 UpdateAccessibleName();
136 void SearchResultView::UpdateDetailsText() {
137 if (!result_ || result_->details().empty()) {
138 details_text_.reset();
139 } else {
140 details_text_.reset(CreateRenderText(result_->details(),
141 result_->details_tags()));
144 UpdateAccessibleName();
147 base::string16 SearchResultView::ComputeAccessibleName() const {
148 if (!result_)
149 return base::string16();
151 base::string16 accessible_name = result_->title();
152 if (!result_->title().empty() && !result_->details().empty())
153 accessible_name += base::ASCIIToUTF16(", ");
154 accessible_name += result_->details();
156 return accessible_name;
159 void SearchResultView::UpdateAccessibleName() {
160 SetAccessibleName(ComputeAccessibleName());
163 const char* SearchResultView::GetClassName() const {
164 return kViewClassName;
167 gfx::Size SearchResultView::GetPreferredSize() const {
168 return gfx::Size(kPreferredWidth, kPreferredHeight);
171 void SearchResultView::Layout() {
172 gfx::Rect rect(GetContentsBounds());
173 if (rect.IsEmpty())
174 return;
176 gfx::Rect icon_bounds(rect);
177 icon_bounds.set_width(GetIconViewWidth());
178 const int top_bottom_padding = (rect.height() - kListIconSize) / 2;
179 icon_bounds.Inset(kIconLeftPadding, top_bottom_padding, kIconRightPadding,
180 top_bottom_padding);
181 icon_bounds.Intersect(rect);
182 icon_->SetBoundsRect(icon_bounds);
184 gfx::Rect badge_icon_bounds(
185 icon_bounds.right() - kListBadgeIconSize + kListBadgeIconOffsetX,
186 icon_bounds.bottom() - kListBadgeIconSize + kListBadgeIconOffsetY,
187 kListBadgeIconSize, kListBadgeIconSize);
188 badge_icon_bounds.Intersect(rect);
189 badge_icon_->SetBoundsRect(badge_icon_bounds);
191 const int max_actions_width =
192 (rect.right() - kActionButtonRightMargin - icon_bounds.right()) / 2;
193 int actions_width = std::min(max_actions_width,
194 actions_view_->GetPreferredSize().width());
196 gfx::Rect actions_bounds(rect);
197 actions_bounds.set_x(rect.right() - kActionButtonRightMargin - actions_width);
198 actions_bounds.set_width(actions_width);
199 actions_view_->SetBoundsRect(actions_bounds);
201 const int progress_width = rect.width() / 5;
202 const int progress_height = progress_bar_->GetPreferredSize().height();
203 const gfx::Rect progress_bounds(
204 rect.right() - kActionButtonRightMargin - progress_width,
205 rect.y() + (rect.height() - progress_height) / 2,
206 progress_width,
207 progress_height);
208 progress_bar_->SetBoundsRect(progress_bounds);
211 bool SearchResultView::OnKeyPressed(const ui::KeyEvent& event) {
212 // |result_| could be NULL when result list is changing.
213 if (!result_)
214 return false;
216 switch (event.key_code()) {
217 case ui::VKEY_TAB: {
218 int new_selected = actions_view_->selected_action()
219 + (event.IsShiftDown() ? -1 : 1);
220 actions_view_->SetSelectedAction(new_selected);
221 return actions_view_->IsValidActionIndex(new_selected);
223 case ui::VKEY_RETURN: {
224 int selected = actions_view_->selected_action();
225 if (actions_view_->IsValidActionIndex(selected)) {
226 OnSearchResultActionActivated(selected, event.flags());
227 } else {
228 list_view_->SearchResultActivated(this, event.flags());
230 return true;
232 default:
233 break;
236 return false;
239 void SearchResultView::ChildPreferredSizeChanged(views::View* child) {
240 Layout();
243 void SearchResultView::OnPaint(gfx::Canvas* canvas) {
244 gfx::Rect rect(GetContentsBounds());
245 if (rect.IsEmpty())
246 return;
248 gfx::Rect content_rect(rect);
249 if (!switches::IsExperimentalAppListEnabled())
250 content_rect.set_height(rect.height() - kBorderSize);
252 const bool selected = list_view_->IsResultViewSelected(this);
253 const bool hover = state() == STATE_HOVERED || state() == STATE_PRESSED;
255 canvas->FillRect(content_rect, switches::IsExperimentalAppListEnabled()
256 ? kCardBackgroundColor
257 : kContentsBackgroundColor);
259 // Possibly call FillRect a second time (these colours are partially
260 // transparent, so the previous FillRect is not redundant).
261 if (selected)
262 canvas->FillRect(content_rect, kSelectedColor);
263 else if (hover)
264 canvas->FillRect(content_rect, kHighlightedColor);
266 if (switches::IsExperimentalAppListEnabled() && !is_last_result_) {
267 gfx::Rect line_rect = content_rect;
268 line_rect.set_height(kBorderSize);
269 line_rect.set_y(content_rect.bottom() - kBorderSize);
270 line_rect.set_x(kSeparatorPadding);
271 canvas->FillRect(line_rect, kSeparatorColor);
274 gfx::Rect border_bottom = gfx::SubtractRects(rect, content_rect);
275 canvas->FillRect(border_bottom, kResultBorderColor);
277 gfx::Rect text_bounds(rect);
278 text_bounds.set_x(GetIconViewWidth());
279 if (actions_view_->visible()) {
280 text_bounds.set_width(
281 rect.width() - GetIconViewWidth() - kTextTrailPadding -
282 actions_view_->bounds().width() -
283 (actions_view_->has_children() ? kActionButtonRightMargin : 0));
284 } else {
285 text_bounds.set_width(rect.width() - GetIconViewWidth() -
286 kTextTrailPadding - progress_bar_->bounds().width() -
287 kActionButtonRightMargin);
289 text_bounds.set_x(GetMirroredXWithWidthInView(text_bounds.x(),
290 text_bounds.width()));
292 if (title_text_ && details_text_) {
293 gfx::Size title_size(text_bounds.width(),
294 title_text_->GetStringSize().height());
295 gfx::Size details_size(text_bounds.width(),
296 details_text_->GetStringSize().height());
297 int total_height = title_size.height() + + details_size.height();
298 int y = text_bounds.y() + (text_bounds.height() - total_height) / 2;
300 title_text_->SetDisplayRect(gfx::Rect(gfx::Point(text_bounds.x(), y),
301 title_size));
302 title_text_->Draw(canvas);
304 y += title_size.height();
305 details_text_->SetDisplayRect(gfx::Rect(gfx::Point(text_bounds.x(), y),
306 details_size));
307 details_text_->Draw(canvas);
308 } else if (title_text_) {
309 gfx::Size title_size(text_bounds.width(),
310 title_text_->GetStringSize().height());
311 gfx::Rect centered_title_rect(text_bounds);
312 centered_title_rect.ClampToCenteredSize(title_size);
313 title_text_->SetDisplayRect(centered_title_rect);
314 title_text_->Draw(canvas);
318 void SearchResultView::ButtonPressed(views::Button* sender,
319 const ui::Event& event) {
320 DCHECK(sender == this);
322 list_view_->SearchResultActivated(this, event.flags());
325 void SearchResultView::OnIconChanged() {
326 const gfx::ImageSkia image(result_ ? result_->icon() : gfx::ImageSkia());
327 // Note this might leave the view with an old icon. But it is needed to avoid
328 // flash when a SearchResult's icon is loaded asynchronously. In this case, it
329 // looks nicer to keep the stale icon for a little while on screen instead of
330 // clearing it out. It should work correctly as long as the SearchResult does
331 // not forget to SetIcon when it's ready.
332 if (image.isNull())
333 return;
335 SetIconImage(image, icon_, kListIconSize);
338 void SearchResultView::OnBadgeIconChanged() {
339 const gfx::ImageSkia image(result_ ? result_->badge_icon()
340 : gfx::ImageSkia());
341 if (image.isNull()) {
342 badge_icon_->SetVisible(false);
343 return;
346 SetIconImage(image, badge_icon_, kListBadgeIconSize);
347 badge_icon_->SetVisible(true);
350 void SearchResultView::SetIconImage(const gfx::ImageSkia& source,
351 views::ImageView* const icon,
352 const int icon_dimension) {
353 // Copy.
354 gfx::ImageSkia image(source);
356 // Scales down big icons but leave small ones unchanged.
357 if (image.width() > icon_dimension || image.height() > icon_dimension) {
358 image = gfx::ImageSkiaOperations::CreateResizedImage(
359 image, skia::ImageOperations::RESIZE_BEST,
360 gfx::Size(icon_dimension, icon_dimension));
361 } else {
362 icon->ResetImageSize();
365 // Set the image to an empty image before we reset the image because
366 // since we're using the same backing store for our images, sometimes
367 // ImageView won't detect that we have a new image set due to the pixel
368 // buffer pointers remaining the same despite the image changing.
369 icon->SetImage(gfx::ImageSkia());
370 icon->SetImage(image);
373 void SearchResultView::OnActionsChanged() {
374 actions_view_->SetActions(result_ ? result_->actions()
375 : SearchResult::Actions());
378 void SearchResultView::OnIsInstallingChanged() {
379 const bool is_installing = result_ && result_->is_installing();
380 actions_view_->SetVisible(!is_installing);
381 progress_bar_->SetVisible(is_installing);
384 void SearchResultView::OnPercentDownloadedChanged() {
385 progress_bar_->SetValue(result_ ? result_->percent_downloaded() / 100.0 : 0);
388 void SearchResultView::OnItemInstalled() {
389 list_view_->OnSearchResultInstalled(this);
392 void SearchResultView::OnSearchResultActionActivated(size_t index,
393 int event_flags) {
394 // |result_| could be NULL when result list is changing.
395 if (!result_)
396 return;
398 DCHECK_LT(index, result_->actions().size());
400 list_view_->SearchResultActionActivated(this, index, event_flags);
403 void SearchResultView::ShowContextMenuForView(views::View* source,
404 const gfx::Point& point,
405 ui::MenuSourceType source_type) {
406 // |result_| could be NULL when result list is changing.
407 if (!result_)
408 return;
410 ui::MenuModel* menu_model = result_->GetContextMenuModel();
411 if (!menu_model)
412 return;
414 context_menu_runner_.reset(
415 new views::MenuRunner(menu_model, views::MenuRunner::HAS_MNEMONICS));
416 if (context_menu_runner_->RunMenuAt(GetWidget(),
417 NULL,
418 gfx::Rect(point, gfx::Size()),
419 views::MENU_ANCHOR_TOPLEFT,
420 source_type) ==
421 views::MenuRunner::MENU_DELETED)
422 return;
425 } // namespace app_list