Index: ios/chrome/browser/ui/omnibox/omnibox_popup_view_ios.mm |
diff --git a/ios/chrome/browser/ui/omnibox/omnibox_popup_view_ios.mm b/ios/chrome/browser/ui/omnibox/omnibox_popup_view_ios.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..12d521ed35f6d59688e3661c1268e02c392ca3e5 |
--- /dev/null |
+++ b/ios/chrome/browser/ui/omnibox/omnibox_popup_view_ios.mm |
@@ -0,0 +1,304 @@ |
+// 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.h" |
+#include "base/metrics/user_metrics.h" |
+#include "base/metrics/user_metrics_action.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" |
+#include "ios/chrome/grit/ios_theme_resources.h" |
+#include "ios/web/public/image_fetcher/image_data_fetcher.h" |
+#include "ios/web/public/web_thread.h" |
+#include "net/url_request/url_request_context_getter.h" |
+#include "ui/base/resource/resource_bundle.h" |
+#include "ui/gfx/geometry/rect.h" |
+#include "ui/gfx/image/image.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<web::ImageDataFetcher> imageFetcher = |
+ base::MakeUnique<web::ImageDataFetcher>( |
+ web::WebThread::GetBlockingPool()); |
+ imageFetcher->SetRequestContextGetter( |
+ edit_view->browser_state()->GetRequestContext()); |
+ |
+ 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]; |
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance(); |
+ gfx::Image shadowImage = |
+ rb.GetNativeImageNamed(IDR_IOS_TOOLBAR_SHADOW_FULL_BLEED); |
+ base::scoped_nsobject<UIImageView> shadowView( |
+ [[UIImageView alloc] initWithImage:shadowImage.ToUIImage()]); |
+ [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_; |
+} |