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"
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"
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();
55 // NONE means default style so do nothing.
56 if (it
->styles
== SearchResult::Tag::NONE
)
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
);
73 const char SearchResultView::kViewClassName
[] = "ui/app_list/SearchResultView";
75 SearchResultView::SearchResultView(SearchResultListView
* list_view
)
76 : views::CustomButton(this),
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);
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();
103 result_
->AddObserver(this);
106 OnBadgeIconChanged();
110 OnIsInstallingChanged();
111 OnPercentDownloadedChanged();
115 void SearchResultView::ClearResultNoRepaint() {
117 result_
->RemoveObserver(this);
121 void SearchResultView::ClearSelectedAction() {
122 actions_view_
->SetSelectedAction(-1);
125 void SearchResultView::UpdateTitleText() {
126 if (!result_
|| result_
->title().empty()) {
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();
140 details_text_
.reset(CreateRenderText(result_
->details(),
141 result_
->details_tags()));
144 UpdateAccessibleName();
147 base::string16
SearchResultView::ComputeAccessibleName() const {
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());
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
,
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,
208 progress_bar_
->SetBoundsRect(progress_bounds
);
211 bool SearchResultView::OnKeyPressed(const ui::KeyEvent
& event
) {
212 // |result_| could be NULL when result list is changing.
216 switch (event
.key_code()) {
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());
228 list_view_
->SearchResultActivated(this, event
.flags());
239 void SearchResultView::ChildPreferredSizeChanged(views::View
* child
) {
243 void SearchResultView::OnPaint(gfx::Canvas
* canvas
) {
244 gfx::Rect
rect(GetContentsBounds());
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).
262 canvas
->FillRect(content_rect
, kSelectedColor
);
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));
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
),
302 title_text_
->Draw(canvas
);
304 y
+= title_size
.height();
305 details_text_
->SetDisplayRect(gfx::Rect(gfx::Point(text_bounds
.x(), y
),
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.
335 SetIconImage(image
, icon_
, kListIconSize
);
338 void SearchResultView::OnBadgeIconChanged() {
339 const gfx::ImageSkia
image(result_
? result_
->badge_icon()
341 if (image
.isNull()) {
342 badge_icon_
->SetVisible(false);
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
) {
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
));
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
,
394 // |result_| could be NULL when result list is changing.
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.
410 ui::MenuModel
* menu_model
= result_
->GetContextMenuModel();
414 context_menu_runner_
.reset(
415 new views::MenuRunner(menu_model
, views::MenuRunner::HAS_MNEMONICS
));
416 if (context_menu_runner_
->RunMenuAt(GetWidget(),
418 gfx::Rect(point
, gfx::Size()),
419 views::MENU_ANCHOR_TOPLEFT
,
421 views::MenuRunner::MENU_DELETED
)
425 } // namespace app_list