• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
GetIconViewWidth()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.
CreateRenderText(const base::string16 & text,const SearchResult::Tags & tags)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 
SearchResultView(SearchResultListView * list_view)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 
~SearchResultView()85 SearchResultView::~SearchResultView() {
86   ClearResultNoRepaint();
87 }
88 
SetResult(SearchResult * result)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 
ClearResultNoRepaint()105 void SearchResultView::ClearResultNoRepaint() {
106   if (result_)
107     result_->RemoveObserver(this);
108   result_ = NULL;
109 }
110 
ClearSelectedAction()111 void SearchResultView::ClearSelectedAction() {
112   actions_view_->SetSelectedAction(-1);
113 }
114 
UpdateTitleText()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 
UpdateDetailsText()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 
GetClassName() const135 const char* SearchResultView::GetClassName() const {
136   return kViewClassName;
137 }
138 
GetPreferredSize() const139 gfx::Size SearchResultView::GetPreferredSize() const {
140   return gfx::Size(kPreferredWidth, kPreferredHeight);
141 }
142 
Layout()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 
OnKeyPressed(const ui::KeyEvent & event)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 
ChildPreferredSizeChanged(views::View * child)202 void SearchResultView::ChildPreferredSizeChanged(views::View* child) {
203   Layout();
204 }
205 
OnPaint(gfx::Canvas * canvas)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 
ButtonPressed(views::Button * sender,const ui::Event & event)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 
OnIconChanged()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 
OnActionsChanged()302 void SearchResultView::OnActionsChanged() {
303   actions_view_->SetActions(result_ ? result_->actions()
304                                     : SearchResult::Actions());
305 }
306 
OnIsInstallingChanged()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 
OnPercentDownloadedChanged()313 void SearchResultView::OnPercentDownloadedChanged() {
314   progress_bar_->SetValue(result_ ? result_->percent_downloaded() / 100.0 : 0);
315 }
316 
OnItemInstalled()317 void SearchResultView::OnItemInstalled() {
318   list_view_->OnSearchResultInstalled(this);
319 }
320 
OnItemUninstalled()321 void SearchResultView::OnItemUninstalled() {
322   list_view_->OnSearchResultUninstalled(this);
323 }
324 
OnSearchResultActionActivated(size_t index,int event_flags)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 
ShowContextMenuForView(views::View * source,const gfx::Point & point,ui::MenuSourceType source_type)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