1/*
2 * Copyright (C) 2010, 2015 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#include "config.h"
27#include "FindController.h"
28
29#include "DrawingArea.h"
30#include "PluginView.h"
31#include "ShareableBitmap.h"
32#include "WKPage.h"
33#include "WebCoreArgumentCoders.h"
34#include "WebPage.h"
35#include "WebPageProxyMessages.h"
36#include <WebCore/DocumentMarkerController.h>
37#include <WebCore/FloatQuad.h>
38#include <WebCore/FocusController.h>
39#include <WebCore/Frame.h>
40#include <WebCore/FrameSelection.h>
41#include <WebCore/FrameView.h>
42#include <WebCore/GraphicsContext.h>
43#include <WebCore/Page.h>
44#include <WebCore/PageOverlayController.h>
45#include <WebCore/PathUtilities.h>
46#include <WebCore/PlatformMouseEvent.h>
47#include <WebCore/PluginDocument.h>
48
49#if PLATFORM(COCOA)
50#include <WebCore/TextIndicatorWindow.h>
51#endif
52
53namespace WebKit {
54using namespace WebCore;
55
56WebCore::FindOptions core(FindOptions options)
57{
58 WebCore::FindOptions result;
59 if (options & FindOptionsCaseInsensitive)
60 result.add(WebCore::CaseInsensitive);
61 if (options & FindOptionsAtWordStarts)
62 result.add(WebCore::AtWordStarts);
63 if (options & FindOptionsTreatMedialCapitalAsWordStart)
64 result.add(WebCore::TreatMedialCapitalAsWordStart);
65 if (options & FindOptionsBackwards)
66 result.add(WebCore::Backwards);
67 if (options & FindOptionsWrapAround)
68 result.add(WebCore::WrapAround);
69 return result;
70}
71
72FindController::FindController(WebPage* webPage)
73 : m_webPage(webPage)
74{
75}
76
77FindController::~FindController()
78{
79}
80
81void FindController::countStringMatches(const String& string, FindOptions options, unsigned maxMatchCount)
82{
83 if (maxMatchCount == std::numeric_limits<unsigned>::max())
84 --maxMatchCount;
85
86 auto* pluginView = WebPage::pluginViewForFrame(m_webPage->mainFrame());
87
88 unsigned matchCount;
89 if (pluginView)
90 matchCount = pluginView->countFindMatches(string, core(options), maxMatchCount + 1);
91 else {
92 matchCount = m_webPage->corePage()->countFindMatches(string, core(options), maxMatchCount + 1);
93 m_webPage->corePage()->unmarkAllTextMatches();
94 }
95
96 if (matchCount > maxMatchCount)
97 matchCount = static_cast<unsigned>(kWKMoreThanMaximumMatchCount);
98
99 m_webPage->send(Messages::WebPageProxy::DidCountStringMatches(string, matchCount));
100}
101
102uint32_t FindController::replaceMatches(const Vector<uint32_t>& matchIndices, const String& replacementText, bool selectionOnly)
103{
104 if (matchIndices.isEmpty())
105 return m_webPage->corePage()->replaceSelectionWithText(replacementText);
106
107 // FIXME: This is an arbitrary cap on the maximum number of matches to try and replace, to prevent the web process from
108 // hanging while replacing an enormous amount of matches. In the future, we should handle replacement in batches, and
109 // periodically update an NSProgress in the UI process when a batch of find-in-page matches are replaced.
110 const uint32_t maximumNumberOfMatchesToReplace = 1000;
111
112 Vector<Ref<Range>> rangesToReplace;
113 rangesToReplace.reserveCapacity(std::min<uint32_t>(maximumNumberOfMatchesToReplace, matchIndices.size()));
114 for (auto index : matchIndices) {
115 if (index < m_findMatches.size())
116 rangesToReplace.uncheckedAppend(*m_findMatches[index]);
117 if (rangesToReplace.size() >= maximumNumberOfMatchesToReplace)
118 break;
119 }
120 return m_webPage->corePage()->replaceRangesWithText(rangesToReplace, replacementText, selectionOnly);
121}
122
123static Frame* frameWithSelection(Page* page)
124{
125 for (Frame* frame = &page->mainFrame(); frame; frame = frame->tree().traverseNext()) {
126 if (frame->selection().isRange())
127 return frame;
128 }
129
130 return 0;
131}
132
133void FindController::updateFindUIAfterPageScroll(bool found, const String& string, FindOptions options, unsigned maxMatchCount, DidWrap didWrap)
134{
135 Frame* selectedFrame = frameWithSelection(m_webPage->corePage());
136
137 auto* pluginView = WebPage::pluginViewForFrame(m_webPage->mainFrame());
138
139 bool shouldShowOverlay = false;
140
141 if (!found) {
142 if (!pluginView)
143 m_webPage->corePage()->unmarkAllTextMatches();
144
145 if (selectedFrame)
146 selectedFrame->selection().clear();
147
148 hideFindIndicator();
149 didFailToFindString();
150
151 m_webPage->send(Messages::WebPageProxy::DidFailToFindString(string));
152 } else {
153 shouldShowOverlay = options & FindOptionsShowOverlay;
154 bool shouldShowHighlight = options & FindOptionsShowHighlight;
155 bool shouldDetermineMatchIndex = options & FindOptionsDetermineMatchIndex;
156 unsigned matchCount = 1;
157
158 if (shouldDetermineMatchIndex) {
159 if (pluginView)
160 matchCount = pluginView->countFindMatches(string, core(options), maxMatchCount + 1);
161 else
162 matchCount = m_webPage->corePage()->countFindMatches(string, core(options), maxMatchCount + 1);
163 }
164
165 if (shouldShowOverlay || shouldShowHighlight) {
166 if (maxMatchCount == std::numeric_limits<unsigned>::max())
167 --maxMatchCount;
168
169 if (pluginView) {
170 if (!shouldDetermineMatchIndex)
171 matchCount = pluginView->countFindMatches(string, core(options), maxMatchCount + 1);
172 shouldShowOverlay = false;
173 } else {
174 m_webPage->corePage()->unmarkAllTextMatches();
175 matchCount = m_webPage->corePage()->markAllMatchesForText(string, core(options), shouldShowHighlight, maxMatchCount + 1);
176 }
177
178 // If we have a large number of matches, we don't want to take the time to paint the overlay.
179 if (matchCount > maxMatchCount) {
180 shouldShowOverlay = false;
181 matchCount = static_cast<unsigned>(kWKMoreThanMaximumMatchCount);
182 }
183 }
184 if (matchCount == static_cast<unsigned>(kWKMoreThanMaximumMatchCount))
185 m_foundStringMatchIndex = -1;
186 else {
187 if (m_foundStringMatchIndex < 0)
188 m_foundStringMatchIndex += matchCount; // FIXME: Shouldn't this just be "="? Why is it correct to add to -1 here?
189 if (m_foundStringMatchIndex >= (int) matchCount)
190 m_foundStringMatchIndex -= matchCount;
191 }
192
193 m_findMatches.clear();
194 Vector<IntRect> matchRects;
195 if (auto range = m_webPage->corePage()->selection().firstRange()) {
196 range->absoluteTextRects(matchRects);
197 m_findMatches.append(range);
198 }
199
200 m_webPage->send(Messages::WebPageProxy::DidFindString(string, matchRects, matchCount, m_foundStringMatchIndex, didWrap == DidWrap::Yes));
201 }
202
203 if (!shouldShowOverlay) {
204 if (m_findPageOverlay)
205 m_webPage->corePage()->pageOverlayController().uninstallPageOverlay(*m_findPageOverlay, PageOverlay::FadeMode::Fade);
206 } else {
207 if (!m_findPageOverlay) {
208 auto findPageOverlay = PageOverlay::create(*this, PageOverlay::OverlayType::Document);
209 m_findPageOverlay = findPageOverlay.ptr();
210 m_webPage->corePage()->pageOverlayController().installPageOverlay(WTFMove(findPageOverlay), PageOverlay::FadeMode::Fade);
211 }
212 m_findPageOverlay->setNeedsDisplay();
213 }
214
215 if (found && (!(options & FindOptionsShowFindIndicator) || !selectedFrame || !updateFindIndicator(*selectedFrame, shouldShowOverlay)))
216 hideFindIndicator();
217}
218
219void FindController::findString(const String& string, FindOptions options, unsigned maxMatchCount)
220{
221 auto* pluginView = WebPage::pluginViewForFrame(m_webPage->mainFrame());
222
223 WebCore::FindOptions coreOptions = core(options);
224
225 // iOS will reveal the selection through a different mechanism, and
226 // we need to avoid sending the non-painted selection change to the UI process
227 // so that it does not clear the selection out from under us.
228#if PLATFORM(IOS_FAMILY)
229 coreOptions.add(DoNotRevealSelection);
230#endif
231
232 willFindString();
233
234 bool foundStringStartsAfterSelection = false;
235 if (!pluginView) {
236 if (Frame* selectedFrame = frameWithSelection(m_webPage->corePage())) {
237 FrameSelection& fs = selectedFrame->selection();
238 if (fs.selectionBounds().isEmpty()) {
239 m_findMatches.clear();
240 int indexForSelection;
241 m_webPage->corePage()->findStringMatchingRanges(string, coreOptions, maxMatchCount, m_findMatches, indexForSelection);
242 m_foundStringMatchIndex = indexForSelection;
243 foundStringStartsAfterSelection = true;
244 }
245 }
246 }
247
248 m_findMatches.clear();
249
250 bool found;
251 DidWrap didWrap = DidWrap::No;
252 if (pluginView)
253 found = pluginView->findString(string, coreOptions, maxMatchCount);
254 else
255 found = m_webPage->corePage()->findString(string, coreOptions, &didWrap);
256
257 if (found) {
258 didFindString();
259
260 if (!foundStringStartsAfterSelection) {
261 if (options & FindOptionsBackwards)
262 m_foundStringMatchIndex--;
263 else
264 m_foundStringMatchIndex++;
265 }
266 }
267
268 RefPtr<WebPage> protectedWebPage = m_webPage;
269 m_webPage->drawingArea()->dispatchAfterEnsuringUpdatedScrollPosition([protectedWebPage, found, string, options, maxMatchCount, didWrap] () {
270 protectedWebPage->findController().updateFindUIAfterPageScroll(found, string, options, maxMatchCount, didWrap);
271 });
272}
273
274void FindController::findStringMatches(const String& string, FindOptions options, unsigned maxMatchCount)
275{
276 m_findMatches.clear();
277 int indexForSelection;
278
279 m_webPage->corePage()->findStringMatchingRanges(string, core(options), maxMatchCount, m_findMatches, indexForSelection);
280
281 Vector<Vector<IntRect>> matchRects;
282 for (size_t i = 0; i < m_findMatches.size(); ++i) {
283 Vector<IntRect> rects;
284 m_findMatches[i]->absoluteTextRects(rects);
285 matchRects.append(WTFMove(rects));
286 }
287
288 m_webPage->send(Messages::WebPageProxy::DidFindStringMatches(string, matchRects, indexForSelection));
289}
290
291void FindController::getImageForFindMatch(uint32_t matchIndex)
292{
293 if (matchIndex >= m_findMatches.size())
294 return;
295 Frame* frame = m_findMatches[matchIndex]->startContainer().document().frame();
296 if (!frame)
297 return;
298
299 VisibleSelection oldSelection = frame->selection().selection();
300 frame->selection().setSelection(VisibleSelection(*m_findMatches[matchIndex]));
301
302 RefPtr<ShareableBitmap> selectionSnapshot = WebFrame::fromCoreFrame(*frame)->createSelectionSnapshot();
303
304 frame->selection().setSelection(oldSelection);
305
306 if (!selectionSnapshot)
307 return;
308
309 ShareableBitmap::Handle handle;
310 selectionSnapshot->createHandle(handle);
311
312 if (handle.isNull())
313 return;
314
315 m_webPage->send(Messages::WebPageProxy::DidGetImageForFindMatch(handle, matchIndex));
316}
317
318void FindController::selectFindMatch(uint32_t matchIndex)
319{
320 if (matchIndex >= m_findMatches.size())
321 return;
322 Frame* frame = m_findMatches[matchIndex]->startContainer().document().frame();
323 if (!frame)
324 return;
325 frame->selection().setSelection(VisibleSelection(*m_findMatches[matchIndex]));
326}
327
328void FindController::hideFindUI()
329{
330 m_findMatches.clear();
331 if (m_findPageOverlay)
332 m_webPage->corePage()->pageOverlayController().uninstallPageOverlay(*m_findPageOverlay, PageOverlay::FadeMode::Fade);
333
334 if (auto* pluginView = WebPage::pluginViewForFrame(m_webPage->mainFrame()))
335 pluginView->findString(emptyString(), { }, 0);
336 else
337 m_webPage->corePage()->unmarkAllTextMatches();
338
339 hideFindIndicator();
340}
341
342#if !PLATFORM(IOS_FAMILY)
343
344bool FindController::updateFindIndicator(Frame& selectedFrame, bool isShowingOverlay, bool shouldAnimate)
345{
346 auto indicator = TextIndicator::createWithSelectionInFrame(selectedFrame, TextIndicatorOptionIncludeMarginIfRangeMatchesSelection, shouldAnimate ? TextIndicatorPresentationTransition::Bounce : TextIndicatorPresentationTransition::None);
347 if (!indicator)
348 return false;
349
350 m_findIndicatorRect = enclosingIntRect(indicator->selectionRectInRootViewCoordinates());
351#if PLATFORM(COCOA)
352 m_webPage->send(Messages::WebPageProxy::SetTextIndicator(indicator->data(), static_cast<uint64_t>(isShowingOverlay ? TextIndicatorWindowLifetime::Permanent : TextIndicatorWindowLifetime::Temporary)));
353#endif
354 m_isShowingFindIndicator = true;
355
356 return true;
357}
358
359void FindController::hideFindIndicator()
360{
361 if (!m_isShowingFindIndicator)
362 return;
363
364 m_webPage->send(Messages::WebPageProxy::ClearTextIndicator());
365 m_isShowingFindIndicator = false;
366 m_foundStringMatchIndex = -1;
367 didHideFindIndicator();
368}
369
370void FindController::willFindString()
371{
372}
373
374void FindController::didFindString()
375{
376}
377
378void FindController::didFailToFindString()
379{
380}
381
382void FindController::didHideFindIndicator()
383{
384}
385
386unsigned FindController::findIndicatorRadius() const
387{
388 return 0;
389}
390
391bool FindController::shouldHideFindIndicatorOnScroll() const
392{
393 return true;
394}
395
396#endif
397
398void FindController::showFindIndicatorInSelection()
399{
400 Frame& selectedFrame = m_webPage->corePage()->focusController().focusedOrMainFrame();
401 updateFindIndicator(selectedFrame, false);
402}
403
404void FindController::deviceScaleFactorDidChange()
405{
406 ASSERT(isShowingOverlay());
407
408 Frame* selectedFrame = frameWithSelection(m_webPage->corePage());
409 if (!selectedFrame)
410 return;
411
412 updateFindIndicator(*selectedFrame, true, false);
413}
414
415void FindController::redraw()
416{
417 if (!m_isShowingFindIndicator)
418 return;
419
420 Frame* selectedFrame = frameWithSelection(m_webPage->corePage());
421 if (!selectedFrame)
422 return;
423
424 updateFindIndicator(*selectedFrame, isShowingOverlay(), false);
425}
426
427Vector<FloatRect> FindController::rectsForTextMatchesInRect(IntRect clipRect)
428{
429 Vector<FloatRect> rects;
430
431 FrameView* mainFrameView = m_webPage->corePage()->mainFrame().view();
432
433 for (Frame* frame = &m_webPage->corePage()->mainFrame(); frame; frame = frame->tree().traverseNext()) {
434 Document* document = frame->document();
435 if (!document)
436 continue;
437
438 for (FloatRect rect : document->markers().renderedRectsForMarkers(DocumentMarker::TextMatch)) {
439 if (!frame->isMainFrame())
440 rect = mainFrameView->windowToContents(frame->view()->contentsToWindow(enclosingIntRect(rect)));
441
442 if (rect.isEmpty() || !rect.intersects(clipRect))
443 continue;
444
445 rects.append(rect);
446 }
447 }
448
449 return rects;
450}
451
452void FindController::willMoveToPage(PageOverlay&, Page* page)
453{
454 if (page)
455 return;
456
457 ASSERT(m_findPageOverlay);
458 m_findPageOverlay = 0;
459}
460
461void FindController::didMoveToPage(PageOverlay&, Page*)
462{
463}
464
465const float shadowOffsetX = 0;
466const float shadowOffsetY = 0;
467const float shadowBlurRadius = 1;
468
469void FindController::drawRect(PageOverlay&, GraphicsContext& graphicsContext, const IntRect& dirtyRect)
470{
471 const int borderWidth = 1;
472
473 Color overlayBackgroundColor(0.1f, 0.1f, 0.1f, 0.25f);
474 Color shadowColor(0.0f, 0.0f, 0.0f, 0.5f);
475
476 IntRect borderInflatedDirtyRect = dirtyRect;
477 borderInflatedDirtyRect.inflate(borderWidth);
478 Vector<FloatRect> rects = rectsForTextMatchesInRect(borderInflatedDirtyRect);
479
480 // Draw the background.
481 graphicsContext.fillRect(dirtyRect, overlayBackgroundColor);
482
483 Vector<Path> whiteFramePaths = PathUtilities::pathsWithShrinkWrappedRects(rects, findIndicatorRadius());
484
485 GraphicsContextStateSaver stateSaver(graphicsContext);
486
487 // Draw white frames around the holes.
488 // We double the thickness because half of the stroke will be erased when we clear the holes.
489 graphicsContext.setShadow(FloatSize(shadowOffsetX, shadowOffsetY), shadowBlurRadius, shadowColor);
490 graphicsContext.setStrokeColor(Color::white);
491 graphicsContext.setStrokeThickness(borderWidth * 2);
492 for (auto& path : whiteFramePaths)
493 graphicsContext.strokePath(path);
494
495 graphicsContext.clearShadow();
496
497 // Clear out the holes.
498 graphicsContext.setCompositeOperation(CompositeClear);
499 for (auto& path : whiteFramePaths)
500 graphicsContext.fillPath(path);
501
502 if (!m_isShowingFindIndicator || !shouldHideFindIndicatorOnScroll())
503 return;
504
505 if (Frame* selectedFrame = frameWithSelection(m_webPage->corePage())) {
506 IntRect findIndicatorRect = selectedFrame->view()->contentsToRootView(enclosingIntRect(selectedFrame->selection().selectionBounds()));
507
508 if (findIndicatorRect != m_findIndicatorRect)
509 hideFindIndicator();
510 }
511}
512
513bool FindController::mouseEvent(PageOverlay&, const PlatformMouseEvent& mouseEvent)
514{
515 if (mouseEvent.type() == PlatformEvent::MousePressed)
516 hideFindUI();
517
518 return false;
519}
520
521void FindController::didInvalidateDocumentMarkerRects()
522{
523 if (m_findPageOverlay)
524 m_findPageOverlay->setNeedsDisplay();
525}
526
527} // namespace WebKit
528