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 { 24 25 const int kPreferredWidth = 300; 26 const int kPreferredHeight = 52; 27 const int kIconDimension = 32; 28 const int kIconPadding = 14; 29 const int kIconViewWidth = kIconDimension + 2 * kIconPadding; 30 const int kTextTrailPadding = kIconPadding; 31 const int kBorderSize = 1; 32 33 // Extra margin at the right of the rightmost action icon. 34 const int kActionButtonRightMargin = 8; 35 36 // Creates a RenderText of given |text| and |styles|. Caller takes ownership 37 // of returned RenderText. 38 gfx::RenderText* CreateRenderText(const base::string16& text, 39 const app_list::SearchResult::Tags& tags) { 40 gfx::RenderText* render_text = gfx::RenderText::CreateInstance(); 41 render_text->SetText(text); 42 render_text->SetColor(app_list::kResultDefaultTextColor); 43 44 for (app_list::SearchResult::Tags::const_iterator it = tags.begin(); 45 it != tags.end(); 46 ++it) { 47 // NONE means default style so do nothing. 48 if (it->styles == app_list::SearchResult::Tag::NONE) 49 continue; 50 51 if (it->styles & app_list::SearchResult::Tag::MATCH) 52 render_text->ApplyStyle(gfx::BOLD, true, it->range); 53 if (it->styles & app_list::SearchResult::Tag::DIM) 54 render_text->ApplyColor(app_list::kResultDimmedTextColor, it->range); 55 else if (it->styles & app_list::SearchResult::Tag::URL) 56 render_text->ApplyColor(app_list::kResultURLTextColor, it->range); 57 } 58 59 return render_text; 60 } 61 62 } // namespace 63 64 namespace app_list { 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::UpdateTitleText() { 112 if (!result_ || result_->title().empty()) { 113 title_text_.reset(); 114 } else { 115 title_text_.reset(CreateRenderText(result_->title(), 116 result_->title_tags())); 117 } 118 } 119 120 void SearchResultView::UpdateDetailsText() { 121 if (!result_ || result_->details().empty()) { 122 details_text_.reset(); 123 } else { 124 details_text_.reset(CreateRenderText(result_->details(), 125 result_->details_tags())); 126 } 127 } 128 129 const char* SearchResultView::GetClassName() const { 130 return kViewClassName; 131 } 132 133 gfx::Size SearchResultView::GetPreferredSize() { 134 return gfx::Size(kPreferredWidth, kPreferredHeight); 135 } 136 137 void SearchResultView::Layout() { 138 gfx::Rect rect(GetContentsBounds()); 139 if (rect.IsEmpty()) 140 return; 141 142 gfx::Rect icon_bounds(rect); 143 icon_bounds.set_width(kIconViewWidth); 144 icon_bounds.Inset(kIconPadding, (rect.height() - kIconDimension) / 2); 145 icon_bounds.Intersect(rect); 146 icon_->SetBoundsRect(icon_bounds); 147 148 const int max_actions_width = 149 (rect.right() - kActionButtonRightMargin - icon_bounds.right()) / 2; 150 int actions_width = std::min(max_actions_width, 151 actions_view_->GetPreferredSize().width()); 152 153 gfx::Rect actions_bounds(rect); 154 actions_bounds.set_x(rect.right() - kActionButtonRightMargin - actions_width); 155 actions_bounds.set_width(actions_width); 156 actions_view_->SetBoundsRect(actions_bounds); 157 158 const int progress_width = rect.width() / 5; 159 const int progress_height = progress_bar_->GetPreferredSize().height(); 160 const gfx::Rect progress_bounds( 161 rect.right() - kActionButtonRightMargin - progress_width, 162 rect.y() + (rect.height() - progress_height) / 2, 163 progress_width, 164 progress_height); 165 progress_bar_->SetBoundsRect(progress_bounds); 166 } 167 168 void SearchResultView::ChildPreferredSizeChanged(views::View* child) { 169 Layout(); 170 } 171 172 void SearchResultView::OnPaint(gfx::Canvas* canvas) { 173 gfx::Rect rect(GetContentsBounds()); 174 if (rect.IsEmpty()) 175 return; 176 177 gfx::Rect content_rect(rect); 178 content_rect.set_height(rect.height() - kBorderSize); 179 180 const bool selected = list_view_->IsResultViewSelected(this); 181 const bool hover = state() == STATE_HOVERED || state() == STATE_PRESSED; 182 if (selected) 183 canvas->FillRect(content_rect, kSelectedColor); 184 else if (hover) 185 canvas->FillRect(content_rect, kHighlightedColor); 186 else 187 canvas->FillRect(content_rect, kContentsBackgroundColor); 188 189 gfx::Rect border_bottom = gfx::SubtractRects(rect, content_rect); 190 canvas->FillRect(border_bottom, kResultBorderColor); 191 192 gfx::Rect text_bounds(rect); 193 text_bounds.set_x(kIconViewWidth); 194 if (actions_view_->visible()) { 195 text_bounds.set_width( 196 rect.width() - kIconViewWidth - kTextTrailPadding - 197 actions_view_->bounds().width() - 198 (actions_view_->has_children() ? kActionButtonRightMargin : 0)); 199 } else { 200 text_bounds.set_width( 201 rect.width() - kIconViewWidth - kTextTrailPadding - 202 progress_bar_->bounds().width() - kActionButtonRightMargin); 203 } 204 text_bounds.set_x(GetMirroredXWithWidthInView(text_bounds.x(), 205 text_bounds.width())); 206 207 if (title_text_ && details_text_) { 208 gfx::Size title_size(text_bounds.width(), 209 title_text_->GetStringSize().height()); 210 gfx::Size details_size(text_bounds.width(), 211 details_text_->GetStringSize().height()); 212 int total_height = title_size.height() + + details_size.height(); 213 int y = text_bounds.y() + (text_bounds.height() - total_height) / 2; 214 215 title_text_->SetDisplayRect(gfx::Rect(gfx::Point(text_bounds.x(), y), 216 title_size)); 217 title_text_->Draw(canvas); 218 219 y += title_size.height(); 220 details_text_->SetDisplayRect(gfx::Rect(gfx::Point(text_bounds.x(), y), 221 details_size)); 222 details_text_->Draw(canvas); 223 } else if (title_text_) { 224 gfx::Size title_size(text_bounds.width(), 225 title_text_->GetStringSize().height()); 226 gfx::Rect centered_title_rect(text_bounds); 227 centered_title_rect.ClampToCenteredSize(title_size); 228 title_text_->SetDisplayRect(centered_title_rect); 229 title_text_->Draw(canvas); 230 } 231 } 232 233 void SearchResultView::ButtonPressed(views::Button* sender, 234 const ui::Event& event) { 235 DCHECK(sender == this); 236 237 delegate_->SearchResultActivated(this, event.flags()); 238 } 239 240 void SearchResultView::OnIconChanged() { 241 gfx::ImageSkia image(result_ ? result_->icon() : gfx::ImageSkia()); 242 // Note this might leave the view with an old icon. But it is needed to avoid 243 // flash when a SearchResult's icon is loaded asynchronously. In this case, it 244 // looks nicer to keep the stale icon for a little while on screen instead of 245 // clearing it out. It should work correctly as long as the SearchResult does 246 // not forget to SetIcon when it's ready. 247 if (image.isNull()) 248 return; 249 250 // Scales down big icons but leave small ones unchanged. 251 if (image.width() > kIconDimension || image.height() > kIconDimension) { 252 image = gfx::ImageSkiaOperations::CreateResizedImage( 253 image, 254 skia::ImageOperations::RESIZE_BEST, 255 gfx::Size(kIconDimension, kIconDimension)); 256 } else { 257 icon_->ResetImageSize(); 258 } 259 260 icon_->SetImage(image); 261 } 262 263 void SearchResultView::OnActionsChanged() { 264 actions_view_->SetActions(result_ ? result_->actions() 265 : SearchResult::Actions()); 266 } 267 268 void SearchResultView::OnIsInstallingChanged() { 269 const bool is_installing = result_ && result_->is_installing(); 270 actions_view_->SetVisible(!is_installing); 271 progress_bar_->SetVisible(is_installing); 272 } 273 274 void SearchResultView::OnPercentDownloadedChanged() { 275 progress_bar_->SetValue(result_->percent_downloaded() / 100.0); 276 } 277 278 void SearchResultView::OnItemInstalled() { 279 delegate_->OnSearchResultInstalled(this); 280 } 281 282 void SearchResultView::OnItemUninstalled() { 283 delegate_->OnSearchResultUninstalled(this); 284 } 285 286 void SearchResultView::OnSearchResultActionActivated(size_t index, 287 int event_flags) { 288 DCHECK(result_); 289 DCHECK_LT(index, result_->actions().size()); 290 291 delegate_->SearchResultActionActivated(this, index, event_flags); 292 } 293 294 void SearchResultView::ShowContextMenuForView(views::View* source, 295 const gfx::Point& point, 296 ui::MenuSourceType source_type) { 297 ui::MenuModel* menu_model = result_->GetContextMenuModel(); 298 if (!menu_model) 299 return; 300 301 context_menu_runner_.reset(new views::MenuRunner(menu_model)); 302 if (context_menu_runner_->RunMenuAt( 303 GetWidget(), NULL, gfx::Rect(point, gfx::Size()), 304 views::MenuItemView::TOPLEFT, source_type, 305 views::MenuRunner::HAS_MNEMONICS) == 306 views::MenuRunner::MENU_DELETED) 307 return; 308 } 309 310 } // namespace app_list 311