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