omnibox_popup_contents_view.cc revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
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 "chrome/browser/ui/views/omnibox/omnibox_popup_contents_view.h" 6 7#include <algorithm> 8 9#include "chrome/browser/search/search.h" 10#include "chrome/browser/themes/theme_properties.h" 11#include "chrome/browser/ui/omnibox/omnibox_view.h" 12#include "chrome/browser/ui/views/location_bar/location_bar_view.h" 13#include "chrome/browser/ui/views/omnibox/omnibox_result_view.h" 14#include "chrome/browser/ui/views/omnibox/touch_omnibox_popup_contents_view.h" 15#include "grit/ui_resources.h" 16#include "ui/base/theme_provider.h" 17#include "ui/gfx/canvas.h" 18#include "ui/gfx/image/image.h" 19#include "ui/gfx/path.h" 20#include "ui/views/controls/image_view.h" 21#include "ui/views/widget/widget.h" 22#include "ui/views/window/non_client_view.h" 23 24#if defined(USE_AURA) 25#include "ui/wm/core/window_animations.h" 26#endif 27 28// This is the number of pixels in the border image interior to the actual 29// border. 30const int kBorderInterior = 6; 31 32class OmniboxPopupContentsView::AutocompletePopupWidget 33 : public views::Widget, 34 public base::SupportsWeakPtr<AutocompletePopupWidget> { 35 public: 36 AutocompletePopupWidget() {} 37 virtual ~AutocompletePopupWidget() {} 38 39 private: 40 DISALLOW_COPY_AND_ASSIGN(AutocompletePopupWidget); 41}; 42 43//////////////////////////////////////////////////////////////////////////////// 44// OmniboxPopupContentsView, public: 45 46OmniboxPopupView* OmniboxPopupContentsView::Create( 47 const gfx::FontList& font_list, 48 OmniboxView* omnibox_view, 49 OmniboxEditModel* edit_model, 50 LocationBarView* location_bar_view) { 51 OmniboxPopupContentsView* view = NULL; 52 if (ui::GetDisplayLayout() == ui::LAYOUT_TOUCH) { 53 view = new TouchOmniboxPopupContentsView( 54 font_list, omnibox_view, edit_model, location_bar_view); 55 } else { 56 view = new OmniboxPopupContentsView( 57 font_list, omnibox_view, edit_model, location_bar_view); 58 } 59 60 view->Init(); 61 return view; 62} 63 64OmniboxPopupContentsView::OmniboxPopupContentsView( 65 const gfx::FontList& font_list, 66 OmniboxView* omnibox_view, 67 OmniboxEditModel* edit_model, 68 LocationBarView* location_bar_view) 69 : model_(new OmniboxPopupModel(this, edit_model)), 70 omnibox_view_(omnibox_view), 71 location_bar_view_(location_bar_view), 72 font_list_(font_list), 73 ignore_mouse_drag_(false), 74 size_animation_(this), 75 left_margin_(0), 76 right_margin_(0), 77 outside_vertical_padding_(0) { 78 // The contents is owned by the LocationBarView. 79 set_owned_by_client(); 80 81 ui::ThemeProvider* theme = location_bar_view_->GetThemeProvider(); 82 bottom_shadow_ = theme->GetImageSkiaNamed(IDR_BUBBLE_B); 83} 84 85void OmniboxPopupContentsView::Init() { 86 // This can't be done in the constructor as at that point we aren't 87 // necessarily our final class yet, and we may have subclasses 88 // overriding CreateResultView. 89 for (size_t i = 0; i < AutocompleteResult::kMaxMatches; ++i) { 90 OmniboxResultView* result_view = CreateResultView(i, font_list_); 91 result_view->SetVisible(false); 92 AddChildViewAt(result_view, static_cast<int>(i)); 93 } 94} 95 96OmniboxPopupContentsView::~OmniboxPopupContentsView() { 97 // We don't need to do anything with |popup_| here. The OS either has already 98 // closed the window, in which case it's been deleted, or it will soon, in 99 // which case there's nothing we need to do. 100} 101 102gfx::Rect OmniboxPopupContentsView::GetPopupBounds() const { 103 if (!size_animation_.is_animating()) 104 return target_bounds_; 105 106 gfx::Rect current_frame_bounds = start_bounds_; 107 int total_height_delta = target_bounds_.height() - start_bounds_.height(); 108 // Round |current_height_delta| instead of truncating so we won't leave single 109 // white pixels at the bottom of the popup as long when animating very small 110 // height differences. 111 int current_height_delta = static_cast<int>( 112 size_animation_.GetCurrentValue() * total_height_delta - 0.5); 113 current_frame_bounds.set_height( 114 current_frame_bounds.height() + current_height_delta); 115 return current_frame_bounds; 116} 117 118void OmniboxPopupContentsView::LayoutChildren() { 119 gfx::Rect contents_rect = GetContentsBounds(); 120 121 contents_rect.Inset(left_margin_, 122 views::NonClientFrameView::kClientEdgeThickness + 123 outside_vertical_padding_, 124 right_margin_, outside_vertical_padding_); 125 int top = contents_rect.y(); 126 for (size_t i = 0; i < AutocompleteResult::kMaxMatches; ++i) { 127 View* v = child_at(i); 128 if (v->visible()) { 129 v->SetBounds(contents_rect.x(), top, contents_rect.width(), 130 v->GetPreferredSize().height()); 131 top = v->bounds().bottom(); 132 } 133 } 134} 135 136//////////////////////////////////////////////////////////////////////////////// 137// OmniboxPopupContentsView, OmniboxPopupView overrides: 138 139bool OmniboxPopupContentsView::IsOpen() const { 140 return popup_ != NULL; 141} 142 143void OmniboxPopupContentsView::InvalidateLine(size_t line) { 144 OmniboxResultView* result = result_view_at(line); 145 result->Invalidate(); 146 147 if (HasMatchAt(line) && GetMatchAtIndex(line).associated_keyword.get()) { 148 result->ShowKeyword(IsSelectedIndex(line) && 149 model_->selected_line_state() == OmniboxPopupModel::KEYWORD); 150 } 151} 152 153void OmniboxPopupContentsView::UpdatePopupAppearance() { 154 const size_t hidden_matches = model_->result().ShouldHideTopMatch() ? 1 : 0; 155 if (model_->result().size() <= hidden_matches || 156 omnibox_view_->IsImeShowingPopup()) { 157 // No matches or the IME is showing a popup window which may overlap 158 // the omnibox popup window. Close any existing popup. 159 if (popup_ != NULL) { 160 size_animation_.Stop(); 161 162 // NOTE: Do NOT use CloseNow() here, as we may be deep in a callstack 163 // triggered by the popup receiving a message (e.g. LBUTTONUP), and 164 // destroying the popup would cause us to read garbage when we unwind back 165 // to that level. 166 popup_->Close(); // This will eventually delete the popup. 167 popup_.reset(); 168 } 169 return; 170 } 171 172 // Update the match cached by each row, in the process of doing so make sure 173 // we have enough row views. 174 const size_t result_size = model_->result().size(); 175 max_match_contents_width_ = 0; 176 for (size_t i = 0; i < result_size; ++i) { 177 OmniboxResultView* view = result_view_at(i); 178 const AutocompleteMatch& match = GetMatchAtIndex(i); 179 view->SetMatch(match); 180 view->SetVisible(i >= hidden_matches); 181 if (match.type == AutocompleteMatchType::SEARCH_SUGGEST_INFINITE) { 182 max_match_contents_width_ = std::max( 183 max_match_contents_width_, view->GetMatchContentsWidth()); 184 } 185 } 186 187 for (size_t i = result_size; i < AutocompleteResult::kMaxMatches; ++i) 188 child_at(i)->SetVisible(false); 189 190 gfx::Point top_left_screen_coord; 191 int width; 192 location_bar_view_->GetOmniboxPopupPositioningInfo( 193 &top_left_screen_coord, &width, &left_margin_, &right_margin_); 194 gfx::Rect new_target_bounds(top_left_screen_coord, 195 gfx::Size(width, CalculatePopupHeight())); 196 197 // If we're animating and our target height changes, reset the animation. 198 // NOTE: If we just reset blindly on _every_ update, then when the user types 199 // rapidly we could get "stuck" trying repeatedly to animate shrinking by the 200 // last few pixels to get to one visible result. 201 if (new_target_bounds.height() != target_bounds_.height()) 202 size_animation_.Reset(); 203 target_bounds_ = new_target_bounds; 204 205 if (popup_ == NULL) { 206 gfx::NativeView popup_parent = 207 location_bar_view_->GetWidget()->GetNativeView(); 208 209 // If the popup is currently closed, we need to create it. 210 popup_ = (new AutocompletePopupWidget)->AsWeakPtr(); 211 views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); 212 params.can_activate = false; 213 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; 214 params.parent = popup_parent; 215 params.bounds = GetPopupBounds(); 216 params.context = popup_parent; 217 popup_->Init(params); 218 // Third-party software such as DigitalPersona identity verification can 219 // hook the underlying window creation methods and use SendMessage to 220 // synchronously change focus/activation, resulting in the popup being 221 // destroyed by the time control returns here. Bail out in this case to 222 // avoid a NULL dereference. 223 if (!popup_.get()) 224 return; 225#if defined(USE_AURA) 226 wm::SetWindowVisibilityAnimationTransition( 227 popup_->GetNativeView(), wm::ANIMATE_NONE); 228#endif 229 popup_->SetContentsView(this); 230 popup_->StackAbove(omnibox_view_->GetRelativeWindowForPopup()); 231 if (!popup_.get()) { 232 // For some IMEs GetRelativeWindowForPopup triggers the omnibox to lose 233 // focus, thereby closing (and destroying) the popup. 234 // TODO(sky): this won't be needed once we close the omnibox on input 235 // window showing. 236 return; 237 } 238 popup_->ShowInactive(); 239 } else { 240 // Animate the popup shrinking, but don't animate growing larger since that 241 // would make the popup feel less responsive. 242 start_bounds_ = GetWidget()->GetWindowBoundsInScreen(); 243 if (target_bounds_.height() < start_bounds_.height()) 244 size_animation_.Show(); 245 else 246 start_bounds_ = target_bounds_; 247 popup_->SetBounds(GetPopupBounds()); 248 } 249 250 Layout(); 251} 252 253gfx::Rect OmniboxPopupContentsView::GetTargetBounds() { 254 return target_bounds_; 255} 256 257void OmniboxPopupContentsView::PaintUpdatesNow() { 258 // TODO(beng): remove this from the interface. 259} 260 261void OmniboxPopupContentsView::OnDragCanceled() { 262 ignore_mouse_drag_ = true; 263} 264 265//////////////////////////////////////////////////////////////////////////////// 266// OmniboxPopupContentsView, OmniboxResultViewModel implementation: 267 268bool OmniboxPopupContentsView::IsSelectedIndex(size_t index) const { 269 return index == model_->selected_line(); 270} 271 272bool OmniboxPopupContentsView::IsHoveredIndex(size_t index) const { 273 return index == model_->hovered_line(); 274} 275 276gfx::Image OmniboxPopupContentsView::GetIconIfExtensionMatch( 277 size_t index) const { 278 if (!HasMatchAt(index)) 279 return gfx::Image(); 280 return model_->GetIconIfExtensionMatch(GetMatchAtIndex(index)); 281} 282 283//////////////////////////////////////////////////////////////////////////////// 284// OmniboxPopupContentsView, AnimationDelegate implementation: 285 286void OmniboxPopupContentsView::AnimationProgressed( 287 const gfx::Animation* animation) { 288 // We should only be running the animation when the popup is already visible. 289 DCHECK(popup_ != NULL); 290 popup_->SetBounds(GetPopupBounds()); 291} 292 293//////////////////////////////////////////////////////////////////////////////// 294// OmniboxPopupContentsView, views::View overrides: 295 296void OmniboxPopupContentsView::Layout() { 297 // Size our children to the available content area. 298 LayoutChildren(); 299 300 // We need to manually schedule a paint here since we are a layered window and 301 // won't implicitly require painting until we ask for one. 302 SchedulePaint(); 303} 304 305views::View* OmniboxPopupContentsView::GetEventHandlerForRect( 306 const gfx::Rect& rect) { 307 return this; 308} 309 310views::View* OmniboxPopupContentsView::GetTooltipHandlerForPoint( 311 const gfx::Point& point) { 312 return NULL; 313} 314 315bool OmniboxPopupContentsView::OnMousePressed( 316 const ui::MouseEvent& event) { 317 ignore_mouse_drag_ = false; // See comment on |ignore_mouse_drag_| in header. 318 if (event.IsLeftMouseButton() || event.IsMiddleMouseButton()) 319 UpdateLineEvent(event, event.IsLeftMouseButton()); 320 return true; 321} 322 323bool OmniboxPopupContentsView::OnMouseDragged( 324 const ui::MouseEvent& event) { 325 if (event.IsLeftMouseButton() || event.IsMiddleMouseButton()) 326 UpdateLineEvent(event, !ignore_mouse_drag_ && event.IsLeftMouseButton()); 327 return true; 328} 329 330void OmniboxPopupContentsView::OnMouseReleased( 331 const ui::MouseEvent& event) { 332 if (ignore_mouse_drag_) { 333 OnMouseCaptureLost(); 334 return; 335 } 336 337 if (event.IsOnlyMiddleMouseButton() || event.IsOnlyLeftMouseButton()) { 338 OpenSelectedLine(event, event.IsOnlyLeftMouseButton() ? CURRENT_TAB : 339 NEW_BACKGROUND_TAB); 340 } 341} 342 343void OmniboxPopupContentsView::OnMouseCaptureLost() { 344 ignore_mouse_drag_ = false; 345} 346 347void OmniboxPopupContentsView::OnMouseMoved( 348 const ui::MouseEvent& event) { 349 model_->SetHoveredLine(GetIndexForPoint(event.location())); 350} 351 352void OmniboxPopupContentsView::OnMouseEntered( 353 const ui::MouseEvent& event) { 354 model_->SetHoveredLine(GetIndexForPoint(event.location())); 355} 356 357void OmniboxPopupContentsView::OnMouseExited( 358 const ui::MouseEvent& event) { 359 model_->SetHoveredLine(OmniboxPopupModel::kNoMatch); 360} 361 362void OmniboxPopupContentsView::OnGestureEvent(ui::GestureEvent* event) { 363 switch (event->type()) { 364 case ui::ET_GESTURE_TAP_DOWN: 365 case ui::ET_GESTURE_SCROLL_BEGIN: 366 case ui::ET_GESTURE_SCROLL_UPDATE: 367 UpdateLineEvent(*event, true); 368 break; 369 case ui::ET_GESTURE_TAP: 370 case ui::ET_GESTURE_SCROLL_END: 371 OpenSelectedLine(*event, CURRENT_TAB); 372 break; 373 default: 374 return; 375 } 376 event->SetHandled(); 377} 378 379//////////////////////////////////////////////////////////////////////////////// 380// OmniboxPopupContentsView, protected: 381 382void OmniboxPopupContentsView::PaintResultViews(gfx::Canvas* canvas) { 383 canvas->DrawColor(result_view_at(0)->GetColor( 384 OmniboxResultView::NORMAL, OmniboxResultView::BACKGROUND)); 385 View::PaintChildren(canvas); 386} 387 388int OmniboxPopupContentsView::CalculatePopupHeight() { 389 DCHECK_GE(static_cast<size_t>(child_count()), model_->result().size()); 390 int popup_height = 0; 391 for (size_t i = model_->result().ShouldHideTopMatch() ? 1 : 0; 392 i < model_->result().size(); ++i) 393 popup_height += child_at(i)->GetPreferredSize().height(); 394 395 // Add enough space on the top and bottom so it looks like there is the same 396 // amount of space between the text and the popup border as there is in the 397 // interior between each row of text. 398 // 399 // Discovering the exact amount of leading and padding around the font is 400 // a bit tricky and platform-specific, but this computation seems to work in 401 // practice. 402 OmniboxResultView* result_view = result_view_at(0); 403 outside_vertical_padding_ = 404 (result_view->GetPreferredSize().height() - 405 result_view->GetTextHeight()); 406 407 return popup_height + 408 views::NonClientFrameView::kClientEdgeThickness + // Top border. 409 outside_vertical_padding_ * 2 + // Padding. 410 bottom_shadow_->height() - kBorderInterior; // Bottom border. 411} 412 413OmniboxResultView* OmniboxPopupContentsView::CreateResultView( 414 int model_index, 415 const gfx::FontList& font_list) { 416 return new OmniboxResultView(this, model_index, location_bar_view_, 417 font_list); 418} 419 420//////////////////////////////////////////////////////////////////////////////// 421// OmniboxPopupContentsView, views::View overrides, protected: 422 423void OmniboxPopupContentsView::OnPaint(gfx::Canvas* canvas) { 424 gfx::Rect contents_bounds = GetContentsBounds(); 425 contents_bounds.set_height( 426 contents_bounds.height() - bottom_shadow_->height() + kBorderInterior); 427 428 gfx::Path path; 429 MakeContentsPath(&path, contents_bounds); 430 canvas->Save(); 431 canvas->sk_canvas()->clipPath(path, 432 SkRegion::kIntersect_Op, 433 true /* doAntialias */); 434 PaintResultViews(canvas); 435 canvas->Restore(); 436 437 // Top border. 438 canvas->FillRect( 439 gfx::Rect(0, 0, width(), views::NonClientFrameView::kClientEdgeThickness), 440 ThemeProperties::GetDefaultColor( 441 ThemeProperties::COLOR_TOOLBAR_SEPARATOR)); 442 443 // Bottom border. 444 canvas->TileImageInt(*bottom_shadow_, 0, height() - bottom_shadow_->height(), 445 width(), bottom_shadow_->height()); 446} 447 448void OmniboxPopupContentsView::PaintChildren(gfx::Canvas* canvas) { 449 // We paint our children inside OnPaint(). 450} 451 452//////////////////////////////////////////////////////////////////////////////// 453// OmniboxPopupContentsView, private: 454 455bool OmniboxPopupContentsView::HasMatchAt(size_t index) const { 456 return index < model_->result().size(); 457} 458 459const AutocompleteMatch& OmniboxPopupContentsView::GetMatchAtIndex( 460 size_t index) const { 461 return model_->result().match_at(index); 462} 463 464void OmniboxPopupContentsView::MakeContentsPath( 465 gfx::Path* path, 466 const gfx::Rect& bounding_rect) { 467 SkRect rect; 468 rect.set(SkIntToScalar(bounding_rect.x()), 469 SkIntToScalar(bounding_rect.y()), 470 SkIntToScalar(bounding_rect.right()), 471 SkIntToScalar(bounding_rect.bottom())); 472 path->addRect(rect); 473} 474 475size_t OmniboxPopupContentsView::GetIndexForPoint( 476 const gfx::Point& point) { 477 if (!HitTestPoint(point)) 478 return OmniboxPopupModel::kNoMatch; 479 480 int nb_match = model_->result().size(); 481 DCHECK(nb_match <= child_count()); 482 for (int i = 0; i < nb_match; ++i) { 483 views::View* child = child_at(i); 484 gfx::Point point_in_child_coords(point); 485 View::ConvertPointToTarget(this, child, &point_in_child_coords); 486 if (child->visible() && child->HitTestPoint(point_in_child_coords)) 487 return i; 488 } 489 return OmniboxPopupModel::kNoMatch; 490} 491 492void OmniboxPopupContentsView::UpdateLineEvent( 493 const ui::LocatedEvent& event, 494 bool should_set_selected_line) { 495 size_t index = GetIndexForPoint(event.location()); 496 model_->SetHoveredLine(index); 497 if (HasMatchAt(index) && should_set_selected_line) 498 model_->SetSelectedLine(index, false, false); 499} 500 501void OmniboxPopupContentsView::OpenSelectedLine( 502 const ui::LocatedEvent& event, 503 WindowOpenDisposition disposition) { 504 size_t index = GetIndexForPoint(event.location()); 505 if (!HasMatchAt(index)) 506 return; 507 omnibox_view_->OpenMatch(model_->result().match_at(index), disposition, 508 GURL(), base::string16(), index); 509} 510 511OmniboxResultView* OmniboxPopupContentsView::result_view_at(size_t i) { 512 return static_cast<OmniboxResultView*>(child_at(static_cast<int>(i))); 513} 514