| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ios/chrome/browser/ui/omnibox/omnibox_popup_view_ios.h" |
| |
| #import <QuartzCore/QuartzCore.h> |
| |
| #include "base/logging.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/threading/sequenced_worker_pool.h" |
| #import "components/image_fetcher/ios/ios_image_data_fetcher_wrapper.h" |
| #include "components/omnibox/browser/autocomplete_match.h" |
| #include "components/omnibox/browser/omnibox_edit_model.h" |
| #include "components/omnibox/browser/omnibox_popup_model.h" |
| #include "components/open_from_clipboard/clipboard_recent_content.h" |
| #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| #import "ios/chrome/browser/experimental_flags.h" |
| #import "ios/chrome/browser/ui/omnibox/omnibox_popup_material_view_controller.h" |
| #import "ios/chrome/browser/ui/omnibox/omnibox_popup_positioner.h" |
| #include "ios/chrome/browser/ui/omnibox/omnibox_util.h" |
| #include "ios/chrome/browser/ui/omnibox/omnibox_view_ios.h" |
| #include "ios/chrome/browser/ui/ui_util.h" |
| #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| #include "ios/chrome/grit/ios_theme_resources.h" |
| #include "ios/web/public/web_thread.h" |
| #include "net/url_request/url_request_context_getter.h" |
| #include "ui/gfx/geometry/rect.h" |
| |
| namespace { |
| const CGFloat kExpandAnimationDuration = 0.1; |
| const CGFloat kCollapseAnimationDuration = 0.05; |
| const CGFloat kWhiteBackgroundHeight = 74; |
| NS_INLINE CGFloat ShadowHeight() { |
| return IsIPadIdiom() ? 10 : 0; |
| } |
| } // namespace |
| |
| using base::UserMetricsAction; |
| |
| OmniboxPopupViewIOS::OmniboxPopupViewIOS(OmniboxViewIOS* edit_view, |
| OmniboxEditModel* edit_model, |
| id<OmniboxPopupPositioner> positioner) |
| : model_(new OmniboxPopupModel(this, edit_model)), |
| edit_view_(edit_view), |
| positioner_(positioner), |
| is_open_(false) { |
| DCHECK(edit_view); |
| DCHECK(edit_model); |
| |
| std::unique_ptr<image_fetcher::IOSImageDataFetcherWrapper> imageFetcher = |
| base::MakeUnique<image_fetcher::IOSImageDataFetcherWrapper>( |
| edit_view->browser_state()->GetRequestContext(), |
| web::WebThread::GetBlockingPool()); |
| |
| popup_controller_.reset([[OmniboxPopupMaterialViewController alloc] |
| initWithPopupView:this |
| withFetcher:std::move(imageFetcher)]); |
| [popup_controller_ setIncognito:edit_view->browser_state()->IsOffTheRecord()]; |
| popupView_.reset([[UIView alloc] initWithFrame:CGRectZero]); |
| [popupView_ setClipsToBounds:YES]; |
| CALayer* popupLayer = [popupView_ layer]; |
| // Adjust popupView_'s anchor point and height so that it animates down |
| // from the top when it appears. |
| popupLayer.anchorPoint = CGPointMake(0.5, 0); |
| UIView* popupControllerView = [popup_controller_ view]; |
| CGRect popupControllerFrame = popupControllerView.frame; |
| popupControllerFrame.origin = CGPointZero; |
| popupControllerView.frame = popupControllerFrame; |
| [popupView_ addSubview:popupControllerView]; |
| if (IsIPadIdiom()) { |
| [popupView_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth]; |
| base::scoped_nsobject<UIImageView> shadowView([[UIImageView alloc] |
| initWithImage:NativeImage(IDR_IOS_TOOLBAR_SHADOW_FULL_BLEED)]); |
| [shadowView setUserInteractionEnabled:NO]; |
| [shadowView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| [popupView_ addSubview:shadowView]; |
| |
| // Add constraints to position |shadowView| at the bottom of |popupView_| |
| // with the same width as |popupView_|. |
| NSDictionary* views = NSDictionaryOfVariableBindings(shadowView); |
| [popupView_ |
| addConstraints:[NSLayoutConstraint |
| constraintsWithVisualFormat:@"H:|[shadowView]|" |
| options:0 |
| metrics:nil |
| views:views]]; |
| [popupView_ addConstraint:[NSLayoutConstraint |
| constraintWithItem:shadowView |
| attribute:NSLayoutAttributeBottom |
| relatedBy:NSLayoutRelationEqual |
| toItem:popupView_ |
| attribute:NSLayoutAttributeBottom |
| multiplier:1 |
| constant:0]]; |
| } else { |
| // Add a white background to prevent seing the logo scroll through the |
| // omnibox. |
| base::scoped_nsobject<UIView> whiteBackground( |
| [[UIView alloc] initWithFrame:CGRectZero]); |
| [popupView_ addSubview:whiteBackground]; |
| [whiteBackground setBackgroundColor:[UIColor whiteColor]]; |
| |
| // Set constraints to |whiteBackground|. |
| [whiteBackground setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| NSDictionary* metrics = @{ @"height" : @(kWhiteBackgroundHeight) }; |
| NSDictionary* views = NSDictionaryOfVariableBindings(whiteBackground); |
| [popupView_ |
| addConstraints:[NSLayoutConstraint |
| constraintsWithVisualFormat:@"H:|[whiteBackground]|" |
| options:0 |
| metrics:nil |
| views:views]]; |
| [popupView_ |
| addConstraints:[NSLayoutConstraint constraintsWithVisualFormat: |
| @"V:[whiteBackground(==height)]" |
| options:0 |
| metrics:metrics |
| views:views]]; |
| [popupView_ addConstraint:[NSLayoutConstraint |
| constraintWithItem:whiteBackground |
| attribute:NSLayoutAttributeBottom |
| relatedBy:NSLayoutRelationEqual |
| toItem:popupView_ |
| attribute:NSLayoutAttributeTop |
| multiplier:1 |
| constant:0]]; |
| // |whiteBackground| extends out of |popupView_| |
| [popupView_ setClipsToBounds:NO]; |
| } |
| } |
| |
| OmniboxPopupViewIOS::~OmniboxPopupViewIOS() { |
| // Destroy the model, in case it tries to call back into us when destroyed. |
| model_.reset(); |
| } |
| |
| // Set left image to globe or magnifying glass depending on which autocomplete |
| // option comes first. |
| void OmniboxPopupViewIOS::UpdateEditViewIcon() { |
| const AutocompleteResult& result = model_->result(); |
| const AutocompleteMatch& match = result.match_at(0); // 0 for first result. |
| int image_id = GetIconForAutocompleteMatchType( |
| match.type, /* is_starred */ false, /* is_incognito */ false); |
| edit_view_->SetLeftImage(image_id); |
| } |
| |
| void OmniboxPopupViewIOS::UpdatePopupAppearance() { |
| const AutocompleteResult& result = model_->result(); |
| UIView* view = popupView_; |
| |
| if (!is_open_ && !result.empty()) { |
| // The popup is not currently open and there are results to display. Update |
| // and animate the cells |
| [popup_controller_ updateMatches:result withAnimation:YES]; |
| } else { |
| // The popup is already displayed or there are no results to display. Update |
| // the cells without animating. |
| [popup_controller_ updateMatches:result withAnimation:NO]; |
| } |
| is_open_ = !result.empty(); |
| |
| if (is_open_) { |
| // Show |result.size| on iPad. Since iPhone can dismiss keyboard, set |
| // height to frame height. |
| CGFloat height = [[popup_controller_ tableView] contentSize].height; |
| UIEdgeInsets insets = [[popup_controller_ tableView] contentInset]; |
| // Note the calculation |insets.top * 2| is correct, it should not be |
| // insets.top + insets.bottom. |insets.bottom| will be larger than |
| // |insets.top| when the keyboard is visible, but |parentHeight| should stay |
| // the same. |
| CGFloat parentHeight = height + insets.top * 2 + ShadowHeight(); |
| UIView* siblingView = [positioner_ popupAnchorView]; |
| if (!IsIPadIdiom()) { |
| [view setAutoresizingMask:UIViewAutoresizingFlexibleWidth | |
| UIViewAutoresizingFlexibleHeight]; |
| [[siblingView superview] insertSubview:view belowSubview:siblingView]; |
| } else { |
| [[siblingView superview] insertSubview:view aboveSubview:siblingView]; |
| } |
| CGFloat currentHeight = view.layer.bounds.size.height; |
| if (currentHeight == 0) |
| AnimateDropdownExpansion(parentHeight); |
| else |
| [view setFrame:[positioner_ popupFrame:parentHeight]]; |
| UIView* popupControllerView = [popup_controller_ view]; |
| CGRect popupControllerFrame = popupControllerView.frame; |
| popupControllerFrame.size.height = view.frame.size.height - ShadowHeight(); |
| popupControllerView.frame = popupControllerFrame; |
| UpdateEditViewIcon(); |
| } else { |
| AnimateDropdownCollapse(); |
| } |
| |
| edit_view_->OnPopupResultsChanged(result); |
| } |
| |
| void OmniboxPopupViewIOS::AnimateDropdownExpansion(CGFloat parentHeight) { |
| CGRect popupFrame = [positioner_ popupFrame:parentHeight]; |
| CALayer* popupLayer = [popupView_ layer]; |
| CGRect bounds = popupLayer.bounds; |
| bounds.size.height = popupFrame.size.height; |
| popupLayer.bounds = bounds; |
| |
| CGRect frame = [popupView_ frame]; |
| frame.size.width = popupFrame.size.width; |
| frame.origin.y = popupFrame.origin.y; |
| [popupView_ setFrame:frame]; |
| |
| CABasicAnimation* growHeight = |
| [CABasicAnimation animationWithKeyPath:@"bounds.size.height"]; |
| growHeight.fromValue = @0; |
| growHeight.toValue = [NSNumber numberWithFloat:popupFrame.size.height]; |
| growHeight.duration = kExpandAnimationDuration; |
| growHeight.timingFunction = |
| [CAMediaTimingFunction functionWithControlPoints:0.4:0:0.2:1]; |
| [popupLayer addAnimation:growHeight forKey:@"growHeight"]; |
| } |
| |
| void OmniboxPopupViewIOS::AnimateDropdownCollapse() { |
| CALayer* popupLayer = [popupView_ layer]; |
| CGRect bounds = popupLayer.bounds; |
| CGFloat currentHeight = bounds.size.height; |
| bounds.size.height = 0; |
| popupLayer.bounds = bounds; |
| |
| UIView* retainedPopupView = popupView_; |
| [CATransaction begin]; |
| [CATransaction setCompletionBlock:^{ |
| [retainedPopupView removeFromSuperview]; |
| }]; |
| CABasicAnimation* shrinkHeight = |
| [CABasicAnimation animationWithKeyPath:@"bounds.size.height"]; |
| shrinkHeight.fromValue = [NSNumber numberWithFloat:currentHeight]; |
| shrinkHeight.toValue = @0; |
| shrinkHeight.duration = kCollapseAnimationDuration; |
| shrinkHeight.timingFunction = |
| [CAMediaTimingFunction functionWithControlPoints:0.4:0:1:1]; |
| [popupLayer addAnimation:shrinkHeight forKey:@"shrinkHeight"]; |
| [CATransaction commit]; |
| } |
| |
| gfx::Rect OmniboxPopupViewIOS::GetTargetBounds() { |
| return gfx::Rect(); |
| } |
| |
| // For phone, allow popup to take focus (and dismiss the keyboard) on scroll. |
| void OmniboxPopupViewIOS::DidScroll() { |
| if (!IsIPadIdiom()) { |
| edit_view_->HideKeyboard(); |
| } |
| } |
| |
| // Puts omnibox back into focus with suggested search terms. |
| void OmniboxPopupViewIOS::CopyToOmnibox(const base::string16& str) { |
| edit_view_->SetUserText(str); |
| edit_view_->FocusOmnibox(); |
| } |
| |
| void OmniboxPopupViewIOS::SetTextAlignment(NSTextAlignment alignment) { |
| [popup_controller_ setTextAlignment:alignment]; |
| } |
| |
| bool OmniboxPopupViewIOS::IsStarredMatch(const AutocompleteMatch& match) const { |
| return model_->IsStarredMatch(match); |
| } |
| |
| void OmniboxPopupViewIOS::DeleteMatch(const AutocompleteMatch& match) const { |
| model_->autocomplete_controller()->DeleteMatch(match); |
| } |
| |
| void OmniboxPopupViewIOS::OpenURLForRow(size_t row) { |
| // Crash reports tell us that |row| is sometimes indexed past the end of |
| // the results array. In those cases, just ignore the request and return |
| // early. See b/5813291. |
| if (row >= model_->result().size()) |
| return; |
| |
| WindowOpenDisposition disposition = WindowOpenDisposition::CURRENT_TAB; |
| base::RecordAction(UserMetricsAction("MobileOmniboxUse")); |
| |
| // OpenMatch() may close the popup, which will clear the result set and, by |
| // extension, |match| and its contents. So copy the relevant match out to |
| // make sure it stays alive until the call completes. |
| AutocompleteMatch match = model_->result().match_at(row); |
| if (match.type == AutocompleteMatchType::CLIPBOARD) { |
| base::RecordAction(UserMetricsAction("MobileOmniboxClipboardToURL")); |
| UMA_HISTOGRAM_LONG_TIMES_100( |
| "MobileOmnibox.PressedClipboardSuggestionAge", |
| ClipboardRecentContent::GetInstance()->GetClipboardContentAge()); |
| } |
| edit_view_->OpenMatch(match, disposition, GURL(), base::string16(), row); |
| } |
| |
| bool OmniboxPopupViewIOS::IsOpen() const { |
| return is_open_; |
| } |