Tapping and holding a link should have a share option
[WebKit-https.git] / Source / WebKit2 / UIProcess / ios / WKPDFView.mm
1 /*
2  * Copyright (C) 2014 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "WKPDFView.h"
28
29 #if PLATFORM(IOS)
30
31 #import "APIFindClient.h"
32 #import "APIUIClient.h"
33 #import "CorePDFSPI.h"
34 #import "SessionState.h"
35 #import "UIKitSPI.h"
36 #import "WKPDFPageNumberIndicator.h"
37 #import "WKWebViewInternal.h"
38 #import "WeakObjCPtr.h"
39 #import "WebPageProxy.h"
40 #import "_WKFindDelegate.h"
41 #import <MobileCoreServices/UTCoreTypes.h>
42 #import <WebCore/FloatRect.h>
43 #import <WebCore/LocalizedStrings.h>
44 #import <wtf/RetainPtr.h>
45 #import <wtf/Vector.h>
46
47 using namespace WebCore;
48
49 const CGFloat pdfPageMargin = 8;
50 const CGFloat pdfMinimumZoomScale = 1;
51 const CGFloat pdfMaximumZoomScale = 5;
52
53 const CGFloat passwordEntryFieldPadding = 10;
54
55 const float overdrawHeightMultiplier = 1.5;
56
57 static const CGFloat smartMagnificationElementPadding = 0.05;
58
59 typedef struct {
60     CGRect frame;
61     RetainPtr<UIPDFPageView> view;
62     RetainPtr<UIPDFPage> page;
63     unsigned index;
64 } PDFPageInfo;
65
66 @interface WKPDFView ()
67 - (void)_resetZoomAnimated:(BOOL)animated;
68 @end
69
70 @implementation WKPDFView {
71     RetainPtr<CGPDFDocumentRef> _cgPDFDocument;
72     RetainPtr<UIPDFDocument> _pdfDocument;
73     RetainPtr<NSString> _suggestedFilename;
74     RetainPtr<WKPDFPageNumberIndicator> _pageNumberIndicator;
75
76     RetainPtr<UIDocumentPasswordView> _passwordView;
77
78     Vector<PDFPageInfo> _pages;
79     unsigned _centerPageNumber;
80
81     CGSize _minimumSize;
82     CGSize _overlaidAccessoryViewsInset;
83     WKWebView *_webView;
84     UIScrollView *_scrollView;
85     UIView *_fixedOverlayView;
86
87     BOOL _isStartingZoom;
88     BOOL _isPerformingSameDocumentNavigation;
89
90     RetainPtr<WKActionSheetAssistant> _actionSheetAssistant;
91     WebKit::InteractionInformationAtPosition _positionInformation;
92
93     unsigned _currentFindPageIndex;
94     unsigned _currentFindMatchIndex;
95     RetainPtr<UIPDFSelection> _currentFindSelection;
96
97     RetainPtr<NSString> _cachedFindString;
98     Vector<RetainPtr<UIPDFSelection>> _cachedFindMatches;
99     unsigned _cachedFindMaximumCount;
100     _WKFindOptions _cachedFindOptionsAffectingResults;
101
102     std::atomic<unsigned> _nextComputeMatchesOperationID;
103     RetainPtr<NSString> _nextCachedFindString;
104     unsigned _nextCachedFindMaximumCount;
105     _WKFindOptions _nextCachedFindOptionsAffectingResults;
106
107     dispatch_queue_t _findQueue;
108
109     RetainPtr<UIWKSelectionAssistant> _webSelectionAssistant;
110 }
111
112 - (instancetype)web_initWithFrame:(CGRect)frame webView:(WKWebView *)webView
113 {
114     if (!(self = [super initWithFrame:frame]))
115         return nil;
116
117     self.backgroundColor = [UIColor grayColor];
118
119     _webView = webView;
120
121     _scrollView = webView.scrollView;
122     [_scrollView setMinimumZoomScale:pdfMinimumZoomScale];
123     [_scrollView setMaximumZoomScale:pdfMaximumZoomScale];
124     [_scrollView setBackgroundColor:[UIColor grayColor]];
125
126     _actionSheetAssistant = adoptNS([[WKActionSheetAssistant alloc] initWithView:self]);
127     [_actionSheetAssistant setDelegate:self];
128
129     _findQueue = dispatch_queue_create("com.apple.WebKit.WKPDFViewComputeMatchesQueue", DISPATCH_QUEUE_SERIAL);
130
131     return self;
132 }
133
134 - (void)dealloc
135 {
136     [[NSNotificationCenter defaultCenter] removeObserver:self];
137
138     [self _clearPages];
139     [_pageNumberIndicator removeFromSuperview];
140     dispatch_release(_findQueue);
141     [super dealloc];
142 }
143
144 - (NSString *)suggestedFilename
145 {
146     return _suggestedFilename.get();
147 }
148
149 - (CGPDFDocumentRef)pdfDocument
150 {
151     return [_pdfDocument CGDocument];
152 }
153
154 - (void)_clearPages
155 {
156     for (auto& page : _pages) {
157         [page.view removeFromSuperview];
158         [page.view setDelegate:nil];
159         [[page.view annotationController] setDelegate:nil];
160     }
161     
162     _pages.clear();
163 }
164
165 - (void)_didLoadPDFDocument
166 {
167     _pdfDocument = adoptNS([[UIPDFDocument alloc] initWithCGPDFDocument:_cgPDFDocument.get()]);
168
169     // FIXME: Restore the scroll position and page scale if navigating from the back/forward list.
170
171     [self _computePageAndDocumentFrames];
172     [self _revalidateViews];
173     [self _scrollToFragment:_webView.URL.fragment];
174 }
175
176 - (void)web_setContentProviderData:(NSData *)data suggestedFilename:(NSString *)filename
177 {
178     _suggestedFilename = adoptNS([filename copy]);
179
180     [self _clearPages];
181
182     RetainPtr<CGDataProviderRef> dataProvider = adoptCF(CGDataProviderCreateWithCFData((CFDataRef)data));
183     _cgPDFDocument = adoptCF(CGPDFDocumentCreateWithProvider(dataProvider.get()));
184
185     if (!_cgPDFDocument)
186         return;
187
188     if (CGPDFDocumentIsUnlocked(_cgPDFDocument.get())) {
189         [self _didLoadPDFDocument];
190         return;
191     }
192
193     [self _showPasswordEntryField];
194 }
195
196 - (void)web_setMinimumSize:(CGSize)size
197 {
198     if (_passwordView) {
199         [self _updatePasswordEntryField];
200         return;
201     }
202
203     _minimumSize = size;
204
205     CGFloat oldDocumentLeftFraction = 0;
206     CGFloat oldDocumentTopFraction = 0;
207     CGSize contentSize = _scrollView.contentSize;
208     if (contentSize.width && contentSize.height) {
209         CGPoint contentOffset = _scrollView.contentOffset;
210         UIEdgeInsets contentInset = _scrollView.contentInset;
211         oldDocumentLeftFraction = (contentOffset.x + contentInset.left) / contentSize.width;
212         oldDocumentTopFraction = (contentOffset.y + contentInset.top) / contentSize.height;
213     }
214
215     [self _computePageAndDocumentFrames];
216
217     // FIXME: This dispatch_async is unnecessary except to work around rdar://problem/15035620.
218     // Once that is resolved, we should do the setContentOffset without the dispatch_async.
219     RetainPtr<WKPDFView> retainedSelf = self;
220     dispatch_async(dispatch_get_main_queue(), [retainedSelf, oldDocumentLeftFraction, oldDocumentTopFraction] {
221         CGSize contentSize = retainedSelf->_scrollView.contentSize;
222         UIEdgeInsets contentInset = retainedSelf->_scrollView.contentInset;
223         [retainedSelf->_scrollView setContentOffset:CGPointMake((oldDocumentLeftFraction * contentSize.width) - contentInset.left, (oldDocumentTopFraction * contentSize.height) - contentInset.top) animated:NO];
224
225         [retainedSelf _revalidateViews];
226     });
227 }
228
229 - (void)scrollViewDidScroll:(UIScrollView *)scrollView
230 {
231     if (scrollView.isZoomBouncing || scrollView._isAnimatingZoom)
232         return;
233
234     [self _revalidateViews];
235
236     if (!_isPerformingSameDocumentNavigation)
237         [_pageNumberIndicator show];
238 }
239
240 - (void)_ensureViewForPage:(PDFPageInfo&)pageInfo
241 {
242     if (pageInfo.view)
243         return;
244
245     pageInfo.view = adoptNS([[UIPDFPageView alloc] initWithPage:pageInfo.page.get() tiledContent:YES]);
246     [pageInfo.view setUseBackingLayer:YES];
247     [pageInfo.view setDelegate:self];
248     [[pageInfo.view annotationController] setDelegate:self];
249     [self addSubview:pageInfo.view.get()];
250
251     [pageInfo.view setFrame:pageInfo.frame];
252     [pageInfo.view contentLayer].contentsScale = self.window.screen.scale;
253 }
254
255 - (void)_revalidateViews
256 {
257     if (_isStartingZoom)
258         return;
259
260     CGRect targetRect = [_scrollView convertRect:_scrollView.bounds toView:self];
261
262     // We apply overdraw after applying scale in order to avoid excessive
263     // memory use caused by scaling the overdraw.
264     CGRect targetRectWithOverdraw = CGRectInset(targetRect, 0, -targetRect.size.height * overdrawHeightMultiplier);
265     CGRect targetRectForCenterPage = CGRectInset(targetRect, 0, targetRect.size.height / 2 - pdfPageMargin * 2);
266
267     _centerPageNumber = 0;
268
269     for (auto& pageInfo : _pages) {
270         if (!CGRectIntersectsRect(pageInfo.frame, targetRectWithOverdraw) && pageInfo.index != _currentFindPageIndex) {
271             [pageInfo.view removeFromSuperview];
272             pageInfo.view = nullptr;
273             continue;
274         }
275
276         if (!_centerPageNumber && CGRectIntersectsRect(pageInfo.frame, targetRectForCenterPage))
277             _centerPageNumber = pageInfo.index + 1;
278
279         [self _ensureViewForPage:pageInfo];
280     }
281
282     [self _updatePageNumberIndicator];
283 }
284
285 - (CGPoint)_offsetForPageNumberIndicator
286 {
287     UIEdgeInsets contentInset = [_webView _computedContentInset];
288     return CGPointMake(contentInset.left, contentInset.top + _overlaidAccessoryViewsInset.height);
289 }
290
291 - (void)_updatePageNumberIndicator
292 {
293     if (_isPerformingSameDocumentNavigation)
294         return;
295
296     if (!_pageNumberIndicator)
297         _pageNumberIndicator = adoptNS([[WKPDFPageNumberIndicator alloc] initWithFrame:CGRectZero]);
298
299     [_fixedOverlayView addSubview:_pageNumberIndicator.get()];
300
301     [_pageNumberIndicator setCurrentPageNumber:_centerPageNumber];
302     [_pageNumberIndicator moveToPoint:[self _offsetForPageNumberIndicator] animated:NO];
303 }
304
305 - (void)web_setOverlaidAccessoryViewsInset:(CGSize)inset
306 {
307     _overlaidAccessoryViewsInset = inset;
308     [_pageNumberIndicator moveToPoint:[self _offsetForPageNumberIndicator] animated:YES];
309 }
310
311 - (void)web_computedContentInsetDidChange
312 {
313     [self _updatePageNumberIndicator];
314 }
315
316 - (void)web_setFixedOverlayView:(UIView *)fixedOverlayView
317 {
318     _fixedOverlayView = fixedOverlayView;
319
320     if (_pageNumberIndicator)
321         [_fixedOverlayView addSubview:_pageNumberIndicator.get()];
322 }
323
324 - (void)_scrollToFragment:(NSString *)fragment
325 {
326     if (![fragment hasPrefix:@"page"])
327         return;
328
329     NSInteger pageIndex = [[fragment substringFromIndex:4] integerValue] - 1;
330     if (pageIndex < 0 || static_cast<std::size_t>(pageIndex) >= _pages.size())
331         return;
332
333     _isPerformingSameDocumentNavigation = YES;
334
335     [_pageNumberIndicator hide];
336     [self _resetZoomAnimated:NO];
337
338     // Ensure that the page margin is visible below the content inset.
339     const CGFloat verticalOffset = _pages[pageIndex].frame.origin.y - _webView._computedContentInset.top - pdfPageMargin;
340     [_scrollView setContentOffset:CGPointMake(_scrollView.contentOffset.x, verticalOffset) animated:NO];
341
342     _isPerformingSameDocumentNavigation = NO;
343 }
344
345 - (void)web_didSameDocumentNavigation:(WKSameDocumentNavigationType)navigationType
346 {
347     // Check for kWKSameDocumentNavigationSessionStatePop instead of kWKSameDocumentNavigationAnchorNavigation since the
348     // latter is only called once when navigating to the same anchor in succession. If the user navigates to a page
349     // then scrolls back and clicks on the same link a second time, we want to scroll again.
350     if (navigationType != kWKSameDocumentNavigationSessionStatePop)
351         return;
352
353     // FIXME: restore the scroll position and page scale if navigating back from a fragment.
354
355     [self _scrollToFragment:_webView.URL.fragment];
356 }
357
358 - (void)_computePageAndDocumentFrames
359 {
360     if (_passwordView)
361         return;
362
363     NSUInteger pageCount = [_pdfDocument numberOfPages];
364     [_pageNumberIndicator setPageCount:pageCount];
365     
366     [self _clearPages];
367
368     _pages.reserveCapacity(pageCount);
369
370     CGRect pageFrame = CGRectMake(0, 0, _minimumSize.width, _minimumSize.height);
371     for (NSUInteger pageIndex = 0; pageIndex < pageCount; ++pageIndex) {
372         UIPDFPage *page = [_pdfDocument pageAtIndex:pageIndex];
373         if (!page)
374             continue;
375
376         CGSize pageSize = [page cropBoxAccountForRotation].size;
377         pageFrame.size.height = pageSize.height / pageSize.width * pageFrame.size.width;
378         CGRect pageFrameWithMarginApplied = CGRectInset(pageFrame, pdfPageMargin, pdfPageMargin);
379
380         PDFPageInfo pageInfo;
381         pageInfo.page = page;
382         pageInfo.frame = pageFrameWithMarginApplied;
383         pageInfo.index = pageIndex;
384         _pages.append(pageInfo);
385         pageFrame.origin.y += pageFrame.size.height - pdfPageMargin;
386     }
387
388     CGFloat scale = _scrollView.zoomScale;
389     CGRect newFrame = [self frame];
390     newFrame.size.width = _minimumSize.width * scale;
391     newFrame.size.height = std::max(pageFrame.origin.y + pdfPageMargin, _minimumSize.height) * scale;
392
393     [self setFrame:newFrame];
394     [_scrollView setContentSize:newFrame.size];
395 }
396
397 - (void)_resetZoomAnimated:(BOOL)animated
398 {
399     _isStartingZoom = YES;
400
401     CGRect scrollViewBounds = _scrollView.bounds;
402     CGPoint centerOfPageInDocumentCoordinates = [_scrollView convertPoint:CGPointMake(CGRectGetMidX(scrollViewBounds), CGRectGetMidY(scrollViewBounds)) toView:self];
403     [_webView _zoomOutWithOrigin:centerOfPageInDocumentCoordinates animated:animated];
404
405     _isStartingZoom = NO;
406 }
407
408 - (void)_highlightLinkAnnotation:(UIPDFLinkAnnotation *)linkAnnotation forDuration:(NSTimeInterval)duration completionHandler:(void (^)(void))completionHandler
409 {
410     static const CGFloat highlightBorderRadius = 3;
411     static const CGFloat highlightColorComponent = 26.0 / 255;
412     static UIColor *highlightColor = [[UIColor alloc] initWithRed:highlightColorComponent green:highlightColorComponent blue:highlightColorComponent alpha:0.3];
413
414     UIPDFPageView *pageView = linkAnnotation.annotationController.pageView;
415     CGRect highlightViewFrame = [self convertRect:[pageView convertRectFromPDFPageSpace:linkAnnotation.Rect] fromView:pageView];
416     RetainPtr<_UIHighlightView> highlightView = adoptNS([[_UIHighlightView alloc] initWithFrame:CGRectInset(highlightViewFrame, -highlightBorderRadius, -highlightBorderRadius)]);
417     [highlightView setOpaque:NO];
418     [highlightView setCornerRadius:highlightBorderRadius];
419     [highlightView setColor:highlightColor];
420
421     ASSERT(isMainThread());
422     [self addSubview:highlightView.get()];
423     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, duration * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
424         [highlightView removeFromSuperview];
425         completionHandler();
426     });
427 }
428
429 - (NSURL *)_URLForLinkAnnotation:(UIPDFLinkAnnotation *)linkAnnotation
430 {
431     NSURL *documentURL = _webView.URL;
432
433     if (NSURL *url = linkAnnotation.url)
434         return [NSURL URLWithString:url.relativeString relativeToURL:documentURL];
435
436     if (NSUInteger pageNumber = linkAnnotation.pageNumber) {
437         String anchorString = ASCIILiteral("#page");
438         anchorString.append(String::number(pageNumber));
439         return [NSURL URLWithString:anchorString relativeToURL:documentURL];
440     }
441
442     return nil;
443 }
444
445 #pragma mark Find-in-Page
446
447 static NSStringCompareOptions stringCompareOptions(_WKFindOptions options)
448 {
449     NSStringCompareOptions findOptions = 0;
450     if (options & _WKFindOptionsCaseInsensitive)
451         findOptions |= NSCaseInsensitiveSearch;
452     if (options & _WKFindOptionsBackwards)
453         findOptions |= NSBackwardsSearch;
454     return findOptions;
455 }
456
457 - (void)_computeMatchesForString:(NSString *)string options:(_WKFindOptions)options maxCount:(NSUInteger)maxCount completionHandler:(void (^)(BOOL success))completionHandler
458 {
459     if (!_pdfDocument) {
460         completionHandler(NO);
461         return;
462     }
463
464     _WKFindOptions optionsAffectingResults = options & ~_WKFindOptionsIrrelevantForBatchResults;
465
466     // If this search is equivalent to the currently cached search, bail and call the completion handler, because the existing cached results are valid.
467     if (!_cachedFindMatches.isEmpty() && [_cachedFindString isEqualToString:string] && _cachedFindOptionsAffectingResults == optionsAffectingResults && _cachedFindMaximumCount == maxCount) {
468         // Also, cancel any running search, because it will eventually replace the now-valid results.
469         ++_nextComputeMatchesOperationID;
470
471         completionHandler(YES);
472         return;
473     }
474
475     // If this search is equivalent to the currently running asynchronous search, bail as if this search were cancelled; the original search's completion handler will eventually fire.
476     if ([_nextCachedFindString isEqualToString:string] && _nextCachedFindOptionsAffectingResults == optionsAffectingResults && _nextCachedFindMaximumCount == maxCount) {
477         completionHandler(NO);
478         return;
479     }
480
481     NSStringCompareOptions findOptions = stringCompareOptions(optionsAffectingResults);
482
483     Vector<PDFPageInfo> pages = _pages;
484
485     unsigned computeMatchesOperationID = ++_nextComputeMatchesOperationID;
486     _nextCachedFindString = string;
487     _nextCachedFindOptionsAffectingResults = optionsAffectingResults;
488     _nextCachedFindMaximumCount = maxCount;
489
490     RetainPtr<WKPDFView> retainedSelf = self;
491     typeof(completionHandler) completionHandlerCopy = Block_copy(completionHandler);
492
493     dispatch_async(_findQueue, [pages, string, findOptions, optionsAffectingResults, maxCount, computeMatchesOperationID, retainedSelf, completionHandlerCopy] {
494         Vector<RetainPtr<UIPDFSelection>> matches;
495
496         for (unsigned pageIndex = 0; pageIndex < pages.size(); ++pageIndex) {
497             UIPDFSelection *match = nullptr;
498             while ((match = [pages[pageIndex].page findString:string fromSelection:match options:findOptions])) {
499                 matches.append(match);
500                 if (matches.size() > maxCount)
501                     goto maxCountExceeded;
502
503                 // If we've enqueued another search, cancel this one.
504                 if (retainedSelf->_nextComputeMatchesOperationID != computeMatchesOperationID) {
505                     dispatch_async(dispatch_get_main_queue(), [completionHandlerCopy] {
506                         completionHandlerCopy(NO);
507                         Block_release(completionHandlerCopy);
508                     });
509                     return;
510                 }
511             };
512         }
513
514     maxCountExceeded:
515         dispatch_async(dispatch_get_main_queue(), [computeMatchesOperationID, string, optionsAffectingResults, maxCount, matches, completionHandlerCopy, retainedSelf] {
516
517             // If another search has been enqueued in the meantime, ignore this result.
518             if (retainedSelf->_nextComputeMatchesOperationID != computeMatchesOperationID) {
519                 Block_release(completionHandlerCopy);
520                 return;
521             }
522
523             retainedSelf->_cachedFindString = string;
524             retainedSelf->_cachedFindOptionsAffectingResults = optionsAffectingResults;
525             retainedSelf->_cachedFindMaximumCount = maxCount;
526             retainedSelf->_cachedFindMatches = matches;
527
528             retainedSelf->_nextCachedFindString = nil;
529
530             completionHandlerCopy(YES);
531             Block_release(completionHandlerCopy);
532         });
533     });
534 }
535
536 - (void)web_countStringMatches:(NSString *)string options:(_WKFindOptions)options maxCount:(NSUInteger)maxCount
537 {
538     RefPtr<WebKit::WebPageProxy> page = _webView->_page;
539     [self _computeMatchesForString:string options:options maxCount:maxCount completionHandler:^(BOOL success) {
540         if (!success)
541             return;
542         page->findClient().didCountStringMatches(page.get(), string, _cachedFindMatches.size());
543     }];
544 }
545
546 - (void)_didFindMatch:(UIPDFSelection *)match
547 {
548     for (auto& pageInfo : _pages) {
549         if (pageInfo.page == match.page) {
550             [self _ensureViewForPage:pageInfo];
551
552             [pageInfo.view highlightSearchSelection:match animated:NO];
553
554             _currentFindPageIndex = pageInfo.index;
555             _currentFindSelection = match;
556
557             CGRect zoomRect = [pageInfo.view convertRectFromPDFPageSpace:match.bounds];
558             [self zoom:pageInfo.view.get() to:zoomRect atPoint:CGPointZero kind:kUIPDFObjectKindText];
559
560             return;
561         }
562     }
563 }
564
565 - (void)web_findString:(NSString *)string options:(_WKFindOptions)options maxCount:(NSUInteger)maxCount
566 {
567     if (_currentFindSelection)
568         [_pages[_currentFindPageIndex].view clearSearchHighlights];
569
570     RetainPtr<UIPDFSelection> previousFindSelection = _currentFindSelection;
571     unsigned previousFindPageIndex = 0;
572     if (previousFindSelection) {
573         previousFindPageIndex = _currentFindPageIndex;
574         if (![_cachedFindString isEqualToString:string]) {
575             NSUInteger location = [_currentFindSelection startIndex];
576             if (location)
577                 previousFindSelection = adoptNS([[UIPDFSelection alloc] initWithPage:[_currentFindSelection page] fromIndex:location - 1 toIndex:location]);
578         }
579     }
580
581     NSStringCompareOptions findOptions = stringCompareOptions(options);
582     bool backwards = (options & _WKFindOptionsBackwards);
583     RefPtr<WebKit::WebPageProxy> page = _webView->_page;
584
585     [self _computeMatchesForString:string options:options maxCount:maxCount completionHandler:^(BOOL success) {
586         if (!success)
587             return;
588
589         unsigned pageIndex = previousFindPageIndex;
590         for (unsigned i = 0; i < _pages.size(); ++i) {
591             UIPDFSelection *match = [_pages[pageIndex].page findString:string fromSelection:(pageIndex == previousFindPageIndex ? previousFindSelection.get() : nil) options:findOptions];
592
593             if (!match) {
594                 if (!pageIndex && backwards)
595                     pageIndex = _pages.size() - 1;
596                 else if (pageIndex == _pages.size() - 1 && !backwards)
597                     pageIndex = 0;
598                 else
599                     pageIndex += backwards ? -1 : 1;
600                 continue;
601             }
602
603             [self _didFindMatch:match];
604
605             if (_cachedFindMatches.size() <= maxCount) {
606                 _currentFindMatchIndex = 0;
607                 for (const auto& knownMatch : _cachedFindMatches) {
608                     if (match.stringRange.location == [knownMatch stringRange].location && match.stringRange.length == [knownMatch stringRange].length) {
609                         page->findClient().didFindString(page.get(), string, _cachedFindMatches.size(), _currentFindMatchIndex);
610                         break;
611                     }
612                     _currentFindMatchIndex++;
613                 }
614             }
615
616             return;
617         }
618         
619         page->findClient().didFailToFindString(page.get(), string);
620     }];
621 }
622
623 - (void)web_hideFindUI
624 {
625     if (_currentFindSelection)
626         [_pages[_currentFindPageIndex].view clearSearchHighlights];
627
628     _currentFindSelection = nullptr;
629     _cachedFindString = nullptr;
630     _cachedFindMatches.clear();
631 }
632
633 #pragma mark UIPDFPageViewDelegate
634
635 - (void)zoom:(UIPDFPageView *)pageView to:(CGRect)targetRect atPoint:(CGPoint)origin kind:(UIPDFObjectKind)kind
636 {
637     _isStartingZoom = YES;
638
639     BOOL isImage = kind == kUIPDFObjectKindGraphic;
640
641     if (!isImage)
642         targetRect = CGRectInset(targetRect, smartMagnificationElementPadding * targetRect.size.width, smartMagnificationElementPadding * targetRect.size.height);
643
644     CGRect rectInDocumentCoordinates = [pageView convertRect:targetRect toView:self];
645     CGPoint originInDocumentCoordinates = [pageView convertPoint:origin toView:self];
646
647     [_webView _zoomToRect:rectInDocumentCoordinates withOrigin:originInDocumentCoordinates fitEntireRect:isImage minimumScale:pdfMinimumZoomScale maximumScale:pdfMaximumZoomScale minimumScrollDistance:0];
648
649     _isStartingZoom = NO;
650 }
651
652 - (void)resetZoom:(UIPDFPageView *)pageView
653 {
654     [self _resetZoomAnimated:YES];
655 }
656
657 #pragma mark UIPDFAnnotationControllerDelegate
658
659 - (void)annotation:(UIPDFAnnotation *)annotation wasTouchedAtPoint:(CGPoint)point controller:(UIPDFAnnotationController *)controller
660 {
661     if (![annotation isKindOfClass:[UIPDFLinkAnnotation class]])
662         return;
663
664     UIPDFLinkAnnotation *linkAnnotation = (UIPDFLinkAnnotation *)annotation;
665     RetainPtr<NSURL> url = [self _URLForLinkAnnotation:linkAnnotation];
666     if (!url)
667         return;
668
669     CGPoint documentPoint = [controller.pageView convertPoint:point toView:self];
670     CGPoint screenPoint = [self.window convertPoint:[self convertPoint:documentPoint toView:nil] toWindow:nil];
671     RetainPtr<WKWebView> retainedWebView = _webView;
672
673     [self _highlightLinkAnnotation:linkAnnotation forDuration:.2 completionHandler:^{
674         retainedWebView->_page->navigateToPDFLinkWithSimulatedClick([url absoluteString], roundedIntPoint(documentPoint), roundedIntPoint(screenPoint));
675     }];
676 }
677
678 - (void)annotation:(UIPDFAnnotation *)annotation isBeingPressedAtPoint:(CGPoint)point controller:(UIPDFAnnotationController *)controller
679 {
680     if (![annotation isKindOfClass:[UIPDFLinkAnnotation class]])
681         return;
682
683     UIPDFLinkAnnotation *linkAnnotation = (UIPDFLinkAnnotation *)annotation;
684     NSURL *url = [self _URLForLinkAnnotation:linkAnnotation];
685     if (!url)
686         return;
687
688     _positionInformation.url = url.absoluteString;
689     _positionInformation.point = roundedIntPoint([controller.pageView convertPoint:point toView:self]);
690     _positionInformation.bounds = roundedIntRect([self convertRect:[controller.pageView convertRectFromPDFPageSpace:annotation.Rect] fromView:controller.pageView]);
691
692     [self _highlightLinkAnnotation:linkAnnotation forDuration:.75 completionHandler:^{
693         [_actionSheetAssistant showLinkSheet];
694     }];
695 }
696
697 #pragma mark WKActionSheetAssistantDelegate
698
699 - (const WebKit::InteractionInformationAtPosition&)positionInformationForActionSheetAssistant:(WKActionSheetAssistant *)assistant
700 {
701     return _positionInformation;
702 }
703
704 - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant performAction:(WebKit::SheetAction)action
705 {
706     if (action != WebKit::SheetAction::Copy)
707         return;
708
709     NSDictionary *representations = @{
710         (NSString *)kUTTypeUTF8PlainText : _positionInformation.url,
711         (NSString *)kUTTypeURL : [NSURL URLWithString:_positionInformation.url]
712     };
713
714     [UIPasteboard generalPasteboard].items = @[ representations ];
715 }
716
717 - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant openElementAtLocation:(CGPoint)location
718 {
719     CGPoint screenPoint = [self.window convertPoint:[self convertPoint:location toView:nil] toWindow:nil];
720     _webView->_page->navigateToPDFLinkWithSimulatedClick(_positionInformation.url, roundedIntPoint(location), roundedIntPoint(screenPoint));
721 }
722
723 - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant shareElementWithURL:(NSURL *)url rect:(CGRect)boundingRect
724 {
725     _webSelectionAssistant = adoptNS([[UIWKSelectionAssistant alloc] initWithView:self]);
726     [_webSelectionAssistant showShareSheetFor:url.absoluteString fromRect:boundingRect];
727     _webSelectionAssistant = nil;
728 }
729
730 #if HAVE(APP_LINKS)
731 - (BOOL)actionSheetAssistant:(WKActionSheetAssistant *)assistant shouldIncludeAppLinkActionsForElement:(_WKActivatedElementInfo *)element
732 {
733     return _webView->_page->uiClient().shouldIncludeAppLinkActionsForElement(element);
734 }
735 #endif
736
737 - (RetainPtr<NSArray>)actionSheetAssistant:(WKActionSheetAssistant *)assistant decideActionsForElement:(_WKActivatedElementInfo *)element defaultActions:(RetainPtr<NSArray>)defaultActions
738 {
739     return _webView->_page->uiClient().actionsForElement(element, WTF::move(defaultActions));
740 }
741
742 #pragma mark Password protection UI
743
744 - (void)_updatePasswordEntryField
745 {
746     [_passwordView setFrame:CGRectMake(0, 0, _webView.bounds.size.width, _webView.bounds.size.height)];
747     [_scrollView setContentSize:[_passwordView bounds].size];
748 }
749
750 - (void)_keyboardDidShow:(NSNotification *)notification
751 {
752     UITextField *passwordField = [_passwordView passwordField];
753     if (!passwordField.isEditing)
754         return;
755
756     CGRect keyboardRect = [UIPeripheralHost visiblePeripheralFrame];
757     if (CGRectIsEmpty(keyboardRect))
758         return;
759
760     UIWindow *window = _scrollView.window;
761     keyboardRect = [window convertRect:keyboardRect fromWindow:nil];
762     keyboardRect = [_scrollView convertRect:keyboardRect fromView:window];
763
764     CGRect passwordFieldFrame = [passwordField convertRect:passwordField.bounds toView:_scrollView];
765
766     CGSize contentSize = [_passwordView bounds].size;
767     contentSize.height += CGRectGetHeight(keyboardRect);
768     [_scrollView setContentSize:contentSize];
769
770     if (CGRectIntersectsRect(passwordFieldFrame, keyboardRect)) {
771         CGFloat yDelta = CGRectGetMaxY(passwordFieldFrame) - CGRectGetMinY(keyboardRect);
772
773         CGPoint contentOffset = _scrollView.contentOffset;
774         contentOffset.y += yDelta + passwordEntryFieldPadding;
775
776         [_scrollView setContentOffset:contentOffset animated:YES];
777     }
778 }
779
780 - (void)_showPasswordEntryField
781 {
782     [_scrollView setMinimumZoomScale:1];
783     [_scrollView setMaximumZoomScale:1];
784     [_scrollView setBackgroundColor:[UIColor groupTableViewBackgroundColor]];
785
786     _passwordView = adoptNS([[UIDocumentPasswordView alloc] initWithDocumentName:_suggestedFilename.get()]);
787     [_passwordView setPasswordDelegate:self];
788
789     [self _updatePasswordEntryField];
790
791     [self addSubview:_passwordView.get()];
792 }
793
794 - (void)_hidePasswordEntryField
795 {
796     [_passwordView removeFromSuperview];
797     _passwordView = nil;
798
799     [_scrollView setMinimumZoomScale:pdfMinimumZoomScale];
800     [_scrollView setMaximumZoomScale:pdfMaximumZoomScale];
801     [_scrollView setBackgroundColor:[UIColor grayColor]];
802 }
803
804 - (void)userDidEnterPassword:(NSString *)password forPasswordView:(UIDocumentPasswordView *)passwordView
805 {
806     [self _tryToUnlockWithPassword:password];
807 }
808
809 - (void)didBeginEditingPassword:(UITextField *)passwordField inView:(UIDocumentPasswordView *)passwordView
810 {
811     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
812 }
813
814 - (void)didEndEditingPassword:(UITextField *)passwordField inView:(UIDocumentPasswordView *)passwordView
815 {
816     [_scrollView setContentSize:[_passwordView frame].size];
817     [_scrollView setContentOffset:CGPointMake(-_scrollView.contentInset.left, -_scrollView.contentInset.top) animated:YES];
818
819     [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidShowNotification object:nil];
820 }
821
822 - (void)_didFailToUnlock
823 {
824     [[_passwordView passwordField] setText:@""];
825     UIAlertController* alert = [UIAlertController alertControllerWithTitle:WEB_UI_STRING("The document could not be opened with that password.", "PDF password failure alert message") message:@"" preferredStyle:UIAlertControllerStyleAlert];
826
827     UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:WEB_UI_STRING_KEY("OK", "OK (PDF password failure alert)", "OK button label in PDF password failure alert") style:UIAlertActionStyleDefault handler:[](UIAlertAction *) { }];
828     
829     [alert addAction:defaultAction];
830
831     [self.window.rootViewController presentViewController:alert animated:YES completion:nil];
832 }
833
834 - (BOOL)_tryToUnlockWithPassword:(NSString *)password
835 {
836     if (CGPDFDocumentUnlockWithPassword(_cgPDFDocument.get(), [password UTF8String])) {
837         [self _hidePasswordEntryField];
838         [self _didLoadPDFDocument];
839         return YES;
840     }
841
842     [self _didFailToUnlock];
843     return NO;
844 }
845
846 @end
847
848 #endif /* PLATFORM(IOS) */