Views Omnibox: tolerate minor click-to-select-all dragging.
[chromium-blink-merge.git] / ui / app_list / views / search_result_view.cc
blob5ba5a2083bd08d96407a020f404a5dfeb2e89282
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"
7 #include <algorithm>
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"
23 namespace app_list {
25 namespace {
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;
35 // Extra margin at the right of the rightmost action icon.
36 const int kActionButtonRightMargin = 8;
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);
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;
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);
61 return render_text;
64 } // namespace
66 // static
67 const char SearchResultView::kViewClassName[] = "ui/app_list/SearchResultView";
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);
80 AddChildView(icon_);
81 AddChildView(actions_view_);
82 AddChildView(progress_bar_);
83 set_context_menu_controller(this);
86 SearchResultView::~SearchResultView() {
87 ClearResultNoRepaint();
90 void SearchResultView::SetResult(SearchResult* result) {
91 ClearResultNoRepaint();
93 result_ = result;
94 if (result_)
95 result_->AddObserver(this);
97 OnIconChanged();
98 OnActionsChanged();
99 UpdateTitleText();
100 UpdateDetailsText();
101 OnIsInstallingChanged();
102 SchedulePaint();
105 void SearchResultView::ClearResultNoRepaint() {
106 if (result_)
107 result_->RemoveObserver(this);
108 result_ = NULL;
111 void SearchResultView::ClearSelectedAction() {
112 actions_view_->SetSelectedAction(-1);
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());
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()));
135 const char* SearchResultView::GetClassName() const {
136 return kViewClassName;
139 gfx::Size SearchResultView::GetPreferredSize() {
140 return gfx::Size(kPreferredWidth, kPreferredHeight);
143 void SearchResultView::Layout() {
144 gfx::Rect rect(GetContentsBounds());
145 if (rect.IsEmpty())
146 return;
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);
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());
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);
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);
174 bool SearchResultView::OnKeyPressed(const ui::KeyEvent& event) {
175 // |result_| could be NULL when result list is changing.
176 if (!result_)
177 return false;
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);
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());
193 return true;
195 default:
196 break;
199 return false;
202 void SearchResultView::ChildPreferredSizeChanged(views::View* child) {
203 Layout();
206 void SearchResultView::OnPaint(gfx::Canvas* canvas) {
207 gfx::Rect rect(GetContentsBounds());
208 if (rect.IsEmpty())
209 return;
211 gfx::Rect content_rect(rect);
212 content_rect.set_height(rect.height() - kBorderSize);
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);
223 gfx::Rect border_bottom = gfx::SubtractRects(rect, content_rect);
224 canvas->FillRect(border_bottom, kResultBorderColor);
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);
238 text_bounds.set_x(GetMirroredXWithWidthInView(text_bounds.x(),
239 text_bounds.width()));
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;
249 title_text_->SetDisplayRect(gfx::Rect(gfx::Point(text_bounds.x(), y),
250 title_size));
251 title_text_->Draw(canvas);
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);
267 void SearchResultView::ButtonPressed(views::Button* sender,
268 const ui::Event& event) {
269 DCHECK(sender == this);
271 delegate_->SearchResultActivated(this, event.flags());
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;
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();
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);
302 void SearchResultView::OnActionsChanged() {
303 actions_view_->SetActions(result_ ? result_->actions()
304 : SearchResult::Actions());
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);
313 void SearchResultView::OnPercentDownloadedChanged() {
314 progress_bar_->SetValue(result_ ? result_->percent_downloaded() / 100.0 : 0);
317 void SearchResultView::OnItemInstalled() {
318 delegate_->OnSearchResultInstalled(this);
321 void SearchResultView::OnItemUninstalled() {
322 delegate_->OnSearchResultUninstalled(this);
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;
331 DCHECK_LT(index, result_->actions().size());
333 delegate_->SearchResultActionActivated(this, index, event_flags);
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;
343 ui::MenuModel* menu_model = result_->GetContextMenuModel();
344 if (!menu_model)
345 return;
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;
356 } // namespace app_list