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