1// Copyright 2018 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/popup_menu/popup_menu_coordinator.h"
6
7#include "base/check.h"
8#include "base/metrics/histogram_macros.h"
9#include "base/metrics/user_metrics.h"
10#include "base/metrics/user_metrics_action.h"
11#include "ios/chrome/browser/application_context.h"
12#include "ios/chrome/browser/bookmarks/bookmark_model_factory.h"
13#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
14#include "ios/chrome/browser/feature_engagement/tracker_factory.h"
15#import "ios/chrome/browser/main/browser.h"
16#import "ios/chrome/browser/overlays/public/overlay_presenter.h"
17#include "ios/chrome/browser/reading_list/reading_list_model_factory.h"
18#import "ios/chrome/browser/search_engines/template_url_service_factory.h"
19#import "ios/chrome/browser/ui/browser_container/browser_container_mediator.h"
20#import "ios/chrome/browser/ui/bubble/bubble_presenter.h"
21#import "ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.h"
22#import "ios/chrome/browser/ui/commands/browser_commands.h"
23#import "ios/chrome/browser/ui/commands/command_dispatcher.h"
24#import "ios/chrome/browser/ui/commands/find_in_page_commands.h"
25#import "ios/chrome/browser/ui/commands/popup_menu_commands.h"
26#import "ios/chrome/browser/ui/popup_menu/popup_menu_action_handler.h"
27#import "ios/chrome/browser/ui/popup_menu/popup_menu_constants.h"
28#import "ios/chrome/browser/ui/popup_menu/popup_menu_mediator.h"
29#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_presenter.h"
30#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_presenter_delegate.h"
31#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_table_view_controller.h"
32#import "ios/chrome/browser/ui/presenters/contained_presenter_delegate.h"
33#import "ios/chrome/browser/ui/util/layout_guide_names.h"
34
35#if !defined(__has_feature) || !__has_feature(objc_arc)
36#error "This file requires ARC support."
37#endif
38
39namespace {
40// Returns the corresponding command type for a Popup menu |type|.
41PopupMenuCommandType CommandTypeFromPopupType(PopupMenuType type) {
42  if (type == PopupMenuTypeToolsMenu)
43    return PopupMenuCommandTypeToolsMenu;
44  return PopupMenuCommandTypeDefault;
45}
46}  // namespace
47
48@interface PopupMenuCoordinator () <PopupMenuCommands,
49                                    PopupMenuPresenterDelegate>
50
51// Presenter for the popup menu, managing the animations.
52@property(nonatomic, strong) PopupMenuPresenter* presenter;
53// Mediator for the popup menu.
54@property(nonatomic, strong) PopupMenuMediator* mediator;
55// Mediator to that alerts the main |mediator| when the web content area
56// is blocked by an overlay.
57@property(nonatomic, strong) BrowserContainerMediator* contentBlockerMediator;
58// ViewController for this mediator.
59@property(nonatomic, strong) PopupMenuTableViewController* viewController;
60// Handles user interaction with the popup menu items.
61@property(nonatomic, strong) PopupMenuActionHandler* actionHandler;
62// Time when the presentation of the popup menu is requested.
63@property(nonatomic, assign) NSTimeInterval requestStartTime;
64
65@end
66
67@implementation PopupMenuCoordinator
68
69@synthesize mediator = _mediator;
70@synthesize presenter = _presenter;
71@synthesize requestStartTime = _requestStartTime;
72@synthesize UIUpdater = _UIUpdater;
73@synthesize bubblePresenter = _bubblePresenter;
74@synthesize viewController = _viewController;
75
76#pragma mark - ChromeCoordinator
77
78- (void)start {
79  [self.browser->GetCommandDispatcher()
80      startDispatchingToTarget:self
81                   forProtocol:@protocol(PopupMenuCommands)];
82  NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
83  [defaultCenter addObserver:self
84                    selector:@selector(applicationDidEnterBackground:)
85                        name:UIApplicationDidEnterBackgroundNotification
86                      object:nil];
87}
88
89- (void)stop {
90  [self.browser->GetCommandDispatcher() stopDispatchingToTarget:self];
91  [self.mediator disconnect];
92  self.mediator = nil;
93  self.viewController = nil;
94}
95
96#pragma mark - Public
97
98- (BOOL)isShowingPopupMenu {
99  return self.presenter != nil;
100}
101
102#pragma mark - PopupMenuCommands
103
104- (void)showNavigationHistoryBackPopupMenu {
105  base::RecordAction(
106      base::UserMetricsAction("MobileToolbarShowTabHistoryMenu"));
107  [self presentPopupOfType:PopupMenuTypeNavigationBackward
108            fromNamedGuide:kBackButtonGuide];
109}
110
111- (void)showNavigationHistoryForwardPopupMenu {
112  base::RecordAction(
113      base::UserMetricsAction("MobileToolbarShowTabHistoryMenu"));
114  [self presentPopupOfType:PopupMenuTypeNavigationForward
115            fromNamedGuide:kForwardButtonGuide];
116}
117
118- (void)showToolsMenuPopup {
119  // The metric is registered at the toolbar level.
120  [self presentPopupOfType:PopupMenuTypeToolsMenu
121            fromNamedGuide:kToolsMenuGuide];
122}
123
124- (void)showTabGridButtonPopup {
125  base::RecordAction(base::UserMetricsAction("MobileToolbarShowTabGridMenu"));
126  [self presentPopupOfType:PopupMenuTypeTabGrid
127            fromNamedGuide:kTabSwitcherGuide];
128}
129
130- (void)showNewTabButtonPopup {
131  base::RecordAction(base::UserMetricsAction("MobileToolbarShowNewTabMenu"));
132  [self presentPopupOfType:PopupMenuTypeNewTab
133            fromNamedGuide:kNewTabButtonGuide];
134}
135
136- (void)dismissPopupMenuAnimated:(BOOL)animated {
137  [self.UIUpdater updateUIForMenuDismissed];
138  [self.presenter dismissAnimated:animated];
139  self.presenter = nil;
140  [self.mediator disconnect];
141  self.mediator = nil;
142  self.viewController = nil;
143}
144
145#pragma mark - PopupMenuLongPressDelegate
146
147- (void)longPressFocusPointChangedTo:(CGPoint)point {
148  [self.viewController focusRowAtPoint:point];
149}
150
151- (void)longPressEndedAtPoint:(CGPoint)point {
152  [self.viewController selectRowAtPoint:point];
153}
154
155#pragma mark - ContainedPresenterDelegate
156
157- (void)containedPresenterDidPresent:(id<ContainedPresenter>)presenter {
158  if (presenter != self.presenter)
159    return;
160
161  if (self.requestStartTime != 0) {
162    base::TimeDelta elapsed = base::TimeDelta::FromSecondsD(
163        [NSDate timeIntervalSinceReferenceDate] - self.requestStartTime);
164    UMA_HISTOGRAM_TIMES("Toolbar.ShowToolsMenuResponsiveness", elapsed);
165    // Reset the start time to ensure that whatever happens, we only record
166    // this once.
167    self.requestStartTime = 0;
168  }
169}
170
171#pragma mark - PopupMenuPresenterDelegate
172
173- (void)popupMenuPresenterWillDismiss:(PopupMenuPresenter*)presenter {
174  [self dismissPopupMenuAnimated:NO];
175}
176
177#pragma mark - Notification callback
178
179- (void)applicationDidEnterBackground:(NSNotification*)note {
180  [self dismissPopupMenuAnimated:NO];
181}
182
183#pragma mark - Private
184
185// Presents a popup menu of type |type| with an animation starting from
186// |guideName|.
187- (void)presentPopupOfType:(PopupMenuType)type
188            fromNamedGuide:(GuideName*)guideName {
189  if (self.presenter)
190    [self dismissPopupMenuAnimated:YES];
191
192  // TODO(crbug.com/1045047): Use HandlerForProtocol after commands protocol
193  // clean up.
194  id<BrowserCommands> callableDispatcher =
195      static_cast<id<BrowserCommands>>(self.browser->GetCommandDispatcher());
196  [callableDispatcher
197      prepareForPopupMenuPresentation:CommandTypeFromPopupType(type)];
198
199  self.requestStartTime = [NSDate timeIntervalSinceReferenceDate];
200
201  PopupMenuTableViewController* tableViewController =
202      [[PopupMenuTableViewController alloc] init];
203  tableViewController.baseViewController = self.baseViewController;
204  if (type == PopupMenuTypeToolsMenu) {
205    tableViewController.tableView.accessibilityIdentifier =
206        kPopupMenuToolsMenuTableViewId;
207  } else if (type == PopupMenuTypeNavigationBackward ||
208             type == PopupMenuTypeNavigationForward) {
209    tableViewController.tableView.accessibilityIdentifier =
210        kPopupMenuNavigationTableViewId;
211  }
212
213  self.viewController = tableViewController;
214
215  BOOL triggerNewIncognitoTabTip = NO;
216  if (type == PopupMenuTypeToolsMenu) {
217    triggerNewIncognitoTabTip =
218        self.bubblePresenter.incognitoTabTipBubblePresenter
219            .triggerFollowUpAction;
220    self.bubblePresenter.incognitoTabTipBubblePresenter.triggerFollowUpAction =
221        NO;
222  }
223
224  self.mediator = [[PopupMenuMediator alloc]
225                   initWithType:type
226                    isIncognito:self.browser->GetBrowserState()
227                                    ->IsOffTheRecord()
228               readingListModel:ReadingListModelFactory::GetForBrowserState(
229                                    self.browser->GetBrowserState())
230      triggerNewIncognitoTabTip:triggerNewIncognitoTabTip
231         browserPolicyConnector:GetApplicationContext()
232                                    ->GetBrowserPolicyConnector()];
233  self.mediator.engagementTracker =
234      feature_engagement::TrackerFactory::GetForBrowserState(
235          self.browser->GetBrowserState());
236  self.mediator.webStateList = self.browser->GetWebStateList();
237  self.mediator.dispatcher =
238      static_cast<id<BrowserCommands>>(self.browser->GetCommandDispatcher());
239  self.mediator.bookmarkModel = ios::BookmarkModelFactory::GetForBrowserState(
240      self.browser->GetBrowserState());
241  self.mediator.prefService = self.browser->GetBrowserState()->GetPrefs();
242  self.mediator.templateURLService =
243      ios::TemplateURLServiceFactory::GetForBrowserState(
244          self.browser->GetBrowserState());
245  self.mediator.popupMenu = tableViewController;
246  OverlayPresenter* overlayPresenter = OverlayPresenter::FromBrowser(
247      self.browser, OverlayModality::kWebContentArea);
248  self.mediator.webContentAreaOverlayPresenter = overlayPresenter;
249
250  self.contentBlockerMediator = [[BrowserContainerMediator alloc]
251                initWithWebStateList:self.browser->GetWebStateList()
252      webContentAreaOverlayPresenter:overlayPresenter];
253  self.contentBlockerMediator.consumer = self.mediator;
254
255  self.actionHandler = [[PopupMenuActionHandler alloc] init];
256  self.actionHandler.baseViewController = self.baseViewController;
257  self.actionHandler.dispatcher =
258      static_cast<id<ApplicationCommands, BrowserCommands, FindInPageCommands,
259                     LoadQueryCommands, TextZoomCommands>>(
260          self.browser->GetCommandDispatcher());
261  self.actionHandler.commandHandler = self.mediator;
262  tableViewController.delegate = self.actionHandler;
263
264  self.presenter = [[PopupMenuPresenter alloc] init];
265  self.presenter.baseViewController = self.baseViewController;
266  self.presenter.presentedViewController = tableViewController;
267  self.presenter.guideName = guideName;
268  self.presenter.delegate = self;
269
270  [self.UIUpdater updateUIForMenuDisplayed:type];
271
272  [self.presenter prepareForPresentation];
273  [self.presenter presentAnimated:YES];
274  return;
275}
276
277@end
278