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