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 23 namespace app_list { 24 25 namespace { 26 27 const int kPreferredWidth = 300; 28 const int kPreferredHeight = 52; 29 const int kIconDimension = 32; 30 const int kIconPadding = 14; 31 const int kIconViewWidth = kIconDimension + 2 * kIconPadding; 32 const int kTextTrailPadding = kIconPadding; 33 const int kBorderSize = 1; 34 35 // Extra margin at the right of the rightmost action icon. 36 const int kActionButtonRightMargin = 8; 37 38 // Creates a RenderText of given |text| and |styles|. Caller takes ownership 39 // of returned RenderText. 40 gfx::RenderText* CreateRenderText(const base::string16& text, 41 const SearchResult::Tags& tags) { 42 gfx::RenderText* render_text = gfx::RenderText::CreateInstance(); 43 render_text->SetText(text); 44 render_text->SetColor(kResultDefaultTextColor); 45 46 for (SearchResult::Tags::const_iterator it = tags.begin(); 47 it != tags.end(); 48 ++it) { 49 // NONE means default style so do nothing. 50 if (it->styles == SearchResult::Tag::NONE) 51 continue; 52 53 if (it->styles & SearchResult::Tag::MATCH) 54 render_text->ApplyStyle(gfx::BOLD, true, it->range); 55 if (it->styles & SearchResult::Tag::DIM) 56 render_text->ApplyColor(kResultDimmedTextColor, it->range); 57 else if (it->styles & SearchResult::Tag::URL) 58 render_text->ApplyColor(kResultURLTextColor, it->range); 59 } 60 61 return render_text; 62 } 63 64 } // namespace 65 66 // static 67 const char SearchResultView::kViewClassName[] = "ui/app_list/SearchResultView"; 68 69 SearchResultView::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 86 SearchResultView::~SearchResultView() { 87 ClearResultNoRepaint(); 88 } 89 90 void 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 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() { 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(kIconViewWidth); 150 icon_bounds.Inset(kIconPadding, (rect.height() - kIconDimension) / 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 delegate_->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(kIconViewWidth); 228 if (actions_view_->visible()) { 229 text_bounds.set_width( 230 rect.width() - kIconViewWidth - kTextTrailPadding - 231 actions_view_->bounds().width() - 232 (actions_view_->has_children() ? kActionButtonRightMargin : 0)); 233 } else { 234 text_bounds.set_width( 235 rect.width() - kIconViewWidth - kTextTrailPadding - 236 progress_bar_->bounds().width() - 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 delegate_->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() > kIconDimension || image.height() > kIconDimension) { 286 image = gfx::ImageSkiaOperations::CreateResizedImage( 287 image, 288 skia::ImageOperations::RESIZE_BEST, 289 gfx::Size(kIconDimension, kIconDimension)); 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 delegate_->OnSearchResultInstalled(this); 319 } 320 321 void SearchResultView::OnItemUninstalled() { 322 delegate_->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 delegate_->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(new views::MenuRunner(menu_model)); 348 if (context_menu_runner_->RunMenuAt( 349 GetWidget(), NULL, gfx::Rect(point, gfx::Size()), 350 views::MenuItemView::TOPLEFT, source_type, 351 views::MenuRunner::HAS_MNEMONICS) == 352 views::MenuRunner::MENU_DELETED) 353 return; 354 } 355 356 } // namespace app_list 357