/* * Copyright (C) 2015-2022 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "WKTextFinderClient.h" #if PLATFORM(MAC) #import "APIFindClient.h" #import "APIFindMatchesClient.h" #import "WebImage.h" #import "WebPageProxy.h" #import #import #import #import #import #import // FIXME: Implement scrollFindMatchToVisible. // FIXME: The NSTextFinder overlay doesn't move with scrolling; we should have a mode where we manage the overlay. static const NSUInteger maximumFindMatches = 1000; @interface WKTextFinderClient () - (void)didFindStringMatchesWithRects:(const Vector>&)rects didWrapAround:(BOOL)didWrapAround; - (void)getImageForMatchResult:(id )findMatch completionHandler:(void (^)(NSImage *generatedImage))completionHandler; - (void)didGetImageForMatchResult:(WebKit::WebImage *)string; @end namespace WebKit { using namespace WebCore; class TextFinderFindClient : public API::FindMatchesClient, public API::FindClient { WTF_MAKE_FAST_ALLOCATED; public: explicit TextFinderFindClient(WKTextFinderClient *textFinderClient) : m_textFinderClient(textFinderClient) { } private: void didFindStringMatches(WebPageProxy* page, const String&, const Vector>& matchRects, int32_t) override { [m_textFinderClient didFindStringMatchesWithRects:matchRects didWrapAround:NO]; } void didGetImageForMatchResult(WebPageProxy* page, WebImage* image, int32_t index) override { [m_textFinderClient didGetImageForMatchResult:image]; } void didFindString(WebPageProxy*, const String&, const Vector& matchRects, uint32_t matchCount, int32_t matchIndex, bool didWrapAround) override { Vector> allMatches; if (matchCount != static_cast(kWKMoreThanMaximumMatchCount)) { // Synthesize a vector of match rects for all `matchCount` matches, // filling in the actual rects for the one that we received. // The rest will remain empty, but it's important to NSTextFinder // that they at least exist. allMatches.resize(matchCount); // FIXME: Clean this up and figure out why we are getting a -1 index if (matchIndex >= 0 && static_cast(matchIndex) < matchCount) allMatches[matchIndex].appendVector(matchRects); } [m_textFinderClient didFindStringMatchesWithRects:allMatches didWrapAround:didWrapAround]; } void didFailToFindString(WebPageProxy*, const String& string) override { [m_textFinderClient didFindStringMatchesWithRects:{ } didWrapAround:NO]; } RetainPtr m_textFinderClient; }; } @interface WKTextFinderMatch : NSObject @property (nonatomic, readonly) unsigned index; @end @implementation WKTextFinderMatch { WKTextFinderClient *_client; NSView *_view; RetainPtr _rects; unsigned _index; } - (instancetype)initWithClient:(WKTextFinderClient *)client view:(NSView *)view index:(unsigned)index rects:(NSArray *)rects { self = [super init]; if (!self) return nil; _client = client; _view = view; _rects = rects; _index = index; return self; } - (NSView *)containingView { return _view; } - (NSArray *)textRects { return _rects.get(); } - (void)generateTextImage:(void (^)(NSImage *generatedImage))completionHandler { [_client getImageForMatchResult:self completionHandler:completionHandler]; } - (unsigned)index { return _index; } @end @implementation WKTextFinderClient { NakedPtr _page; NSView *_view; Deque> _findReplyCallbacks; Deque> _imageReplyCallbacks; BOOL _usePlatformFindUI; } - (instancetype)initWithPage:(NakedRef)page view:(NSView *)view usePlatformFindUI:(BOOL)usePlatformFindUI { self = [super init]; if (!self) return nil; _page = page.ptr(); _view = view; _usePlatformFindUI = usePlatformFindUI; _page->setFindMatchesClient(makeUnique(self)); _page->setFindClient(makeUnique(self)); return self; } - (void)willDestroyView:(NSView *)view { _page->setFindMatchesClient(nullptr); _page->setFindClient(nullptr); _page = nullptr; _view = nil; } #pragma mark - NSTextFinderClient SPI - (void)replaceMatches:(NSArray *)matches withString:(NSString *)replacementText inSelectionOnly:(BOOL)selectionOnly resultCollector:(void (^)(NSUInteger replacementCount))resultCollector { Vector matchIndices; matchIndices.reserveInitialCapacity(matches.count); for (id match in matches) { if ([match isKindOfClass:WKTextFinderMatch.class]) matchIndices.uncheckedAppend([(WKTextFinderMatch *)match index]); } _page->replaceMatches(WTFMove(matchIndices), replacementText, selectionOnly, [collector = makeBlockPtr(resultCollector)] (uint64_t numberOfReplacements) { collector(numberOfReplacements); }); } - (void)findMatchesForString:(NSString *)targetString relativeToMatch:(id )relativeMatch findOptions:(NSTextFinderAsynchronousDocumentFindOptions)findOptions maxResults:(NSUInteger)maxResults resultCollector:(void (^)(NSArray *matches, BOOL didWrap))resultCollector { // Limit the number of results, for performance reasons; NSTextFinder always // passes either 1 or NSUIntegerMax, which results in search time being // proportional to document length. maxResults = std::min(maxResults, maximumFindMatches); OptionSet kitFindOptions; if (findOptions & NSTextFinderAsynchronousDocumentFindOptionsBackwards) kitFindOptions.add(WebKit::FindOptions::Backwards); if (findOptions & NSTextFinderAsynchronousDocumentFindOptionsWrap) kitFindOptions.add(WebKit::FindOptions::WrapAround); if (findOptions & NSTextFinderAsynchronousDocumentFindOptionsCaseInsensitive) kitFindOptions.add(WebKit::FindOptions::CaseInsensitive); if (findOptions & NSTextFinderAsynchronousDocumentFindOptionsStartsWith) kitFindOptions.add(WebKit::FindOptions::AtWordStarts); if (!_usePlatformFindUI) { kitFindOptions.add(WebKit::FindOptions::ShowOverlay); kitFindOptions.add(WebKit::FindOptions::ShowFindIndicator); kitFindOptions.add(WebKit::FindOptions::DetermineMatchIndex); } RetainPtr progress = [NSProgress progressWithTotalUnitCount:1]; auto copiedResultCollector = Block_copy(resultCollector); _findReplyCallbacks.append([progress, copiedResultCollector] (NSArray *matches, bool didWrap) { [progress setCompletedUnitCount:1]; copiedResultCollector(matches, didWrap); Block_release(copiedResultCollector); }); if (maxResults == 1) _page->findString(targetString, kitFindOptions, maxResults); else _page->findStringMatches(targetString, kitFindOptions, maxResults); } - (void)getSelectedText:(void (^)(NSString *selectedTextString))completionHandler { void (^copiedCompletionHandler)(NSString *) = Block_copy(completionHandler); _page->getSelectionOrContentsAsString([copiedCompletionHandler] (const String& string) { copiedCompletionHandler(string); Block_release(copiedCompletionHandler); }); } - (void)selectFindMatch:(id )findMatch completionHandler:(void (^)(void))completionHandler { ASSERT([findMatch isKindOfClass:[WKTextFinderMatch class]]); ASSERT(!completionHandler); WKTextFinderMatch *textFinderMatch = static_cast(findMatch); _page->selectFindMatch(textFinderMatch.index); } - (void)scrollFindMatchToVisible:(id )findMatch { if (_usePlatformFindUI) return; ASSERT([findMatch isKindOfClass:[WKTextFinderMatch class]]); WKTextFinderMatch *textFinderMatch = static_cast(findMatch); _page->indicateFindMatch(textFinderMatch.index); } #pragma mark - FindMatchesClient - (void)didFindStringMatchesWithRects:(const Vector>&)rectsForMatches didWrapAround:(BOOL)didWrapAround { if (_findReplyCallbacks.isEmpty()) return; unsigned index = 0; auto matchObjects = createNSArray(rectsForMatches, [&] (auto& rects) { RetainPtr rectsArray; if (_usePlatformFindUI) rectsArray = createNSArray(rects); else rectsArray = @[]; return adoptNS([[WKTextFinderMatch alloc] initWithClient:self view:_view index:index++ rects:rectsArray.get()]); }); _findReplyCallbacks.takeFirst()(matchObjects.get(), didWrapAround); } - (void)didGetImageForMatchResult:(WebKit::WebImage *)image { if (_imageReplyCallbacks.isEmpty()) return; auto size = image->size(); size.scale(1 / _page->deviceScaleFactor()); auto nativeImage = image->copyNativeImage(WebCore::DontCopyBackingStore); if (!nativeImage) return; auto imageCallback = _imageReplyCallbacks.takeFirst(); imageCallback(adoptNS([[NSImage alloc] initWithCGImage:nativeImage->platformImage().get() size:size]).get()); } #pragma mark - WKTextFinderMatch callbacks - (void)getImageForMatchResult:(id )findMatch completionHandler:(void (^)(NSImage *generatedImage))completionHandler { if (!_usePlatformFindUI) { completionHandler(nil); return; } ASSERT([findMatch isKindOfClass:[WKTextFinderMatch class]]); WKTextFinderMatch *textFinderMatch = static_cast(findMatch); auto copiedImageCallback = Block_copy(completionHandler); _imageReplyCallbacks.append([copiedImageCallback] (NSImage *image) { copiedImageCallback(image); Block_release(copiedImageCallback); }); // FIXME: There is no guarantee that this will ever result in didGetImageForMatchResult // being called (and thus us calling our completion handler); we should harden this // against all of the early returns in FindController::getImageForFindMatch. _page->getImageForFindMatch(textFinderMatch.index); } @end #endif // PLATFORM(MAC)