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

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

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