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