Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(93)

Side by Side Diff: ios/chrome/browser/ui/ntp/google_landing_controller.mm

Issue 2848003002: Rename GoogleLandingController to GoogleLandingViewController (Closed)
Patch Set: Missing file Created 3 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2013 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 #import "ios/chrome/browser/ui/ntp/google_landing_controller.h"
6
7 #include <algorithm>
8
9 #include "base/mac/foundation_util.h"
10 #include "base/metrics/user_metrics.h"
11 #include "base/strings/sys_string_conversions.h"
12 #include "components/strings/grit/components_strings.h"
13 #import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h"
14 #import "ios/chrome/browser/ui/commands/generic_chrome_command.h"
15 #include "ios/chrome/browser/ui/commands/ios_command_ids.h"
16 #import "ios/chrome/browser/ui/context_menu/context_menu_coordinator.h"
17 #import "ios/chrome/browser/ui/ntp/google_landing_data_source.h"
18 #import "ios/chrome/browser/ui/ntp/most_visited_cell.h"
19 #import "ios/chrome/browser/ui/ntp/most_visited_layout.h"
20 #import "ios/chrome/browser/ui/ntp/new_tab_page_header_constants.h"
21 #import "ios/chrome/browser/ui/ntp/new_tab_page_header_view.h"
22 #import "ios/chrome/browser/ui/ntp/whats_new_header_view.h"
23 #import "ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller. h"
24 #import "ios/chrome/browser/ui/toolbar/web_toolbar_controller.h"
25 #include "ios/chrome/browser/ui/ui_util.h"
26 #import "ios/chrome/browser/ui/uikit_ui_util.h"
27 #import "ios/chrome/browser/ui/url_loader.h"
28 #include "ios/chrome/common/string_util.h"
29 #include "ios/chrome/grit/ios_strings.h"
30 #import "ios/third_party/material_components_ios/src/components/Snackbar/src/Mat erialSnackbar.h"
31 #import "ios/third_party/material_components_ios/src/components/Typography/src/M aterialTypography.h"
32 #import "ios/web/public/web_state/context_menu_params.h"
33 #import "net/base/mac/url_conversions.h"
34 #include "ui/base/l10n/l10n_util.h"
35 #include "ui/base/page_transition_types.h"
36
37 using base::UserMetricsAction;
38
39 namespace {
40
41 enum {
42 SectionWithOmnibox,
43 SectionWithMostVisited,
44 NumberOfCollectionViewSections,
45 };
46
47 enum InterfaceOrientation {
48 ALL,
49 IPHONE_LANDSCAPE,
50 };
51
52 const CGFloat kVoiceSearchButtonWidth = 48;
53 const UIEdgeInsets kSearchBoxStretchInsets = {3, 3, 3, 3};
54
55 // Height for the doodle frame when Google is not the default search engine.
56 const CGFloat kNonGoogleSearchDoodleHeight = 60;
57 // Height for the header view on tablet when Google is not the default search
58 // engine.
59 const CGFloat kNonGoogleSearchHeaderHeightIPad = 10;
60
61 const CGFloat kHintLabelSidePadding = 12;
62 const CGFloat kNTPSearchFieldBottomPadding = 16;
63 const CGFloat kWhatsNewHeaderHiddenHeight = 8;
64 const CGFloat kDoodleTopMarginIPadPortrait = 82;
65 const CGFloat kDoodleTopMarginIPadLandscape = 82;
66 const NSInteger kMaxNumMostVisitedFaviconRows = 2;
67 const CGFloat kMaxSearchFieldFrameMargin = 200;
68 const CGFloat kShiftTilesDownAnimationDuration = 0.2;
69
70 const CGFloat kMostVisitedPaddingIPhone = 16;
71 const CGFloat kMostVisitedPaddingIPadFavicon = 24;
72
73 } // namespace
74
75 @interface GoogleLandingController (UsedByGoogleLandingView)
76 // Update frames for subviews depending on the interface orientation.
77 - (void)updateSubviewFrames;
78 // Resets the collection view's inset to 0.
79 - (void)resetSectionInset;
80 - (void)reloadData;
81 @end
82
83 // Subclassing the main UIScrollView allows calls for setFrame.
84 @interface GoogleLandingView : UIView {
85 GoogleLandingController* _googleLanding;
86 }
87
88 - (void)setFrameDelegate:(GoogleLandingController*)delegate;
89
90 @end
91
92 @implementation GoogleLandingView
93
94 - (void)setFrameDelegate:(GoogleLandingController*)delegate {
95 _googleLanding = delegate;
96 }
97
98 - (void)setFrame:(CGRect)frame {
99 // On iPad and in fullscreen, the collection view's inset is very large.
100 // When Chrome enters slide over mode, the previously set inset is larger than
101 // the newly set collection view's width, which makes the collection view
102 // throw an exception.
103 // To prevent this from happening, we reset the inset to 0 before changing the
104 // frame.
105 [_googleLanding resetSectionInset];
106 [super setFrame:frame];
107 [_googleLanding updateSubviewFrames];
108 [_googleLanding reloadData];
109 }
110
111 @end
112
113 @interface GoogleLandingController ()<OverscrollActionsControllerDelegate,
114 UICollectionViewDataSource,
115 UICollectionViewDelegate,
116 UICollectionViewDelegateFlowLayout,
117 UIGestureRecognizerDelegate,
118 WhatsNewHeaderViewDelegate> {
119 // Fake omnibox.
120 base::scoped_nsobject<UIButton> _searchTapTarget;
121
122 // A collection view for the most visited sites.
123 base::scoped_nsobject<UICollectionView> _mostVisitedView;
124
125 // The overscroll actions controller managing accelerators over the toolbar.
126 base::scoped_nsobject<OverscrollActionsController>
127 _overscrollActionsController;
128
129 // |YES| when notifications indicate the omnibox is focused.
130 BOOL _omniboxFocused;
131
132 // Tap and swipe gesture recognizers when the omnibox is focused.
133 base::scoped_nsobject<UITapGestureRecognizer> _tapGestureRecognizer;
134 base::scoped_nsobject<UISwipeGestureRecognizer> _swipeGestureRecognizer;
135
136 // Handles displaying the context menu for all form factors.
137 base::scoped_nsobject<ContextMenuCoordinator> _contextMenuCoordinator;
138
139 // URL of the last deleted most viewed entry. If present the UI to restore it
140 // is shown.
141 base::scoped_nsobject<NSURL> _deletedUrl;
142
143 // |YES| if the view has finished its first layout. This is useful when
144 // determining if the view has sized itself for tablet.
145 BOOL _viewLoaded;
146
147 // |YES| if the fakebox header should be animated on scroll.
148 BOOL _animateHeader;
149
150 // |YES| if the collection scrollView is scrolled all the way to the top. Used
151 // to lock this position in place on various frame changes.
152 BOOL _scrolledToTop;
153
154 // |YES| if this NTP panel is visible. When set to |NO| various UI updates
155 // are ignored.
156 BOOL _isShowing;
157
158 CFTimeInterval _shiftTilesDownStartTime;
159 CGSize _mostVisitedCellSize;
160 base::scoped_nsobject<NSLayoutConstraint> _hintLabelLeadingConstraint;
161 base::scoped_nsobject<NSLayoutConstraint> _voiceTapTrailingConstraint;
162 base::scoped_nsobject<NSMutableArray> _supplementaryViews;
163 base::scoped_nsobject<NewTabPageHeaderView> _headerView;
164 base::scoped_nsobject<WhatsNewHeaderView> _promoHeaderView;
165 }
166
167 // Redeclare the |view| property to be the GoogleLandingView subclass instead of
168 // a generic UIView.
169 @property(nonatomic, readwrite, strong) GoogleLandingView* view;
170
171 // Whether the Google logo or doodle is being shown.
172 @property(nonatomic, assign) BOOL logoIsShowing;
173
174 // Exposes view and methods to drive the doodle.
175 @property(nonatomic, assign) id<LogoVendor> logoVendor;
176
177 // |YES| if this consumer is incognito.
178 @property(nonatomic, assign) BOOL isOffTheRecord;
179
180 // |YES| if this consumer is has voice search enabled.
181 @property(nonatomic, assign) BOOL voiceSearchIsEnabled;
182
183 // Gets the maximum number of sites shown.
184 @property(nonatomic, assign) NSUInteger maximumMostVisitedSitesShown;
185
186 // Gets the text of a what's new promo.
187 @property(nonatomic, retain) NSString* promoText;
188
189 // Gets the icon of a what's new promo.
190 // TODO(crbug.com/694750): This should not be WhatsNewIcon.
191 @property(nonatomic, assign) WhatsNewIcon promoIcon;
192
193 // |YES| if a what's new promo can be displayed.
194 @property(nonatomic, assign) BOOL promoCanShow;
195
196 // The number of tabs to show in the google landing fake toolbar.
197 @property(nonatomic, assign) int tabCount;
198
199 // |YES| if the google landing toolbar can show the forward arrow, cached and
200 // pushed into the header view.
201 @property(nonatomic, assign) BOOL canGoForward;
202
203 // |YES| if the google landing toolbar can show the back arrow, cached and
204 // pushed into the header view.
205 @property(nonatomic, assign) BOOL canGoBack;
206
207 // iPhone landscape uses a slightly different layout for the doodle and search
208 // field frame. Returns the proper frame from |frames| based on orientation,
209 // centered in the view.
210 - (CGRect)getOrientationFrame:(const CGRect[])frames;
211 // Returns the proper frame for the doodle.
212 - (CGRect)doodleFrame;
213 // Returns the proper frame for the search field.
214 - (CGRect)searchFieldFrame;
215 // Returns the height to use for the What's New promo view.
216 - (CGFloat)promoHeaderHeight;
217 // Add fake search field and voice search microphone.
218 - (void)addSearchField;
219 // Add most visited collection view.
220 - (void)addMostVisited;
221 // Update the iPhone fakebox's frame based on the current scroll view offset.
222 - (void)updateSearchField;
223 // Scrolls most visited to the top of the view when the omnibox is focused.
224 - (void)locationBarBecomesFirstResponder;
225 // Scroll the view back to 0,0 when the omnibox loses focus.
226 - (void)locationBarResignsFirstResponder;
227 // When the search field is tapped.
228 - (void)searchFieldTapped:(id)sender;
229 // Tells WebToolbarController to resign focus to the omnibox.
230 - (void)blurOmnibox;
231 // Called when a user does a long press on a most visited item.
232 - (void)handleMostVisitedLongPress:
233 (UILongPressGestureRecognizer*)longPressGesture;
234 // When the user removes a most visited a bubble pops up to undo the action.
235 - (void)showMostVisitedUndoForURL:(NSURL*)url;
236 // If Google is not the default search engine, hide the logo, doodle and
237 // fakebox.
238 - (void)updateLogoAndFakeboxDisplay;
239 // Helper method to set UICollectionViewFlowLayout insets for most visited.
240 - (void)setFlowLayoutInset:(UICollectionViewFlowLayout*)layout;
241 // Instructs the UICollectionView and UIView to reload it's data and layout.
242 - (void)reloadData;
243 // Returns the size of |self.mostVisitedData|.
244 - (NSUInteger)numberOfItems;
245 // Returns the number of non empty tiles (as opposed to the placeholder tiles).
246 - (NSInteger)numberOfNonEmptyTilesShown;
247 // Returns the URL for the mosted visited item in |self.mostVisitedData|.
248 - (GURL)urlForIndex:(NSUInteger)index;
249 // Returns the expected height of the NewTabPageHeaderView.
250 - (CGFloat)heightForSectionWithOmnibox;
251 // Returns the nearest ancestor view that is kind of |aClass|.
252 - (UIView*)nearestAncestorOfView:(UIView*)view withClass:(Class)aClass;
253 // Updates the collection view's scroll view offset for the next frame of the
254 // shiftTilesDown animation.
255 - (void)shiftTilesDownAnimationDidFire:(CADisplayLink*)link;
256 // Returns the size to use for Most Visited cells in the NTP.
257 - (CGSize)mostVisitedCellSize;
258 // Returns the padding for use between Most Visited cells.
259 - (CGFloat)mostVisitedCellPadding;
260
261 @end
262
263 @implementation GoogleLandingController
264
265 @dynamic view;
266 @synthesize logoVendor = _logoVendor;
267 @synthesize dataSource = _dataSource;
268 // Property declared in NewTabPagePanelProtocol.
269 @synthesize delegate = _delegate;
270 @synthesize dispatcher = _dispatcher;
271 @synthesize isOffTheRecord = _isOffTheRecord;
272 @synthesize logoIsShowing = _logoIsShowing;
273 @synthesize promoText = _promoText;
274 @synthesize promoIcon = _promoIcon;
275 @synthesize promoCanShow = _promoCanShow;
276 @synthesize maximumMostVisitedSitesShown = _maximumMostVisitedSitesShown;
277 @synthesize tabCount = _tabCount;
278 @synthesize canGoForward = _canGoForward;
279 @synthesize canGoBack = _canGoBack;
280 @synthesize voiceSearchIsEnabled = _voiceSearchIsEnabled;
281
282 - (void)loadView {
283 self.view = [[[GoogleLandingView alloc]
284 initWithFrame:[UIScreen mainScreen].bounds] autorelease];
285 }
286
287 - (void)viewDidLoad {
288 [super viewDidLoad];
289 [self.view setAutoresizingMask:UIViewAutoresizingFlexibleHeight |
290 UIViewAutoresizingFlexibleWidth];
291 [self.view setFrameDelegate:self];
292
293 // Initialise |shiftTilesDownStartTime| to a sentinel value to indicate that
294 // the animation has not yet started.
295 _shiftTilesDownStartTime = -1;
296 _mostVisitedCellSize = [self mostVisitedCellSize];
297 _isShowing = YES;
298 _scrolledToTop = NO;
299 _animateHeader = YES;
300
301 _tapGestureRecognizer.reset([[UITapGestureRecognizer alloc]
302 initWithTarget:self
303 action:@selector(blurOmnibox)]);
304 [_tapGestureRecognizer setDelegate:self];
305 _swipeGestureRecognizer.reset([[UISwipeGestureRecognizer alloc]
306 initWithTarget:self
307 action:@selector(blurOmnibox)]);
308 [_swipeGestureRecognizer setDirection:UISwipeGestureRecognizerDirectionDown];
309
310 [self addSearchField];
311 [self addMostVisited];
312 [self addOverscrollActions];
313 [self reload];
314 }
315
316 - (void)viewDidLayoutSubviews {
317 [self updateSubviewFrames];
318 }
319
320 - (void)viewWillTransitionToSize:(CGSize)size
321 withTransitionCoordinator:
322 (id<UIViewControllerTransitionCoordinator>)coordinator {
323 [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
324
325 void (^alongsideBlock)(id<UIViewControllerTransitionCoordinatorContext>) = ^(
326 id<UIViewControllerTransitionCoordinatorContext> context) {
327 if (IsIPadIdiom() && _scrolledToTop) {
328 // Keep the most visited thumbnails scrolled to the top.
329 [_mostVisitedView setContentOffset:CGPointMake(0, [self pinnedOffsetY])];
330 return;
331 };
332
333 // Invalidate the layout so that the collection view's header size is reset
334 // for the new orientation.
335 if (!_scrolledToTop) {
336 [[_mostVisitedView collectionViewLayout] invalidateLayout];
337 }
338
339 // Call -scrollViewDidScroll: so that the omnibox's frame is adjusted for
340 // the scroll view's offset.
341 [self scrollViewDidScroll:_mostVisitedView];
342
343 };
344 [coordinator animateAlongsideTransition:alongsideBlock completion:nil];
345 }
346
347 - (void)dealloc {
348 [[NSNotificationCenter defaultCenter] removeObserver:self];
349 [_mostVisitedView setDelegate:nil];
350 [_mostVisitedView setDataSource:nil];
351 [_overscrollActionsController invalidate];
352 [super dealloc];
353 }
354
355 #pragma mark - Private
356
357 - (CGSize)mostVisitedCellSize {
358 if (IsIPadIdiom()) {
359 // On iPads, split-screen and slide-over may require showing smaller cells.
360 CGSize maximumCellSize = [MostVisitedCell maximumSize];
361 CGSize viewSize = self.view.bounds.size;
362 CGFloat smallestDimension =
363 viewSize.height > viewSize.width ? viewSize.width : viewSize.height;
364 CGFloat cellWidth = AlignValueToPixel(
365 (smallestDimension - 3 * [self mostVisitedCellPadding]) / 2);
366 if (cellWidth < maximumCellSize.width) {
367 return CGSizeMake(cellWidth, cellWidth);
368 } else {
369 return maximumCellSize;
370 }
371 } else {
372 return [MostVisitedCell maximumSize];
373 }
374 }
375
376 - (CGFloat)mostVisitedCellPadding {
377 return IsIPadIdiom() ? kMostVisitedPaddingIPadFavicon
378 : kMostVisitedPaddingIPhone;
379 }
380
381 - (CGFloat)viewWidth {
382 return [self.view frame].size.width;
383 }
384
385 - (int)numberOfColumns {
386 CGFloat width = [self viewWidth];
387 CGFloat padding = [self mostVisitedCellPadding];
388 // Try to fit 4 columns.
389 if (width >= 5 * padding + _mostVisitedCellSize.width * 4)
390 return 4;
391 // Try to fit 3 columns.
392 if (width >= 4 * padding + _mostVisitedCellSize.width * 3)
393 return 3;
394 // Try to fit 2 columns.
395 if (width >= 3 * padding + _mostVisitedCellSize.width * 2)
396 return 2;
397 // We never want to have a layout with only one column, however: At launch,
398 // the view's size is initialized to the width of 320, which can only fit
399 // one column on iPhone 6 and 6+. TODO(crbug.com/506183): Get rid of the
400 // unecessary resize, and add a NOTREACHED() here.
401 return 1;
402 }
403
404 - (CGFloat)leftMargin {
405 int columns = [self numberOfColumns];
406 CGFloat whitespace = [self viewWidth] - columns * _mostVisitedCellSize.width -
407 (columns - 1) * [self mostVisitedCellPadding];
408 CGFloat margin = AlignValueToPixel(whitespace / 2);
409 DCHECK(margin >= [self mostVisitedCellPadding]);
410 return margin;
411 }
412
413 - (CGRect)doodleFrame {
414 const CGRect kDoodleFrame[2] = {
415 {{0, 66}, {0, 120}}, {{0, 56}, {0, 120}},
416 };
417 CGRect doodleFrame = [self getOrientationFrame:kDoodleFrame];
418 if (!IsIPadIdiom() && !self.logoIsShowing)
419 doodleFrame.size.height = kNonGoogleSearchDoodleHeight;
420 if (IsIPadIdiom()) {
421 doodleFrame.origin.y = IsPortrait() ? kDoodleTopMarginIPadPortrait
422 : kDoodleTopMarginIPadLandscape;
423 }
424 return doodleFrame;
425 }
426
427 - (CGRect)searchFieldFrame {
428 CGFloat y = CGRectGetMaxY([self doodleFrame]);
429 CGFloat leftMargin = [self leftMargin];
430 if (leftMargin > kMaxSearchFieldFrameMargin)
431 leftMargin = kMaxSearchFieldFrameMargin;
432 const CGRect kSearchFieldFrame[2] = {
433 {{leftMargin, y + 32}, {0, 50}}, {{leftMargin, y + 16}, {0, 50}},
434 };
435 CGRect searchFieldFrame = [self getOrientationFrame:kSearchFieldFrame];
436 if (IsIPadIdiom()) {
437 CGFloat iPadTopMargin = IsPortrait() ? kDoodleTopMarginIPadPortrait
438 : kDoodleTopMarginIPadLandscape;
439 searchFieldFrame.origin.y += iPadTopMargin - 32;
440 }
441 return searchFieldFrame;
442 }
443
444 - (CGRect)getOrientationFrame:(const CGRect[])frames {
445 UIInterfaceOrientation orient =
446 [[UIApplication sharedApplication] statusBarOrientation];
447 InterfaceOrientation inter_orient =
448 (IsIPadIdiom() || UIInterfaceOrientationIsPortrait(orient))
449 ? ALL
450 : IPHONE_LANDSCAPE;
451
452 // Calculate width based on screen width and origin x.
453 CGRect frame = frames[inter_orient];
454 frame.size.width = fmax(self.view.bounds.size.width - 2 * frame.origin.x, 50);
455 return frame;
456 }
457
458 - (CGFloat)promoHeaderHeight {
459 CGFloat promoMaxWidth = [self viewWidth] - 2 * [self leftMargin];
460 NSString* text = self.promoText;
461 return [WhatsNewHeaderView heightToFitText:text inWidth:promoMaxWidth];
462 }
463
464 - (void)updateLogoAndFakeboxDisplay {
465 if (self.logoVendor.showingLogo != self.logoIsShowing) {
466 self.logoVendor.showingLogo = self.logoIsShowing;
467 if (_viewLoaded) {
468 [self updateSubviewFrames];
469
470 // Adjust the height of |_headerView| to fit its content which may have
471 // been shifted due to the visibility of the doodle.
472 CGRect headerFrame = [_headerView frame];
473 headerFrame.size.height = [self heightForSectionWithOmnibox];
474 [_headerView setFrame:headerFrame];
475
476 // Adjust vertical positioning of |_promoHeaderView|.
477 CGFloat omniboxHeaderHeight =
478 [self collectionView:_mostVisitedView
479 layout:[_mostVisitedView
480 collectionViewLayout]
481 referenceSizeForHeaderInSection:0]
482 .height;
483 CGRect whatsNewFrame = [_promoHeaderView frame];
484 whatsNewFrame.origin.y = omniboxHeaderHeight;
485 [_promoHeaderView setFrame:whatsNewFrame];
486 }
487 if (IsIPadIdiom())
488 [_searchTapTarget setHidden:!self.logoIsShowing];
489 }
490 }
491
492 // Initialize and add a search field tap target and a voice search button.
493 - (void)addSearchField {
494 CGRect searchFieldFrame = [self searchFieldFrame];
495 _searchTapTarget.reset([[UIButton alloc] initWithFrame:searchFieldFrame]);
496 if (IsIPadIdiom()) {
497 UIImage* searchBoxImage = [[UIImage imageNamed:@"ntp_google_search_box"]
498 resizableImageWithCapInsets:kSearchBoxStretchInsets];
499 [_searchTapTarget setBackgroundImage:searchBoxImage
500 forState:UIControlStateNormal];
501 }
502 [_searchTapTarget setAdjustsImageWhenHighlighted:NO];
503 [_searchTapTarget addTarget:self
504 action:@selector(searchFieldTapped:)
505 forControlEvents:UIControlEventTouchUpInside];
506 [_searchTapTarget
507 setAccessibilityLabel:l10n_util::GetNSString(IDS_OMNIBOX_EMPTY_HINT)];
508 // Set isAccessibilityElement to NO so that Voice Search button is accessible.
509 [_searchTapTarget setIsAccessibilityElement:NO];
510
511 // Set up fakebox hint label.
512 CGRect hintFrame = CGRectInset([_searchTapTarget bounds], 12, 3);
513 const CGFloat kVoiceSearchOffset = 48;
514 hintFrame.size.width = searchFieldFrame.size.width - kVoiceSearchOffset;
515 base::scoped_nsobject<UILabel> searchHintLabel(
516 [[UILabel alloc] initWithFrame:hintFrame]);
517 [_searchTapTarget addSubview:searchHintLabel];
518 [searchHintLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
519 [searchHintLabel
520 addConstraint:[NSLayoutConstraint
521 constraintWithItem:searchHintLabel
522 attribute:NSLayoutAttributeHeight
523 relatedBy:NSLayoutRelationEqual
524 toItem:nil
525 attribute:NSLayoutAttributeNotAnAttribute
526 multiplier:1
527 constant:hintFrame.size.height]];
528 [_searchTapTarget
529 addConstraint:[NSLayoutConstraint
530 constraintWithItem:searchHintLabel
531 attribute:NSLayoutAttributeCenterY
532 relatedBy:NSLayoutRelationEqual
533 toItem:_searchTapTarget
534 attribute:NSLayoutAttributeCenterY
535 multiplier:1
536 constant:0]];
537 _hintLabelLeadingConstraint.reset(
538 [[NSLayoutConstraint constraintWithItem:searchHintLabel
539 attribute:NSLayoutAttributeLeading
540 relatedBy:NSLayoutRelationEqual
541 toItem:_searchTapTarget
542 attribute:NSLayoutAttributeLeading
543 multiplier:1
544 constant:kHintLabelSidePadding] retain]);
545 [_searchTapTarget addConstraint:_hintLabelLeadingConstraint];
546 [searchHintLabel setText:l10n_util::GetNSString(IDS_OMNIBOX_EMPTY_HINT)];
547 if (base::i18n::IsRTL()) {
548 [searchHintLabel setTextAlignment:NSTextAlignmentRight];
549 }
550 [searchHintLabel
551 setTextColor:[UIColor
552 colorWithWhite:kiPhoneOmniboxPlaceholderColorBrightness
553 alpha:1.0]];
554 [searchHintLabel setFont:[MDCTypography subheadFont]];
555
556 // Add a voice search button.
557 UIImage* micImage = [UIImage imageNamed:@"voice_icon"];
558 base::scoped_nsobject<UIButton> voiceTapTarget(
559 [[UIButton alloc] initWithFrame:CGRectZero]);
560 [_searchTapTarget addSubview:voiceTapTarget];
561
562 [voiceTapTarget setTranslatesAutoresizingMaskIntoConstraints:NO];
563 [_searchTapTarget
564 addConstraint:[NSLayoutConstraint
565 constraintWithItem:voiceTapTarget
566 attribute:NSLayoutAttributeCenterY
567 relatedBy:NSLayoutRelationEqual
568 toItem:_searchTapTarget
569 attribute:NSLayoutAttributeCenterY
570 multiplier:1
571 constant:0]];
572 _voiceTapTrailingConstraint.reset(
573 [[NSLayoutConstraint constraintWithItem:voiceTapTarget
574 attribute:NSLayoutAttributeTrailing
575 relatedBy:NSLayoutRelationEqual
576 toItem:_searchTapTarget
577 attribute:NSLayoutAttributeTrailing
578 multiplier:1
579 constant:0] retain]);
580 [_searchTapTarget addConstraint:_voiceTapTrailingConstraint];
581 [voiceTapTarget
582 addConstraint:[NSLayoutConstraint
583 constraintWithItem:voiceTapTarget
584 attribute:NSLayoutAttributeHeight
585 relatedBy:NSLayoutRelationEqual
586 toItem:nil
587 attribute:NSLayoutAttributeNotAnAttribute
588 multiplier:0
589 constant:kVoiceSearchButtonWidth]];
590 [voiceTapTarget
591 addConstraint:[NSLayoutConstraint
592 constraintWithItem:voiceTapTarget
593 attribute:NSLayoutAttributeWidth
594 relatedBy:NSLayoutRelationEqual
595 toItem:nil
596 attribute:NSLayoutAttributeNotAnAttribute
597 multiplier:0
598 constant:kVoiceSearchButtonWidth]];
599 [_searchTapTarget
600 addConstraint:[NSLayoutConstraint
601 constraintWithItem:searchHintLabel
602 attribute:NSLayoutAttributeTrailing
603 relatedBy:NSLayoutRelationEqual
604 toItem:voiceTapTarget
605 attribute:NSLayoutAttributeLeading
606 multiplier:1
607 constant:0]];
608 [voiceTapTarget setAdjustsImageWhenHighlighted:NO];
609 [voiceTapTarget setImage:micImage forState:UIControlStateNormal];
610 [voiceTapTarget setTag:IDC_VOICE_SEARCH];
611 [voiceTapTarget setAccessibilityLabel:l10n_util::GetNSString(
612 IDS_IOS_ACCNAME_VOICE_SEARCH)];
613 [voiceTapTarget setAccessibilityIdentifier:@"Voice Search"];
614
615 if (self.voiceSearchIsEnabled) {
616 [voiceTapTarget addTarget:self
617 action:@selector(loadVoiceSearch:)
618 forControlEvents:UIControlEventTouchUpInside];
619 [voiceTapTarget addTarget:self
620 action:@selector(preloadVoiceSearch:)
621 forControlEvents:UIControlEventTouchDown];
622 } else {
623 [voiceTapTarget setEnabled:NO];
624 }
625 }
626
627 - (void)loadVoiceSearch:(id)sender {
628 DCHECK(self.voiceSearchIsEnabled);
629 base::RecordAction(UserMetricsAction("MobileNTPMostVisitedVoiceSearch"));
630 [sender chromeExecuteCommand:sender];
631 }
632
633 - (void)preloadVoiceSearch:(id)sender {
634 DCHECK(self.voiceSearchIsEnabled);
635 [sender removeTarget:self
636 action:@selector(preloadVoiceSearch:)
637 forControlEvents:UIControlEventTouchDown];
638
639 // Use a GenericChromeCommand because |sender| already has a tag set for a
640 // different command.
641 base::scoped_nsobject<GenericChromeCommand> command(
642 [[GenericChromeCommand alloc] initWithTag:IDC_PRELOAD_VOICE_SEARCH]);
643 [sender chromeExecuteCommand:command];
644 }
645
646 - (void)setFlowLayoutInset:(UICollectionViewFlowLayout*)layout {
647 CGFloat leftMargin = [self leftMargin];
648 [layout setSectionInset:UIEdgeInsetsMake(0, leftMargin, 0, leftMargin)];
649 }
650
651 - (void)resetSectionInset {
652 UICollectionViewFlowLayout* flowLayout =
653 (UICollectionViewFlowLayout*)[_mostVisitedView collectionViewLayout];
654 [flowLayout setSectionInset:UIEdgeInsetsZero];
655 }
656
657 - (void)updateSubviewFrames {
658 _mostVisitedCellSize = [self mostVisitedCellSize];
659 UICollectionViewFlowLayout* flowLayout =
660 base::mac::ObjCCastStrict<UICollectionViewFlowLayout>(
661 [_mostVisitedView collectionViewLayout]);
662 [flowLayout setItemSize:_mostVisitedCellSize];
663 self.logoVendor.view.frame = [self doodleFrame];
664
665 [self setFlowLayoutInset:flowLayout];
666 [flowLayout invalidateLayout];
667 [_promoHeaderView setSideMargin:[self leftMargin]];
668
669 // On the iPhone 6 Plus, if the app is started in landscape after a fresh
670 // install, the UICollectionViewLayout incorrectly sizes the widths of the
671 // supplementary views to the portrait width. Correct that here to ensure
672 // that the header is property laid out to the UICollectionView's width.
673 // crbug.com/491131
674 CGFloat collectionViewWidth = CGRectGetWidth([_mostVisitedView bounds]);
675 CGFloat collectionViewMinX = CGRectGetMinX([_mostVisitedView bounds]);
676 for (UIView* supplementaryView in _supplementaryViews.get()) {
677 CGRect supplementaryViewFrame = supplementaryView.frame;
678 supplementaryViewFrame.origin.x = collectionViewMinX;
679 supplementaryViewFrame.size.width = collectionViewWidth;
680 supplementaryView.frame = supplementaryViewFrame;
681 }
682
683 BOOL isScrollableNTP = !IsIPadIdiom() || IsCompactTablet();
684 if (isScrollableNTP && _scrolledToTop) {
685 // Set the scroll view's offset to the pinned offset to keep the omnibox
686 // at the top of the screen if it isn't already.
687 CGFloat pinnedOffsetY = [self pinnedOffsetY];
688 if ([_mostVisitedView contentOffset].y < pinnedOffsetY) {
689 [_mostVisitedView setContentOffset:CGPointMake(0, pinnedOffsetY)];
690 } else {
691 [self updateSearchField];
692 }
693 } else {
694 [_searchTapTarget setFrame:[self searchFieldFrame]];
695 }
696
697 if (!_viewLoaded) {
698 _viewLoaded = YES;
699 [self.logoVendor fetchDoodle];
700 }
701 [self.delegate updateNtpBarShadowForPanelController:self];
702 }
703
704 // Initialize and add a panel with most visited sites.
705 - (void)addMostVisited {
706 CGRect mostVisitedFrame = [self.view bounds];
707 base::scoped_nsobject<UICollectionViewFlowLayout> flowLayout;
708 if (IsIPadIdiom())
709 flowLayout.reset([[UICollectionViewFlowLayout alloc] init]);
710 else
711 flowLayout.reset([[MostVisitedLayout alloc] init]);
712
713 [flowLayout setScrollDirection:UICollectionViewScrollDirectionVertical];
714 [flowLayout setItemSize:_mostVisitedCellSize];
715 [flowLayout setMinimumInteritemSpacing:8];
716 [flowLayout setMinimumLineSpacing:[self mostVisitedCellPadding]];
717 DCHECK(!_mostVisitedView);
718 _mostVisitedView.reset([[UICollectionView alloc]
719 initWithFrame:mostVisitedFrame
720 collectionViewLayout:flowLayout]);
721 [_mostVisitedView setAutoresizingMask:UIViewAutoresizingFlexibleHeight |
722 UIViewAutoresizingFlexibleWidth];
723 [_mostVisitedView setDelegate:self];
724 [_mostVisitedView setDataSource:self];
725 [_mostVisitedView registerClass:[MostVisitedCell class]
726 forCellWithReuseIdentifier:@"classCell"];
727 [_mostVisitedView setBackgroundColor:[UIColor clearColor]];
728 [_mostVisitedView setBounces:YES];
729 [_mostVisitedView setShowsHorizontalScrollIndicator:NO];
730 [_mostVisitedView setShowsVerticalScrollIndicator:NO];
731 [_mostVisitedView registerClass:[WhatsNewHeaderView class]
732 forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
733 withReuseIdentifier:@"whatsNew"];
734 [_mostVisitedView registerClass:[NewTabPageHeaderView class]
735 forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
736 withReuseIdentifier:@"header"];
737 [_mostVisitedView setAccessibilityIdentifier:@"Google Landing"];
738
739 [self.view addSubview:_mostVisitedView];
740 }
741
742 - (void)updateSearchField {
743 NSArray* constraints =
744 @[ _hintLabelLeadingConstraint, _voiceTapTrailingConstraint ];
745 [_headerView updateSearchField:_searchTapTarget
746 withInitialFrame:[self searchFieldFrame]
747 subviewConstraints:constraints
748 forOffset:[_mostVisitedView contentOffset].y];
749 }
750
751 - (void)addOverscrollActions {
752 if (!IsIPadIdiom()) {
753 _overscrollActionsController.reset([[OverscrollActionsController alloc]
754 initWithScrollView:_mostVisitedView]);
755 [_overscrollActionsController setStyle:OverscrollStyle::NTP_NON_INCOGNITO];
756 [_overscrollActionsController setDelegate:self];
757 }
758 }
759
760 // Check to see if the promo label should be hidden.
761 - (void)hideWhatsNewIfNecessary {
762 if (![_promoHeaderView isHidden] && !self.promoCanShow) {
763 [_promoHeaderView setHidden:YES];
764 [self.view setNeedsLayout];
765 }
766 }
767
768 - (void)locationBarBecomesFirstResponder {
769 if (!_isShowing)
770 return;
771
772 _omniboxFocused = YES;
773 [self shiftTilesUp];
774 }
775
776 - (void)shiftTilesUp {
777 _scrolledToTop = YES;
778 // Add gesture recognizer to background |self.view| when omnibox is focused.
779 [self.view addGestureRecognizer:_tapGestureRecognizer];
780 [self.view addGestureRecognizer:_swipeGestureRecognizer];
781
782 CGFloat pinnedOffsetY = [self pinnedOffsetY];
783 _animateHeader = !IsIPadIdiom();
784
785 [UIView animateWithDuration:0.25
786 animations:^{
787 if ([_mostVisitedView contentOffset].y < pinnedOffsetY) {
788 [_mostVisitedView setContentOffset:CGPointMake(0, pinnedOffsetY)];
789 [[_mostVisitedView collectionViewLayout] invalidateLayout];
790 }
791 }
792 completion:^(BOOL finished) {
793 // Check to see if we are still scrolled to the top -- it's possible
794 // (and difficult) to resign the first responder and initiate a
795 // -shiftTilesDown before the animation here completes.
796 if (_scrolledToTop) {
797 _animateHeader = NO;
798 if (!IsIPadIdiom()) {
799 [self.dispatcher onFakeboxAnimationComplete];
800 [_headerView fadeOutShadow];
801 [_searchTapTarget setHidden:YES];
802 }
803 }
804 }];
805 }
806
807 - (void)searchFieldTapped:(id)sender {
808 [self.dispatcher focusFakebox];
809 }
810
811 - (void)blurOmnibox {
812 if (_omniboxFocused) {
813 [self.dispatcher cancelOmniboxEdit];
814 } else {
815 [self locationBarResignsFirstResponder];
816 }
817 }
818
819 - (void)locationBarResignsFirstResponder {
820 if (!_isShowing && !_scrolledToTop)
821 return;
822
823 _omniboxFocused = NO;
824 if ([_contextMenuCoordinator isVisible]) {
825 return;
826 }
827
828 [self shiftTilesDown];
829 }
830
831 - (void)shiftTilesDown {
832 _animateHeader = YES;
833 _scrolledToTop = NO;
834 if (!IsIPadIdiom()) {
835 [_searchTapTarget setHidden:NO];
836 [self.dispatcher onFakeboxBlur];
837 }
838
839 // Reload most visited sites in case the number of placeholder cells needs to
840 // be updated after an orientation change.
841 [_mostVisitedView reloadData];
842
843 // Reshow views that are within range of the most visited collection view
844 // (if necessary).
845 [self.view removeGestureRecognizer:_tapGestureRecognizer];
846 [self.view removeGestureRecognizer:_swipeGestureRecognizer];
847
848 // CADisplayLink is used for this animation instead of the standard UIView
849 // animation because the standard animation did not properly convert the
850 // fakebox from its scrolled up mode to its scrolled down mode. Specifically,
851 // calling |UICollectionView reloadData| adjacent to the standard animation
852 // caused the fakebox's views to jump incorrectly. CADisplayLink avoids this
853 // problem because it allows |shiftTilesDownAnimationDidFire| to directly
854 // control each frame.
855 CADisplayLink* link = [CADisplayLink
856 displayLinkWithTarget:self
857 selector:@selector(shiftTilesDownAnimationDidFire:)];
858 [link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
859
860 // Dismisses modal UI elements if displayed. Must be called at the end of
861 // -locationBarResignsFirstResponder since it could result in -dealloc being
862 // called.
863 [self dismissModals];
864 }
865
866 - (void)shiftTilesDownAnimationDidFire:(CADisplayLink*)link {
867 // If this is the first frame of the animation, store the starting timestamp
868 // and do nothing.
869 if (_shiftTilesDownStartTime == -1) {
870 _shiftTilesDownStartTime = link.timestamp;
871 return;
872 }
873
874 CFTimeInterval timeElapsed = link.timestamp - _shiftTilesDownStartTime;
875 double percentComplete = timeElapsed / kShiftTilesDownAnimationDuration;
876 // Ensure that the percentage cannot be above 1.0.
877 if (percentComplete > 1.0)
878 percentComplete = 1.0;
879
880 // Find how much the collection view should be scrolled up in the next frame.
881 CGFloat yOffset = (1.0 - percentComplete) * [self pinnedOffsetY];
882 [_mostVisitedView setContentOffset:CGPointMake(0, yOffset)];
883
884 if (percentComplete == 1.0) {
885 [link invalidate];
886 // Reset |shiftTilesDownStartTime to its sentinal value.
887 _shiftTilesDownStartTime = -1;
888 [[_mostVisitedView collectionViewLayout] invalidateLayout];
889 }
890 }
891
892 - (void)reloadData {
893 // -reloadData updates from |self.mostVisitedData|.
894 // -invalidateLayout is necessary because sometimes the flowLayout has the
895 // wrong cached size and will throw an internal exception if the
896 // -numberOfItems shrinks. -setNeedsLayout is needed in case
897 // -numberOfItems increases enough to add a new row and change the height
898 // of _mostVisitedView.
899 [_mostVisitedView reloadData];
900 [[_mostVisitedView collectionViewLayout] invalidateLayout];
901 [self.view setNeedsLayout];
902 }
903
904 - (CGFloat)heightForSectionWithOmnibox {
905 CGFloat headerHeight =
906 CGRectGetMaxY([self searchFieldFrame]) + kNTPSearchFieldBottomPadding;
907 if (IsIPadIdiom()) {
908 if (self.logoIsShowing) {
909 if (!self.promoCanShow) {
910 UIInterfaceOrientation orient =
911 [[UIApplication sharedApplication] statusBarOrientation];
912 const CGFloat kTopSpacingMaterialPortrait = 56;
913 const CGFloat kTopSpacingMaterialLandscape = 32;
914 headerHeight += UIInterfaceOrientationIsPortrait(orient)
915 ? kTopSpacingMaterialPortrait
916 : kTopSpacingMaterialLandscape;
917 }
918 } else {
919 headerHeight = kNonGoogleSearchHeaderHeightIPad;
920 }
921 }
922 return headerHeight;
923 }
924
925 #pragma mark - ToolbarOwner
926
927 - (ToolbarController*)relinquishedToolbarController {
928 return [_headerView relinquishedToolbarController];
929 }
930
931 - (void)reparentToolbarController {
932 [_headerView reparentToolbarController];
933 }
934
935 #pragma mark - UICollectionView Methods.
936
937 - (CGSize)collectionView:(UICollectionView*)collectionView
938 layout:
939 (UICollectionViewLayout*)collectionViewLayout
940 referenceSizeForHeaderInSection:(NSInteger)section {
941 CGFloat headerHeight = 0;
942 if (section == SectionWithOmnibox) {
943 headerHeight = [self heightForSectionWithOmnibox];
944 ((UICollectionViewFlowLayout*)collectionViewLayout).headerReferenceSize =
945 CGSizeMake(0, headerHeight);
946 } else if (section == SectionWithMostVisited) {
947 if (self.promoCanShow) {
948 headerHeight = [self promoHeaderHeight];
949 } else {
950 headerHeight = kWhatsNewHeaderHiddenHeight;
951 }
952 }
953 return CGSizeMake(0, headerHeight);
954 }
955
956 #pragma mark - UICollectionViewDelegate
957
958 - (BOOL)collectionView:(UICollectionView*)collectionView
959 shouldSelectItemAtIndexPath:(NSIndexPath*)indexPath {
960 return indexPath.row < static_cast<NSInteger>([self numberOfItems]);
961 }
962
963 - (void)collectionView:(UICollectionView*)collectionView
964 didSelectItemAtIndexPath:(NSIndexPath*)indexPath {
965 MostVisitedCell* cell =
966 (MostVisitedCell*)[collectionView cellForItemAtIndexPath:indexPath];
967
968 // Keep the UICollectionView alive for one second while the screen
969 // reader does its thing.
970 // TODO(jif): This needs a radar, since it is almost certainly a
971 // UIKit accessibility bug. crbug.com/529271
972 if (UIAccessibilityIsVoiceOverRunning()) {
973 UICollectionView* blockView = [_mostVisitedView retain];
974 dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
975 static_cast<int64_t>(1 * NSEC_PER_SEC)),
976 dispatch_get_main_queue(), ^{
977 [blockView release];
978 });
979 }
980
981 const NSUInteger visitedIndex = indexPath.row;
982 [self blurOmnibox];
983 DCHECK(visitedIndex < [self numberOfItems]);
984 [self.dataSource logMostVisitedClick:visitedIndex tileType:cell.tileType];
985 [self.dispatcher loadURL:[self urlForIndex:visitedIndex]
986 referrer:web::Referrer()
987 transition:ui::PAGE_TRANSITION_AUTO_BOOKMARK
988 rendererInitiated:NO];
989 }
990
991 #pragma mark - UICollectionViewDataSource
992
993 - (UICollectionReusableView*)collectionView:(UICollectionView*)collectionView
994 viewForSupplementaryElementOfKind:(NSString*)kind
995 atIndexPath:(NSIndexPath*)indexPath {
996 DCHECK(kind == UICollectionElementKindSectionHeader);
997
998 if (!_supplementaryViews)
999 _supplementaryViews.reset([[NSMutableArray alloc] init]);
1000
1001 if (indexPath.section == SectionWithOmnibox) {
1002 if (!_headerView) {
1003 _headerView.reset([[collectionView
1004 dequeueReusableSupplementaryViewOfKind:
1005 UICollectionElementKindSectionHeader
1006 withReuseIdentifier:@"header"
1007 forIndexPath:indexPath] retain]);
1008 [_headerView addSubview:[self.logoVendor view]];
1009 [_headerView addSubview:_searchTapTarget];
1010 [_headerView addViewsToSearchField:_searchTapTarget];
1011
1012 if (!IsIPadIdiom()) {
1013 // iPhone header also contains a toolbar since the normal toolbar is
1014 // hidden.
1015 [_headerView addToolbarWithDataSource:self.dataSource
1016 dispatcher:self.dispatcher];
1017 [_headerView setToolbarTabCount:self.tabCount];
1018 [_headerView setCanGoForward:self.canGoForward];
1019 [_headerView setCanGoBack:self.canGoBack];
1020 }
1021 [_supplementaryViews addObject:_headerView];
1022 }
1023 return _headerView;
1024 }
1025
1026 if (indexPath.section == SectionWithMostVisited) {
1027 if (!_promoHeaderView) {
1028 _promoHeaderView.reset([[collectionView
1029 dequeueReusableSupplementaryViewOfKind:
1030 UICollectionElementKindSectionHeader
1031 withReuseIdentifier:@"whatsNew"
1032 forIndexPath:indexPath] retain]);
1033 [_promoHeaderView setSideMargin:[self leftMargin]];
1034 [_promoHeaderView setDelegate:self];
1035 if (self.promoCanShow) {
1036 [_promoHeaderView setText:self.promoText];
1037 [_promoHeaderView setIcon:self.promoIcon];
1038 [self.dataSource promoViewed];
1039 }
1040 [_supplementaryViews addObject:_promoHeaderView];
1041 }
1042 return _promoHeaderView;
1043 }
1044
1045 NOTREACHED();
1046 return nil;
1047 }
1048
1049 - (NSInteger)numberOfSectionsInCollectionView:
1050 (UICollectionView*)collectionView {
1051 return NumberOfCollectionViewSections;
1052 }
1053
1054 - (NSInteger)collectionView:(UICollectionView*)collectionView
1055 numberOfItemsInSection:(NSInteger)section {
1056 // The first section only contains a header view and no items.
1057 if (section == SectionWithOmnibox)
1058 return 0;
1059
1060 // Phone always contains the maximum number of cells. Cells in excess of the
1061 // number of thumbnails are used solely for layout/sizing.
1062 if (!IsIPadIdiom())
1063 return self.maximumMostVisitedSitesShown;
1064
1065 return [self numberOfNonEmptyTilesShown];
1066 }
1067
1068 - (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView
1069 cellForItemAtIndexPath:(NSIndexPath*)indexPath {
1070 MostVisitedCell* cell = (MostVisitedCell*)[collectionView
1071 dequeueReusableCellWithReuseIdentifier:@"classCell"
1072 forIndexPath:indexPath];
1073 BOOL isPlaceholder = indexPath.row >= (int)[self numberOfItems];
1074 if (isPlaceholder) {
1075 [cell showPlaceholder];
1076 for (UIGestureRecognizer* ges in cell.gestureRecognizers) {
1077 [cell removeGestureRecognizer:ges];
1078 }
1079
1080 // When -numberOfItems is 0, always remove the placeholder.
1081 if (indexPath.row >= [self numberOfColumns] || [self numberOfItems] == 0) {
1082 // This cell is completely empty and only exists for layout/sizing
1083 // purposes.
1084 [cell removePlaceholderImage];
1085 }
1086 return cell;
1087 }
1088
1089 const ntp_tiles::NTPTile& ntpTile =
1090 [self.dataSource mostVisitedAtIndex:indexPath.row];
1091 NSString* title = base::SysUTF16ToNSString(ntpTile.title);
1092
1093 [cell setupWithURL:ntpTile.url title:title dataSource:self.dataSource];
1094
1095 base::scoped_nsobject<UILongPressGestureRecognizer> longPress(
1096 [[UILongPressGestureRecognizer alloc]
1097 initWithTarget:self
1098 action:@selector(handleMostVisitedLongPress:)]);
1099 [cell addGestureRecognizer:longPress];
1100
1101 return cell;
1102 }
1103
1104 #pragma mark - Context Menu
1105
1106 // Called when a user does a long press on a most visited item.
1107 - (void)handleMostVisitedLongPress:(UILongPressGestureRecognizer*)sender {
1108 if (sender.state == UIGestureRecognizerStateBegan) {
1109 // Only one long press at a time.
1110 if ([_contextMenuCoordinator isVisible]) {
1111 return;
1112 }
1113
1114 NSIndexPath* indexPath = [_mostVisitedView
1115 indexPathForCell:static_cast<UICollectionViewCell*>(sender.view)];
1116 const NSUInteger index = indexPath.row;
1117
1118 // A long press occured on one of the most visited button. Popup a context
1119 // menu.
1120 DCHECK(index < [self numberOfItems]);
1121
1122 web::ContextMenuParams params;
1123 // Get view coordinates in local space.
1124 params.location = [sender locationInView:self.view];
1125 params.view.reset([self.view retain]);
1126
1127 // Present sheet/popover using controller that is added to view hierarchy.
1128 UIViewController* topController = [params.view window].rootViewController;
1129 while (topController.presentedViewController)
1130 topController = topController.presentedViewController;
1131
1132 _contextMenuCoordinator.reset([[ContextMenuCoordinator alloc]
1133 initWithBaseViewController:topController
1134 params:params]);
1135
1136 ProceduralBlock action;
1137
1138 // Open In New Tab.
1139 GURL url = [self urlForIndex:index];
1140 base::WeakNSObject<GoogleLandingController> weakSelf(self);
1141 action = ^{
1142 base::scoped_nsobject<GoogleLandingController> strongSelf(
1143 [weakSelf retain]);
1144 if (!strongSelf)
1145 return;
1146 MostVisitedCell* cell = (MostVisitedCell*)sender.view;
1147 [[strongSelf dataSource] logMostVisitedClick:index
1148 tileType:cell.tileType];
1149 [[strongSelf dispatcher] webPageOrderedOpen:url
1150 referrer:web::Referrer()
1151 inBackground:YES
1152 appendTo:kCurrentTab];
1153 };
1154 [_contextMenuCoordinator
1155 addItemWithTitle:l10n_util::GetNSStringWithFixup(
1156 IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWTAB)
1157 action:action];
1158
1159 if (!self.isOffTheRecord) {
1160 // Open in Incognito Tab.
1161 action = ^{
1162 base::scoped_nsobject<GoogleLandingController> strongSelf(
1163 [weakSelf retain]);
1164 if (!strongSelf)
1165 return;
1166 MostVisitedCell* cell = (MostVisitedCell*)sender.view;
1167 [[strongSelf dataSource] logMostVisitedClick:index
1168 tileType:cell.tileType];
1169 [[strongSelf dispatcher] webPageOrderedOpen:url
1170 referrer:web::Referrer()
1171 inIncognito:YES
1172 inBackground:NO
1173 appendTo:kCurrentTab];
1174 };
1175 [_contextMenuCoordinator
1176 addItemWithTitle:l10n_util::GetNSStringWithFixup(
1177 IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWINCOGNITOTAB)
1178 action:action];
1179 }
1180
1181 // Remove the most visited url.
1182 NSString* title =
1183 l10n_util::GetNSStringWithFixup(IDS_BOOKMARK_BUBBLE_REMOVE_BOOKMARK);
1184 action = ^{
1185 base::scoped_nsobject<GoogleLandingController> strongSelf(
1186 [weakSelf retain]);
1187 // Early return if the controller has been deallocated.
1188 if (!strongSelf)
1189 return;
1190 base::RecordAction(UserMetricsAction("MostVisited_UrlBlacklisted"));
1191 [[strongSelf dataSource] addBlacklistedURL:url];
1192 [strongSelf showMostVisitedUndoForURL:net::NSURLWithGURL(url)];
1193 };
1194 [_contextMenuCoordinator addItemWithTitle:title action:action];
1195
1196 [_contextMenuCoordinator start];
1197
1198 if (IsIPadIdiom())
1199 [self blurOmnibox];
1200 }
1201 }
1202
1203 - (void)showMostVisitedUndoForURL:(NSURL*)url {
1204 _deletedUrl.reset([url retain]);
1205
1206 MDCSnackbarMessageAction* action =
1207 [[[MDCSnackbarMessageAction alloc] init] autorelease];
1208 base::WeakNSObject<GoogleLandingController> weakSelf(self);
1209 action.handler = ^{
1210 base::scoped_nsobject<GoogleLandingController> strongSelf(
1211 [weakSelf retain]);
1212 if (!strongSelf)
1213 return;
1214 [[strongSelf dataSource]
1215 removeBlacklistedURL:net::GURLWithNSURL(_deletedUrl)];
1216 };
1217 action.title = l10n_util::GetNSString(IDS_NEW_TAB_UNDO_THUMBNAIL_REMOVE);
1218 action.accessibilityIdentifier = @"Undo";
1219
1220 TriggerHapticFeedbackForNotification(UINotificationFeedbackTypeSuccess);
1221 MDCSnackbarMessage* message = [MDCSnackbarMessage
1222 messageWithText:l10n_util::GetNSString(
1223 IDS_IOS_NEW_TAB_MOST_VISITED_ITEM_REMOVED)];
1224 message.action = action;
1225 message.category = @"MostVisitedUndo";
1226 [MDCSnackbarManager showMessage:message];
1227 }
1228
1229 - (void)onPromoLabelTapped {
1230 [self.dispatcher cancelOmniboxEdit];
1231 [_promoHeaderView setHidden:YES];
1232 [self.view setNeedsLayout];
1233 [self.dataSource promoTapped];
1234 }
1235
1236 // Returns the Y value to use for the scroll view's contentOffset when scrolling
1237 // the omnibox to the top of the screen.
1238 - (CGFloat)pinnedOffsetY {
1239 CGFloat headerHeight = [_headerView frame].size.height;
1240 CGFloat offsetY =
1241 headerHeight - ntp_header::kScrolledToTopOmniboxBottomMargin;
1242 if (!IsIPadIdiom())
1243 offsetY -= ntp_header::kToolbarHeight;
1244
1245 return offsetY;
1246 }
1247
1248 #pragma mark - NewTabPagePanelProtocol
1249
1250 - (void)reload {
1251 // Fetch the doodle after the view finishes laying out. Otherwise, tablet
1252 // may fetch the wrong sized doodle.
1253 if (_viewLoaded)
1254 [self.logoVendor fetchDoodle];
1255 [self updateLogoAndFakeboxDisplay];
1256 [self hideWhatsNewIfNecessary];
1257 }
1258
1259 - (void)wasShown {
1260 _isShowing = YES;
1261 [_headerView hideToolbarViewsForNewTabPage];
1262 }
1263
1264 - (void)wasHidden {
1265 _isShowing = NO;
1266 }
1267
1268 - (void)dismissModals {
1269 [_contextMenuCoordinator stop];
1270 }
1271
1272 - (void)dismissKeyboard {
1273 }
1274
1275 - (void)setScrollsToTop:(BOOL)enable {
1276 }
1277
1278 - (CGFloat)alphaForBottomShadow {
1279 // Get the frame of the bottommost cell in |self.view|'s coordinate system.
1280 NSInteger section = SectionWithMostVisited;
1281 // Account for the fact that the tableview may not yet contain
1282 // |numberOfNonEmptyTilesShown| tiles because it hasn't been updated yet.
1283 NSUInteger lastItemIndex =
1284 std::min([_mostVisitedView numberOfItemsInSection:SectionWithMostVisited],
1285 [self numberOfNonEmptyTilesShown]) -
1286 1;
1287 DCHECK(lastItemIndex >= 0);
1288 NSIndexPath* lastCellIndexPath =
1289 [NSIndexPath indexPathForItem:lastItemIndex inSection:section];
1290 UICollectionViewLayoutAttributes* attributes =
1291 [_mostVisitedView layoutAttributesForItemAtIndexPath:lastCellIndexPath];
1292 CGRect lastCellFrame = attributes.frame;
1293 CGRect cellFrameInSuperview =
1294 [_mostVisitedView convertRect:lastCellFrame toView:self.view];
1295
1296 // Calculate when the bottom of the cell passes through the bottom of
1297 // |self.view|.
1298 CGFloat maxY = CGRectGetMaxY(cellFrameInSuperview);
1299 CGFloat viewHeight = CGRectGetHeight(self.view.frame);
1300
1301 CGFloat pixelsBelowFrame = maxY - viewHeight;
1302 CGFloat alpha = pixelsBelowFrame / kNewTabPageDistanceToFadeShadow;
1303 alpha = MIN(MAX(alpha, 0), 1);
1304 return alpha;
1305 }
1306
1307 - (void)willUpdateSnapshot {
1308 [_overscrollActionsController clear];
1309 }
1310
1311 #pragma mark - LogoAnimationControllerOwnerOwner
1312
1313 - (id<LogoAnimationControllerOwner>)logoAnimationControllerOwner {
1314 return [self.logoVendor logoAnimationControllerOwner];
1315 }
1316
1317 #pragma mark - UIScrollViewDelegate Methods.
1318
1319 - (void)scrollViewDidScroll:(UIScrollView*)scrollView {
1320 [self.delegate updateNtpBarShadowForPanelController:self];
1321 [_overscrollActionsController scrollViewDidScroll:scrollView];
1322
1323 // Blur the omnibox when the scroll view is scrolled below the pinned offset.
1324 CGFloat pinnedOffsetY = [self pinnedOffsetY];
1325 if (_omniboxFocused && scrollView.dragging &&
1326 scrollView.contentOffset.y < pinnedOffsetY) {
1327 [self.dispatcher cancelOmniboxEdit];
1328 }
1329
1330 if (IsIPadIdiom()) {
1331 return;
1332 }
1333
1334 if (_animateHeader) {
1335 [self updateSearchField];
1336 }
1337 }
1338
1339 - (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView {
1340 [_overscrollActionsController scrollViewWillBeginDragging:scrollView];
1341 }
1342
1343 - (void)scrollViewDidEndDragging:(UIScrollView*)scrollView
1344 willDecelerate:(BOOL)decelerate {
1345 [_overscrollActionsController scrollViewDidEndDragging:scrollView
1346 willDecelerate:decelerate];
1347 }
1348
1349 - (void)scrollViewWillEndDragging:(UIScrollView*)scrollView
1350 withVelocity:(CGPoint)velocity
1351 targetContentOffset:(inout CGPoint*)targetContentOffset {
1352 [_overscrollActionsController scrollViewWillEndDragging:scrollView
1353 withVelocity:velocity
1354 targetContentOffset:targetContentOffset];
1355
1356 if (IsIPadIdiom() || _omniboxFocused)
1357 return;
1358
1359 CGFloat pinnedOffsetY = [self pinnedOffsetY];
1360 CGFloat offsetY = scrollView.contentOffset.y;
1361 CGFloat targetY = targetContentOffset->y;
1362 if (offsetY > 0 && offsetY < pinnedOffsetY) {
1363 // Omnibox is currently between middle and top of screen.
1364 if (velocity.y > 0) { // scrolling upwards
1365 if (targetY < pinnedOffsetY) {
1366 // Scroll the omnibox up to |pinnedOffsetY| if velocity is upwards but
1367 // scrolling will stop before reaching |pinnedOffsetY|.
1368 targetContentOffset->y = offsetY;
1369 [_mostVisitedView setContentOffset:CGPointMake(0, pinnedOffsetY)
1370 animated:YES];
1371 }
1372 _scrolledToTop = YES;
1373 } else { // scrolling downwards
1374 if (targetY > 0) {
1375 // Scroll the omnibox down to zero if velocity is downwards or 0 but
1376 // scrolling will stop before reaching 0.
1377 targetContentOffset->y = offsetY;
1378 [_mostVisitedView setContentOffset:CGPointZero animated:YES];
1379 }
1380 _scrolledToTop = NO;
1381 }
1382 } else if (offsetY > pinnedOffsetY &&
1383 targetContentOffset->y < pinnedOffsetY) {
1384 // Most visited cells are currently scrolled up past the omnibox but will
1385 // end the scroll below the omnibox. Stop the scroll at just below the
1386 // omnibox.
1387 targetContentOffset->y = offsetY;
1388 [_mostVisitedView setContentOffset:CGPointMake(0, pinnedOffsetY)
1389 animated:YES];
1390 _scrolledToTop = YES;
1391 } else if (offsetY >= pinnedOffsetY) {
1392 _scrolledToTop = YES;
1393 } else if (offsetY <= 0) {
1394 _scrolledToTop = NO;
1395 }
1396 }
1397
1398 #pragma mark - Most visited / Suggestions service wrapper methods.
1399
1400 - (NSUInteger)numberOfItems {
1401 NSUInteger numItems = [self.dataSource mostVisitedSize];
1402 NSUInteger maxItems = [self numberOfColumns] * kMaxNumMostVisitedFaviconRows;
1403 return MIN(maxItems, numItems);
1404 }
1405
1406 - (NSInteger)numberOfNonEmptyTilesShown {
1407 NSInteger numCells =
1408 MIN([self numberOfItems], self.maximumMostVisitedSitesShown);
1409 return MAX(numCells, [self numberOfColumns]);
1410 }
1411
1412 - (GURL)urlForIndex:(NSUInteger)index {
1413 return [self.dataSource mostVisitedAtIndex:index].url;
1414 }
1415
1416 #pragma mark - GoogleLandingController (ExposedForTesting) methods.
1417
1418 - (BOOL)scrolledToTop {
1419 return _scrolledToTop;
1420 }
1421
1422 - (BOOL)animateHeader {
1423 return _animateHeader;
1424 }
1425
1426 #pragma mark - OverscrollActionsControllerDelegate
1427
1428 - (void)overscrollActionsController:(OverscrollActionsController*)controller
1429 didTriggerAction:(OverscrollAction)action {
1430 switch (action) {
1431 case OverscrollAction::NEW_TAB: {
1432 base::scoped_nsobject<GenericChromeCommand> command(
1433 [[GenericChromeCommand alloc] initWithTag:IDC_NEW_TAB]);
1434 [[self view] chromeExecuteCommand:command];
1435 } break;
1436 case OverscrollAction::CLOSE_TAB: {
1437 base::scoped_nsobject<GenericChromeCommand> command(
1438 [[GenericChromeCommand alloc] initWithTag:IDC_CLOSE_TAB]);
1439 [[self view] chromeExecuteCommand:command];
1440 } break;
1441 case OverscrollAction::REFRESH:
1442 [self reload];
1443 break;
1444 case OverscrollAction::NONE:
1445 NOTREACHED();
1446 break;
1447 }
1448 }
1449
1450 - (BOOL)shouldAllowOverscrollActions {
1451 return YES;
1452 }
1453
1454 - (UIView*)toolbarSnapshotView {
1455 return [[_headerView toolBarView] snapshotViewAfterScreenUpdates:NO];
1456 }
1457
1458 - (UIView*)headerView {
1459 return self.view;
1460 }
1461
1462 - (CGFloat)overscrollActionsControllerHeaderInset:
1463 (OverscrollActionsController*)controller {
1464 return 0;
1465 }
1466
1467 - (CGFloat)overscrollHeaderHeight {
1468 return [_headerView toolBarView].bounds.size.height;
1469 }
1470
1471 #pragma mark - UIGestureRecognizerDelegate
1472
1473 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
1474 shouldReceiveTouch:(UITouch*)touch {
1475 return [self nearestAncestorOfView:touch.view
1476 withClass:[MostVisitedCell class]] == nil;
1477 }
1478
1479 - (UIView*)nearestAncestorOfView:(UIView*)view withClass:(Class)aClass {
1480 if (!view) {
1481 return nil;
1482 }
1483 if ([view isKindOfClass:aClass]) {
1484 return view;
1485 }
1486 return [self nearestAncestorOfView:[view superview] withClass:aClass];
1487 }
1488
1489 #pragma mark - GoogleLandingConsumer
1490
1491 - (void)setLogoIsShowing:(BOOL)logoIsShowing {
1492 _logoIsShowing = logoIsShowing;
1493 [self updateLogoAndFakeboxDisplay];
1494 }
1495
1496 - (void)mostVisitedDataUpdated {
1497 [self reloadData];
1498 }
1499
1500 - (void)mostVisitedIconMadeAvailableAtIndex:(NSUInteger)index {
1501 if (index >= [self numberOfItems])
1502 return;
1503
1504 NSIndexPath* indexPath =
1505 [NSIndexPath indexPathForRow:index inSection:SectionWithMostVisited];
1506 [_mostVisitedView reloadItemsAtIndexPaths:@[ indexPath ]];
1507 }
1508
1509 - (void)setTabCount:(int)tabCount {
1510 _tabCount = tabCount;
1511 [_headerView setToolbarTabCount:self.tabCount];
1512 }
1513
1514 - (void)setCanGoForward:(BOOL)canGoForward {
1515 _canGoForward = canGoForward;
1516 [_headerView setCanGoForward:self.canGoForward];
1517 }
1518
1519 - (void)setCanGoBack:(BOOL)canGoBack {
1520 _canGoBack = canGoBack;
1521 [_headerView setCanGoBack:self.canGoBack];
1522 }
1523 @end
OLDNEW
« no previous file with comments | « ios/chrome/browser/ui/ntp/google_landing_controller.h ('k') | ios/chrome/browser/ui/ntp/google_landing_controller_unittest.mm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698