summaryrefslogtreecommitdiffstats
path: root/ios/chrome/browser/find_in_page/find_in_page_controller.mm
blob: 81528336a4c3c42ba1b3f754c72b6a0bb59cd14b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/find_in_page/find_in_page_controller.h"

#import <UIKit/UIKit.h>
#import <cmath>

#include "base/ios/ios_util.h"
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_nsobject.h"
#import "ios/chrome/browser/find_in_page/find_in_page_model.h"
#import "ios/chrome/browser/find_in_page/js_findinpage_manager.h"
#import "ios/chrome/browser/web/dom_altering_lock.h"
#import "ios/web/public/web_state/crw_web_view_proxy.h"
#import "ios/web/public/web_state/crw_web_view_scroll_view_proxy.h"
#import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
#import "ios/web/public/web_state/web_state.h"
#import "ios/web/public/web_state/web_state_observer_bridge.h"

NSString* const kFindBarTextFieldWillBecomeFirstResponderNotification =
    @"kFindBarTextFieldWillBecomeFirstResponderNotification";
NSString* const kFindBarTextFieldDidResignFirstResponderNotification =
    @"kFindBarTextFieldDidResignFirstResponderNotification";

namespace {
// The delay (in secs) after which the find in page string will be pumped again.
const NSTimeInterval kRecurringPumpDelay = .01;
}

@interface FindInPageController () <DOMAltering, CRWWebStateObserver>
// The find in page controller delegate.
@property(nonatomic, readonly) id<FindInPageControllerDelegate> delegate;
// The web view's scroll view.
@property(nonatomic, readonly) CRWWebViewScrollViewProxy* webViewScrollView;

// Convenience method to obtain UIPasteboardNameFind from UIPasteBoard.
- (UIPasteboard*)findPasteboard;
// Find in Page text field listeners.
- (void)findBarTextFieldWillBecomeFirstResponder:(NSNotification*)note;
- (void)findBarTextFieldDidResignFirstResponder:(NSNotification*)note;
// Keyboard listeners.
- (void)keyboardDidShow:(NSNotification*)note;
- (void)keyboardWillHide:(NSNotification*)note;
// Constantly injects the find string in page until
// |disableFindInPageWithCompletionHandler:| is called or the find operation is
// complete. Calls |completionHandler| if the find operation is complete.
// |completionHandler| can be nil.
- (void)startPumpingWithCompletionHandler:(ProceduralBlock)completionHandler;
// Gives find in page more time to complete. Calls |completionHandler| with
// a BOOL indicating if the find operation was successful. |completionHandler|
// can be nil.
- (void)pumpFindStringInPageWithCompletionHandler:
    (void (^)(BOOL))completionHandler;
// Processes the result of a single find in page pump. Calls |completionHandler|
// if pumping is done. Re-pumps if necessary.
- (void)processPumpResult:(BOOL)finished
              scrollPoint:(CGPoint)scrollPoint
        completionHandler:(ProceduralBlock)completionHandler;
// Prevent scrolling past the end of the page.
- (CGPoint)limitOverscroll:(CRWWebViewScrollViewProxy*)scrollViewProxy
                   atPoint:(CGPoint)point;
// Returns the associated web state. May be null.
- (web::WebState*)webState;
@end

@implementation FindInPageController {
 @private
  // Object that manages find_in_page.js injection into the web view.
  JsFindinpageManager* _findInPageJsManager;
  id<FindInPageControllerDelegate> _delegate;

  // Access to the web view from the web state.
  base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy;

  // True when a find is in progress. Used to avoid running JavaScript during
  // disable when there is nothing to clear.
  BOOL _findStringStarted;

  // Bridge to observe the web state from Objective-C.
  scoped_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
}

@synthesize delegate = _delegate;

- (id)initWithWebState:(web::WebState*)webState
              delegate:(id<FindInPageControllerDelegate>)delegate {
  self = [super init];
  if (self) {
    DCHECK(delegate);
    _findInPageJsManager = base::mac::ObjCCastStrict<JsFindinpageManager>(
        [webState->GetJSInjectionReceiver()
            instanceOfClass:[JsFindinpageManager class]]);
    _delegate = delegate;
    _webStateObserverBridge.reset(
        new web::WebStateObserverBridge(webState, self));
    _webViewProxy.reset([webState->GetWebViewProxy() retain]);
    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(findBarTextFieldWillBecomeFirstResponder:)
               name:kFindBarTextFieldWillBecomeFirstResponderNotification
             object:nil];
    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(findBarTextFieldDidResignFirstResponder:)
               name:kFindBarTextFieldDidResignFirstResponderNotification
             object:nil];
    DOMAlteringLock::CreateForWebState(webState);
  }
  return self;
}

- (void)dealloc {
  [[NSNotificationCenter defaultCenter] removeObserver:self];
  [super dealloc];
}

- (FindInPageModel*)findInPageModel {
  return [_findInPageJsManager findInPageModel];
}

- (BOOL)canFindInPage {
  return [_webViewProxy hasSearchableTextContent];
}

- (void)initFindInPage {
  [_findInPageJsManager inject];

  // Initialize the module with our frame size.
  CGRect frame = [_webViewProxy bounds];
  [_findInPageJsManager setWidth:frame.size.width height:frame.size.height];
}

- (CRWWebViewScrollViewProxy*)webViewScrollView {
  return [_webViewProxy scrollViewProxy];
}

- (CGPoint)limitOverscroll:(CRWWebViewScrollViewProxy*)scrollViewProxy
                   atPoint:(CGPoint)point {
  CGFloat contentHeight = scrollViewProxy.contentSize.height;
  CGFloat frameHeight = scrollViewProxy.frame.size.height;
  CGFloat maxScroll = std::max<CGFloat>(0, contentHeight - frameHeight);
  if (point.y > maxScroll) {
    point.y = maxScroll;
  }
  return point;
}

- (void)processPumpResult:(BOOL)finished
              scrollPoint:(CGPoint)scrollPoint
        completionHandler:(ProceduralBlock)completionHandler {
  if (finished) {
    [_delegate willAdjustScrollPosition];
    scrollPoint = [self limitOverscroll:[_webViewProxy scrollViewProxy]
                                atPoint:scrollPoint];
    [[_webViewProxy scrollViewProxy] setContentOffset:scrollPoint animated:YES];
    if (completionHandler)
      completionHandler();
  } else {
    [self performSelector:@selector(startPumpingWithCompletionHandler:)
               withObject:completionHandler
               afterDelay:kRecurringPumpDelay];
  }
}

- (void)findStringInPage:(NSString*)query
       completionHandler:(ProceduralBlock)completionHandler {
  ProceduralBlockWithBool lockAction = ^(BOOL hasLock) {
    if (!hasLock) {
      if (completionHandler) {
        completionHandler();
      }
      return;
    }
    // Cancel any previous pumping.
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    [self initFindInPage];
    // Keep track of whether a find is in progress so to avoid running
    // JavaScript during disable if unnecessary.
    _findStringStarted = YES;
    base::WeakNSObject<FindInPageController> weakSelf(self);
    [_findInPageJsManager findString:query
                   completionHandler:^(BOOL finished, CGPoint point) {
                     [weakSelf processPumpResult:finished
                                     scrollPoint:point
                               completionHandler:completionHandler];
                   }];
  };
  DOMAlteringLock::FromWebState([self webState])->Acquire(self, lockAction);
}

- (void)startPumpingWithCompletionHandler:(ProceduralBlock)completionHandler {
  base::WeakNSObject<FindInPageController> weakSelf(self);
  id completionHandlerBlock = ^void(BOOL findFinished) {
    if (findFinished) {
      // Pumping complete. Nothing else to do.
      if (completionHandler)
        completionHandler();
      return;
    }
    // Further pumping is required.
    [weakSelf performSelector:@selector(startPumpingWithCompletionHandler:)
                   withObject:completionHandler
                   afterDelay:kRecurringPumpDelay];
  };
  [self pumpFindStringInPageWithCompletionHandler:completionHandlerBlock];
}

- (void)pumpFindStringInPageWithCompletionHandler:
    (void (^)(BOOL))completionHandler {
  base::WeakNSObject<FindInPageController> weakSelf(self);
  [_findInPageJsManager pumpWithCompletionHandler:^(BOOL finished,
                                                    CGPoint point) {
    base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]);
    if (finished) {
      [[strongSelf delegate] willAdjustScrollPosition];
      point = [strongSelf limitOverscroll:[strongSelf webViewScrollView]
                                  atPoint:point];
      [[strongSelf webViewScrollView] setContentOffset:point animated:YES];
    }
    completionHandler(finished);
  }];
}

- (void)findNextStringInPageWithCompletionHandler:
    (ProceduralBlock)completionHandler {
  [self initFindInPage];
  base::WeakNSObject<FindInPageController> weakSelf(self);
  [_findInPageJsManager nextMatchWithCompletionHandler:^(CGPoint point) {
    base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]);
    [[strongSelf delegate] willAdjustScrollPosition];
    point = [strongSelf limitOverscroll:[strongSelf webViewScrollView]
                                atPoint:point];
    [[strongSelf webViewScrollView] setContentOffset:point animated:YES];
    if (completionHandler)
      completionHandler();
  }];
}

// Highlight the previous search match, update model and scroll to match.
- (void)findPreviousStringInPageWithCompletionHandler:
    (ProceduralBlock)completionHandler {
  [self initFindInPage];
  base::WeakNSObject<FindInPageController> weakSelf(self);
  [_findInPageJsManager previousMatchWithCompletionHandler:^(CGPoint point) {
    base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]);
    [[strongSelf delegate] willAdjustScrollPosition];
    point = [strongSelf limitOverscroll:[strongSelf webViewScrollView]
                                atPoint:point];
    [[strongSelf webViewScrollView] setContentOffset:point animated:YES];
    if (completionHandler)
      completionHandler();
  }];
}

// Remove highlights from the page and disable the model.
- (void)disableFindInPageWithCompletionHandler:
    (ProceduralBlock)completionHandler {
  if (![self canFindInPage])
    return;
  // Cancel any queued calls to |recurringPumpWithCompletionHandler|.
  [NSObject cancelPreviousPerformRequestsWithTarget:self];
  base::WeakNSObject<FindInPageController> weakSelf(self);
  ProceduralBlock handler = ^{
    base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]);
    if (strongSelf) {
      [strongSelf.get().findInPageModel setEnabled:NO];
      web::WebState* webState = [strongSelf webState];
      if (webState)
        DOMAlteringLock::FromWebState(webState)->Release(strongSelf);
    }
    if (completionHandler)
      completionHandler();
  };
  // Only run JSFindInPageManager disable if there is a string in progress to
  // avoid WKWebView crash on deallocation due to outstanding completion
  // handler.
  if (_findStringStarted) {
    [_findInPageJsManager disableWithCompletionHandler:handler];
    _findStringStarted = NO;
  } else {
    handler();
  }
}

- (void)saveSearchTerm {
  [self findPasteboard].string = [[self findInPageModel] text];
}

- (void)restoreSearchTerm {
  // Pasteboards always return nil in background:
  if ([[UIApplication sharedApplication] applicationState] !=
      UIApplicationStateActive) {
    return;
  }

  NSString* term = [self findPasteboard].string;
  [[self findInPageModel] updateQuery:(term ? term : @"") matches:0];
}

- (UIPasteboard*)findPasteboard {
  return [UIPasteboard pasteboardWithName:UIPasteboardNameFind create:NO];
}

- (web::WebState*)webState {
  return _webStateObserverBridge ? _webStateObserverBridge->web_state()
                                 : nullptr;
}

#pragma mark - Notification listeners

- (void)findBarTextFieldWillBecomeFirstResponder:(NSNotification*)note {
  // Listen to the keyboard appearance notifications.
  NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
  [defaultCenter addObserver:self
                    selector:@selector(keyboardDidShow:)
                        name:UIKeyboardDidShowNotification
                      object:nil];
  [defaultCenter addObserver:self
                    selector:@selector(keyboardWillHide:)
                        name:UIKeyboardWillHideNotification
                      object:nil];
}

- (void)findBarTextFieldDidResignFirstResponder:(NSNotification*)note {
  // Resign from the keyboard appearance notifications on the next turn of the
  // runloop.
  dispatch_async(dispatch_get_main_queue(), ^{
    NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
    [defaultCenter removeObserver:self
                             name:UIKeyboardDidShowNotification
                           object:nil];
    [defaultCenter removeObserver:self
                             name:UIKeyboardWillHideNotification
                           object:nil];
  });
}

- (void)keyboardDidShow:(NSNotification*)note {
  NSDictionary* info = [note userInfo];
  CGSize kbSize =
      [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
  UIInterfaceOrientation orientation =
      [[UIApplication sharedApplication] statusBarOrientation];
  CGFloat kbHeight = kbSize.height;
  // Prior to iOS 8, the keyboard frame was not dependent on interface
  // orientation, so height and width need to be swapped in landscape mode.
  if (UIInterfaceOrientationIsLandscape(orientation) &&
      !base::ios::IsRunningOnIOS8OrLater()) {
    kbHeight = kbSize.width;
  }

  UIEdgeInsets insets = UIEdgeInsetsZero;
  insets.bottom = kbHeight;
  [_webViewProxy registerInsets:insets forCaller:self];
}

- (void)keyboardWillHide:(NSNotification*)note {
  [_webViewProxy unregisterInsetsForCaller:self];
}

- (void)detachFromWebState {
  _webStateObserverBridge.reset();
}

#pragma mark - CRWWebStateObserver Methods

- (void)webStateDestroyed:(web::WebState*)webState {
  [self detachFromWebState];
}

#pragma mark - DOMAltering Methods

- (BOOL)canReleaseDOMLock {
  return NO;
}

- (void)releaseDOMLockWithCompletionHandler:(ProceduralBlock)completionHandler {
  NOTREACHED();
}

@end