OLD | NEW |
(Empty) | |
| 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 "ios/chrome/browser/ui/omnibox/omnibox_popup_view_ios.h" |
| 6 |
| 7 #import <QuartzCore/QuartzCore.h> |
| 8 |
| 9 #include "base/logging.h" |
| 10 #include "base/memory/ptr_util.h" |
| 11 #include "base/metrics/histogram.h" |
| 12 #include "base/metrics/user_metrics.h" |
| 13 #include "base/metrics/user_metrics_action.h" |
| 14 #include "components/omnibox/browser/autocomplete_match.h" |
| 15 #include "components/omnibox/browser/omnibox_edit_model.h" |
| 16 #include "components/omnibox/browser/omnibox_popup_model.h" |
| 17 #include "components/open_from_clipboard/clipboard_recent_content.h" |
| 18 #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| 19 #import "ios/chrome/browser/experimental_flags.h" |
| 20 #import "ios/chrome/browser/ui/omnibox/omnibox_popup_material_view_controller.h" |
| 21 #import "ios/chrome/browser/ui/omnibox/omnibox_popup_positioner.h" |
| 22 #include "ios/chrome/browser/ui/omnibox/omnibox_util.h" |
| 23 #include "ios/chrome/browser/ui/omnibox/omnibox_view_ios.h" |
| 24 #include "ios/chrome/browser/ui/ui_util.h" |
| 25 #include "ios/chrome/grit/ios_theme_resources.h" |
| 26 #include "ios/web/public/image_fetcher/image_data_fetcher.h" |
| 27 #include "ios/web/public/web_thread.h" |
| 28 #include "net/url_request/url_request_context_getter.h" |
| 29 #include "ui/base/resource/resource_bundle.h" |
| 30 #include "ui/gfx/geometry/rect.h" |
| 31 #include "ui/gfx/image/image.h" |
| 32 |
| 33 namespace { |
| 34 const CGFloat kExpandAnimationDuration = 0.1; |
| 35 const CGFloat kCollapseAnimationDuration = 0.05; |
| 36 const CGFloat kWhiteBackgroundHeight = 74; |
| 37 NS_INLINE CGFloat ShadowHeight() { |
| 38 return IsIPadIdiom() ? 10 : 0; |
| 39 } |
| 40 } // namespace |
| 41 |
| 42 using base::UserMetricsAction; |
| 43 |
| 44 OmniboxPopupViewIOS::OmniboxPopupViewIOS(OmniboxViewIOS* edit_view, |
| 45 OmniboxEditModel* edit_model, |
| 46 id<OmniboxPopupPositioner> positioner) |
| 47 : model_(new OmniboxPopupModel(this, edit_model)), |
| 48 edit_view_(edit_view), |
| 49 positioner_(positioner), |
| 50 is_open_(false) { |
| 51 DCHECK(edit_view); |
| 52 DCHECK(edit_model); |
| 53 |
| 54 std::unique_ptr<web::ImageDataFetcher> imageFetcher = |
| 55 base::MakeUnique<web::ImageDataFetcher>( |
| 56 web::WebThread::GetBlockingPool()); |
| 57 imageFetcher->SetRequestContextGetter( |
| 58 edit_view->browser_state()->GetRequestContext()); |
| 59 |
| 60 popup_controller_.reset([[OmniboxPopupMaterialViewController alloc] |
| 61 initWithPopupView:this |
| 62 withFetcher:std::move(imageFetcher)]); |
| 63 [popup_controller_ setIncognito:edit_view->browser_state()->IsOffTheRecord()]; |
| 64 popupView_.reset([[UIView alloc] initWithFrame:CGRectZero]); |
| 65 [popupView_ setClipsToBounds:YES]; |
| 66 CALayer* popupLayer = [popupView_ layer]; |
| 67 // Adjust popupView_'s anchor point and height so that it animates down |
| 68 // from the top when it appears. |
| 69 popupLayer.anchorPoint = CGPointMake(0.5, 0); |
| 70 UIView* popupControllerView = [popup_controller_ view]; |
| 71 CGRect popupControllerFrame = popupControllerView.frame; |
| 72 popupControllerFrame.origin = CGPointZero; |
| 73 popupControllerView.frame = popupControllerFrame; |
| 74 [popupView_ addSubview:popupControllerView]; |
| 75 if (IsIPadIdiom()) { |
| 76 [popupView_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth]; |
| 77 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); |
| 78 gfx::Image shadowImage = |
| 79 rb.GetNativeImageNamed(IDR_IOS_TOOLBAR_SHADOW_FULL_BLEED); |
| 80 base::scoped_nsobject<UIImageView> shadowView( |
| 81 [[UIImageView alloc] initWithImage:shadowImage.ToUIImage()]); |
| 82 [shadowView setUserInteractionEnabled:NO]; |
| 83 [shadowView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| 84 [popupView_ addSubview:shadowView]; |
| 85 |
| 86 // Add constraints to position |shadowView| at the bottom of |popupView_| |
| 87 // with the same width as |popupView_|. |
| 88 NSDictionary* views = NSDictionaryOfVariableBindings(shadowView); |
| 89 [popupView_ |
| 90 addConstraints:[NSLayoutConstraint |
| 91 constraintsWithVisualFormat:@"H:|[shadowView]|" |
| 92 options:0 |
| 93 metrics:nil |
| 94 views:views]]; |
| 95 [popupView_ addConstraint:[NSLayoutConstraint |
| 96 constraintWithItem:shadowView |
| 97 attribute:NSLayoutAttributeBottom |
| 98 relatedBy:NSLayoutRelationEqual |
| 99 toItem:popupView_ |
| 100 attribute:NSLayoutAttributeBottom |
| 101 multiplier:1 |
| 102 constant:0]]; |
| 103 } else { |
| 104 // Add a white background to prevent seing the logo scroll through the |
| 105 // omnibox. |
| 106 base::scoped_nsobject<UIView> whiteBackground( |
| 107 [[UIView alloc] initWithFrame:CGRectZero]); |
| 108 [popupView_ addSubview:whiteBackground]; |
| 109 [whiteBackground setBackgroundColor:[UIColor whiteColor]]; |
| 110 |
| 111 // Set constraints to |whiteBackground|. |
| 112 [whiteBackground setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| 113 NSDictionary* metrics = @{ @"height" : @(kWhiteBackgroundHeight) }; |
| 114 NSDictionary* views = NSDictionaryOfVariableBindings(whiteBackground); |
| 115 [popupView_ |
| 116 addConstraints:[NSLayoutConstraint |
| 117 constraintsWithVisualFormat:@"H:|[whiteBackground]|" |
| 118 options:0 |
| 119 metrics:nil |
| 120 views:views]]; |
| 121 [popupView_ |
| 122 addConstraints:[NSLayoutConstraint constraintsWithVisualFormat: |
| 123 @"V:[whiteBackground(==height)]" |
| 124 options:0 |
| 125 metrics:metrics |
| 126 views:views]]; |
| 127 [popupView_ addConstraint:[NSLayoutConstraint |
| 128 constraintWithItem:whiteBackground |
| 129 attribute:NSLayoutAttributeBottom |
| 130 relatedBy:NSLayoutRelationEqual |
| 131 toItem:popupView_ |
| 132 attribute:NSLayoutAttributeTop |
| 133 multiplier:1 |
| 134 constant:0]]; |
| 135 // |whiteBackground| extends out of |popupView_| |
| 136 [popupView_ setClipsToBounds:NO]; |
| 137 } |
| 138 } |
| 139 |
| 140 OmniboxPopupViewIOS::~OmniboxPopupViewIOS() { |
| 141 // Destroy the model, in case it tries to call back into us when destroyed. |
| 142 model_.reset(); |
| 143 } |
| 144 |
| 145 // Set left image to globe or magnifying glass depending on which autocomplete |
| 146 // option comes first. |
| 147 void OmniboxPopupViewIOS::UpdateEditViewIcon() { |
| 148 const AutocompleteResult& result = model_->result(); |
| 149 const AutocompleteMatch& match = result.match_at(0); // 0 for first result. |
| 150 int image_id = GetIconForAutocompleteMatchType( |
| 151 match.type, /* is_starred */ false, /* is_incognito */ false); |
| 152 edit_view_->SetLeftImage(image_id); |
| 153 } |
| 154 |
| 155 void OmniboxPopupViewIOS::UpdatePopupAppearance() { |
| 156 const AutocompleteResult& result = model_->result(); |
| 157 UIView* view = popupView_; |
| 158 |
| 159 if (!is_open_ && !result.empty()) { |
| 160 // The popup is not currently open and there are results to display. Update |
| 161 // and animate the cells |
| 162 [popup_controller_ updateMatches:result withAnimation:YES]; |
| 163 } else { |
| 164 // The popup is already displayed or there are no results to display. Update |
| 165 // the cells without animating. |
| 166 [popup_controller_ updateMatches:result withAnimation:NO]; |
| 167 } |
| 168 is_open_ = !result.empty(); |
| 169 |
| 170 if (is_open_) { |
| 171 // Show |result.size| on iPad. Since iPhone can dismiss keyboard, set |
| 172 // height to frame height. |
| 173 CGFloat height = [[popup_controller_ tableView] contentSize].height; |
| 174 UIEdgeInsets insets = [[popup_controller_ tableView] contentInset]; |
| 175 // Note the calculation |insets.top * 2| is correct, it should not be |
| 176 // insets.top + insets.bottom. |insets.bottom| will be larger than |
| 177 // |insets.top| when the keyboard is visible, but |parentHeight| should stay |
| 178 // the same. |
| 179 CGFloat parentHeight = height + insets.top * 2 + ShadowHeight(); |
| 180 UIView* siblingView = [positioner_ popupAnchorView]; |
| 181 if (!IsIPadIdiom()) { |
| 182 [view setAutoresizingMask:UIViewAutoresizingFlexibleWidth | |
| 183 UIViewAutoresizingFlexibleHeight]; |
| 184 [[siblingView superview] insertSubview:view belowSubview:siblingView]; |
| 185 } else { |
| 186 [[siblingView superview] insertSubview:view aboveSubview:siblingView]; |
| 187 } |
| 188 CGFloat currentHeight = view.layer.bounds.size.height; |
| 189 if (currentHeight == 0) |
| 190 AnimateDropdownExpansion(parentHeight); |
| 191 else |
| 192 [view setFrame:[positioner_ popupFrame:parentHeight]]; |
| 193 UIView* popupControllerView = [popup_controller_ view]; |
| 194 CGRect popupControllerFrame = popupControllerView.frame; |
| 195 popupControllerFrame.size.height = view.frame.size.height - ShadowHeight(); |
| 196 popupControllerView.frame = popupControllerFrame; |
| 197 UpdateEditViewIcon(); |
| 198 } else { |
| 199 AnimateDropdownCollapse(); |
| 200 } |
| 201 |
| 202 edit_view_->OnPopupResultsChanged(result); |
| 203 } |
| 204 |
| 205 void OmniboxPopupViewIOS::AnimateDropdownExpansion(CGFloat parentHeight) { |
| 206 CGRect popupFrame = [positioner_ popupFrame:parentHeight]; |
| 207 CALayer* popupLayer = [popupView_ layer]; |
| 208 CGRect bounds = popupLayer.bounds; |
| 209 bounds.size.height = popupFrame.size.height; |
| 210 popupLayer.bounds = bounds; |
| 211 |
| 212 CGRect frame = [popupView_ frame]; |
| 213 frame.size.width = popupFrame.size.width; |
| 214 frame.origin.y = popupFrame.origin.y; |
| 215 [popupView_ setFrame:frame]; |
| 216 |
| 217 CABasicAnimation* growHeight = |
| 218 [CABasicAnimation animationWithKeyPath:@"bounds.size.height"]; |
| 219 growHeight.fromValue = @0; |
| 220 growHeight.toValue = [NSNumber numberWithFloat:popupFrame.size.height]; |
| 221 growHeight.duration = kExpandAnimationDuration; |
| 222 growHeight.timingFunction = |
| 223 [CAMediaTimingFunction functionWithControlPoints:0.4:0:0.2:1]; |
| 224 [popupLayer addAnimation:growHeight forKey:@"growHeight"]; |
| 225 } |
| 226 |
| 227 void OmniboxPopupViewIOS::AnimateDropdownCollapse() { |
| 228 CALayer* popupLayer = [popupView_ layer]; |
| 229 CGRect bounds = popupLayer.bounds; |
| 230 CGFloat currentHeight = bounds.size.height; |
| 231 bounds.size.height = 0; |
| 232 popupLayer.bounds = bounds; |
| 233 |
| 234 UIView* retainedPopupView = popupView_; |
| 235 [CATransaction begin]; |
| 236 [CATransaction setCompletionBlock:^{ |
| 237 [retainedPopupView removeFromSuperview]; |
| 238 }]; |
| 239 CABasicAnimation* shrinkHeight = |
| 240 [CABasicAnimation animationWithKeyPath:@"bounds.size.height"]; |
| 241 shrinkHeight.fromValue = [NSNumber numberWithFloat:currentHeight]; |
| 242 shrinkHeight.toValue = @0; |
| 243 shrinkHeight.duration = kCollapseAnimationDuration; |
| 244 shrinkHeight.timingFunction = |
| 245 [CAMediaTimingFunction functionWithControlPoints:0.4:0:1:1]; |
| 246 [popupLayer addAnimation:shrinkHeight forKey:@"shrinkHeight"]; |
| 247 [CATransaction commit]; |
| 248 } |
| 249 |
| 250 gfx::Rect OmniboxPopupViewIOS::GetTargetBounds() { |
| 251 return gfx::Rect(); |
| 252 } |
| 253 |
| 254 // For phone, allow popup to take focus (and dismiss the keyboard) on scroll. |
| 255 void OmniboxPopupViewIOS::DidScroll() { |
| 256 if (!IsIPadIdiom()) { |
| 257 edit_view_->HideKeyboard(); |
| 258 } |
| 259 } |
| 260 |
| 261 // Puts omnibox back into focus with suggested search terms. |
| 262 void OmniboxPopupViewIOS::CopyToOmnibox(const base::string16& str) { |
| 263 edit_view_->SetUserText(str); |
| 264 edit_view_->FocusOmnibox(); |
| 265 } |
| 266 |
| 267 void OmniboxPopupViewIOS::SetTextAlignment(NSTextAlignment alignment) { |
| 268 [popup_controller_ setTextAlignment:alignment]; |
| 269 } |
| 270 |
| 271 bool OmniboxPopupViewIOS::IsStarredMatch(const AutocompleteMatch& match) const { |
| 272 return model_->IsStarredMatch(match); |
| 273 } |
| 274 |
| 275 void OmniboxPopupViewIOS::DeleteMatch(const AutocompleteMatch& match) const { |
| 276 model_->autocomplete_controller()->DeleteMatch(match); |
| 277 } |
| 278 |
| 279 void OmniboxPopupViewIOS::OpenURLForRow(size_t row) { |
| 280 // Crash reports tell us that |row| is sometimes indexed past the end of |
| 281 // the results array. In those cases, just ignore the request and return |
| 282 // early. See b/5813291. |
| 283 if (row >= model_->result().size()) |
| 284 return; |
| 285 |
| 286 WindowOpenDisposition disposition = WindowOpenDisposition::CURRENT_TAB; |
| 287 base::RecordAction(UserMetricsAction("MobileOmniboxUse")); |
| 288 |
| 289 // OpenMatch() may close the popup, which will clear the result set and, by |
| 290 // extension, |match| and its contents. So copy the relevant match out to |
| 291 // make sure it stays alive until the call completes. |
| 292 AutocompleteMatch match = model_->result().match_at(row); |
| 293 if (match.type == AutocompleteMatchType::CLIPBOARD) { |
| 294 base::RecordAction(UserMetricsAction("MobileOmniboxClipboardToURL")); |
| 295 UMA_HISTOGRAM_LONG_TIMES_100( |
| 296 "MobileOmnibox.PressedClipboardSuggestionAge", |
| 297 ClipboardRecentContent::GetInstance()->GetClipboardContentAge()); |
| 298 } |
| 299 edit_view_->OpenMatch(match, disposition, GURL(), base::string16(), row); |
| 300 } |
| 301 |
| 302 bool OmniboxPopupViewIOS::IsOpen() const { |
| 303 return is_open_; |
| 304 } |
OLD | NEW |