Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / web / TextFinder.cpp
blob060d446c65d9a3cb3d9b6d8d579cc5d88596f752
1 /*
2 * Copyright (C) 2009 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 #include "config.h"
33 #include "web/TextFinder.h"
35 #include "core/dom/Range.h"
36 #include "core/dom/shadow/ShadowRoot.h"
37 #include "core/editing/Editor.h"
38 #include "core/editing/VisibleSelection.h"
39 #include "core/editing/iterators/SearchBuffer.h"
40 #include "core/editing/markers/DocumentMarker.h"
41 #include "core/editing/markers/DocumentMarkerController.h"
42 #include "core/frame/FrameView.h"
43 #include "core/layout/LayoutObject.h"
44 #include "core/layout/TextAutosizer.h"
45 #include "core/page/Page.h"
46 #include "modules/accessibility/AXObject.h"
47 #include "modules/accessibility/AXObjectCacheImpl.h"
48 #include "platform/RuntimeEnabledFeatures.h"
49 #include "platform/Timer.h"
50 #include "public/platform/WebVector.h"
51 #include "public/web/WebAXObject.h"
52 #include "public/web/WebFindOptions.h"
53 #include "public/web/WebFrameClient.h"
54 #include "public/web/WebViewClient.h"
55 #include "web/FindInPageCoordinates.h"
56 #include "web/WebLocalFrameImpl.h"
57 #include "web/WebViewImpl.h"
58 #include "wtf/CurrentTime.h"
60 namespace blink {
62 TextFinder::FindMatch::FindMatch(PassRefPtrWillBeRawPtr<Range> range, int ordinal)
63 : m_range(range)
64 , m_ordinal(ordinal)
68 DEFINE_TRACE(TextFinder::FindMatch)
70 visitor->trace(m_range);
73 class TextFinder::DeferredScopeStringMatches : public NoBaseWillBeGarbageCollectedFinalized<TextFinder::DeferredScopeStringMatches> {
74 public:
75 static PassOwnPtrWillBeRawPtr<DeferredScopeStringMatches> create(TextFinder* textFinder, int identifier, const WebString& searchText, const WebFindOptions& options, bool reset)
77 return adoptPtrWillBeNoop(new DeferredScopeStringMatches(textFinder, identifier, searchText, options, reset));
80 DEFINE_INLINE_TRACE()
82 visitor->trace(m_textFinder);
85 void dispose()
87 if (m_timer.isActive())
88 m_timer.stop();
91 private:
92 DeferredScopeStringMatches(TextFinder* textFinder, int identifier, const WebString& searchText, const WebFindOptions& options, bool reset)
93 : m_timer(this, &DeferredScopeStringMatches::doTimeout)
94 , m_textFinder(textFinder)
95 , m_identifier(identifier)
96 , m_searchText(searchText)
97 , m_options(options)
98 , m_reset(reset)
100 m_timer.startOneShot(0.0, FROM_HERE);
103 void doTimeout(Timer<DeferredScopeStringMatches>*)
105 m_textFinder->callScopeStringMatches(this, m_identifier, m_searchText, m_options, m_reset);
108 Timer<DeferredScopeStringMatches> m_timer;
109 RawPtrWillBeMember<TextFinder> m_textFinder;
110 const int m_identifier;
111 const WebString m_searchText;
112 const WebFindOptions m_options;
113 const bool m_reset;
116 bool TextFinder::find(int identifier, const WebString& searchText, const WebFindOptions& options, bool wrapWithinFrame, WebRect* selectionRect)
118 if (!ownerFrame().frame() || !ownerFrame().frame()->page())
119 return false;
121 WebLocalFrameImpl* mainFrameImpl = ownerFrame().viewImpl()->mainFrameImpl();
123 if (!options.findNext)
124 ownerFrame().frame()->page()->unmarkAllTextMatches();
125 else
126 setMarkerActive(m_activeMatch.get(), false);
128 if (m_activeMatch && &m_activeMatch->ownerDocument() != ownerFrame().frame()->document())
129 m_activeMatch = nullptr;
131 // If the user has selected something since the last Find operation we want
132 // to start from there. Otherwise, we start searching from where the last Find
133 // operation left off (either a Find or a FindNext operation).
134 VisibleSelection selection(ownerFrame().frame()->selection().selection());
135 bool activeSelection = !selection.isNone();
136 if (activeSelection) {
137 m_activeMatch = firstRangeOf(selection).get();
138 ownerFrame().frame()->selection().clear();
141 ASSERT(ownerFrame().frame() && ownerFrame().frame()->view());
142 const FindOptions findOptions = (options.forward ? 0 : Backwards)
143 | (options.matchCase ? 0 : CaseInsensitive)
144 | (wrapWithinFrame ? WrapAround : 0)
145 | (options.wordStart ? AtWordStarts : 0)
146 | (options.medialCapitalAsWordStart ? TreatMedialCapitalAsWordStart : 0)
147 | (options.findNext ? 0 : StartInSelection);
148 m_activeMatch = ownerFrame().frame()->editor().findStringAndScrollToVisible(searchText, m_activeMatch.get(), findOptions);
150 if (!m_activeMatch) {
151 // If we're finding next the next active match might not be in the current frame.
152 // In this case we don't want to clear the matches cache.
153 if (!options.findNext)
154 clearFindMatchesCache();
156 ownerFrame().frameView()->invalidatePaintForTickmarks();
157 return false;
160 // If the user is browsing a page with autosizing, adjust the zoom to the
161 // column where the next hit has been found. Doing this when autosizing is
162 // not set will result in a zoom reset on small devices.
163 if (ownerFrame().frame()->document()->textAutosizer()->pageNeedsAutosizing()) {
164 ownerFrame().viewImpl()->zoomToFindInPageRect(ownerFrame().frameView()->contentsToRootFrame(enclosingIntRect(LayoutObject::absoluteBoundingBoxRectForRange(m_activeMatch.get()))));
167 setMarkerActive(m_activeMatch.get(), true);
168 WebLocalFrameImpl* oldActiveFrame = mainFrameImpl->ensureTextFinder().m_currentActiveMatchFrame;
169 mainFrameImpl->ensureTextFinder().m_currentActiveMatchFrame = &ownerFrame();
171 // Make sure no node is focused. See http://crbug.com/38700.
172 ownerFrame().frame()->document()->setFocusedElement(nullptr);
174 if (!options.findNext || activeSelection) {
175 // This is either a Find operation or a Find-next from a new start point
176 // due to a selection, so we set the flag to ask the scoping effort
177 // to find the active rect for us and report it back to the UI.
178 m_locatingActiveRect = true;
179 } else {
180 if (oldActiveFrame != &ownerFrame()) {
181 if (options.forward)
182 m_activeMatchIndexInCurrentFrame = 0;
183 else
184 m_activeMatchIndexInCurrentFrame = m_lastMatchCount - 1;
185 } else {
186 if (options.forward)
187 ++m_activeMatchIndexInCurrentFrame;
188 else
189 --m_activeMatchIndexInCurrentFrame;
191 if (m_activeMatchIndexInCurrentFrame + 1 > m_lastMatchCount)
192 m_activeMatchIndexInCurrentFrame = 0;
193 if (m_activeMatchIndexInCurrentFrame == -1)
194 m_activeMatchIndexInCurrentFrame = m_lastMatchCount - 1;
196 if (selectionRect) {
197 *selectionRect = ownerFrame().frameView()->contentsToRootFrame(m_activeMatch->boundingBox());
198 reportFindInPageSelection(*selectionRect, m_activeMatchIndexInCurrentFrame + 1, identifier);
202 return true;
205 void TextFinder::stopFindingAndClearSelection()
207 cancelPendingScopingEffort();
209 // Remove all markers for matches found and turn off the highlighting.
210 ownerFrame().frame()->document()->markers().removeMarkers(DocumentMarker::TextMatch);
211 ownerFrame().frame()->editor().setMarkedTextMatchesAreHighlighted(false);
212 clearFindMatchesCache();
214 // Let the frame know that we don't want tickmarks anymore.
215 ownerFrame().frameView()->invalidatePaintForTickmarks();
218 void TextFinder::reportFindInPageResultToAccessibility(int identifier)
220 AXObjectCacheImpl* axObjectCache = toAXObjectCacheImpl(ownerFrame().frame()->document()->existingAXObjectCache());
221 if (!axObjectCache)
222 return;
224 AXObject* startObject = axObjectCache->get(m_activeMatch->startContainer());
225 AXObject* endObject = axObjectCache->get(m_activeMatch->endContainer());
226 if (!startObject || !endObject)
227 return;
229 WebLocalFrameImpl* mainFrameImpl = ownerFrame().viewImpl()->mainFrameImpl();
230 if (mainFrameImpl && mainFrameImpl->client()) {
231 mainFrameImpl->client()->handleAccessibilityFindInPageResult(
232 identifier, m_activeMatchIndexInCurrentFrame + 1,
233 WebAXObject(startObject), m_activeMatch->startOffset(),
234 WebAXObject(endObject), m_activeMatch->endOffset());
238 template <typename Strategy>
239 void TextFinder::scopeStringMatchesAlgorithm(int identifier, const WebString& searchText, const WebFindOptions& options, bool reset)
241 if (reset) {
242 // This is a brand new search, so we need to reset everything.
243 // Scoping is just about to begin.
244 m_scopingInProgress = true;
246 // Need to keep the current identifier locally in order to finish the
247 // request in case the frame is detached during the process.
248 m_findRequestIdentifier = identifier;
250 // Clear highlighting for this frame.
251 LocalFrame* frame = ownerFrame().frame();
252 if (frame && frame->page() && frame->editor().markedTextMatchesAreHighlighted())
253 frame->page()->unmarkAllTextMatches();
255 // Clear the tickmarks and results cache.
256 clearFindMatchesCache();
258 // Clear the counters from last operation.
259 m_lastMatchCount = 0;
260 m_nextInvalidateAfter = 0;
261 m_resumeScopingFromRange = nullptr;
263 // The view might be null on detached frames.
264 if (frame && frame->page())
265 ownerFrame().viewImpl()->mainFrameImpl()->ensureTextFinder().m_framesScopingCount++;
267 // Now, defer scoping until later to allow find operation to finish quickly.
268 scopeStringMatchesSoon(identifier, searchText, options, false); // false means just reset, so don't do it again.
269 return;
272 if (!shouldScopeMatches(searchText)) {
273 // Note that we want to defer the final update when resetting even if shouldScopeMatches returns false.
274 // This is done in order to prevent sending a final message based only on the results of the first frame
275 // since m_framesScopingCount would be 0 as other frames have yet to reset.
276 finishCurrentScopingEffort(identifier);
277 return;
280 WebLocalFrameImpl* mainFrameImpl = ownerFrame().viewImpl()->mainFrameImpl();
281 PositionAlgorithm<Strategy> searchStart = PositionAlgorithm<Strategy>::firstPositionInNode(ownerFrame().frame()->document());
282 PositionAlgorithm<Strategy> searchEnd = PositionAlgorithm<Strategy>::lastPositionInNode(ownerFrame().frame()->document());
283 ASSERT(searchStart.document() == searchEnd.document());
285 if (m_resumeScopingFromRange) {
286 // This is a continuation of a scoping operation that timed out and didn't
287 // complete last time around, so we should start from where we left off.
288 ASSERT(m_resumeScopingFromRange->collapsed());
289 searchStart = fromPositionInDOMTree<Strategy>(m_resumeScopingFromRange->endPosition());
290 if (searchStart.document() != searchEnd.document())
291 return;
294 // This timeout controls how long we scope before releasing control. This
295 // value does not prevent us from running for longer than this, but it is
296 // periodically checked to see if we have exceeded our allocated time.
297 const double maxScopingDuration = 0.1; // seconds
299 int matchCount = 0;
300 bool timedOut = false;
301 double startTime = currentTime();
302 do {
303 // Find next occurrence of the search string.
304 // FIXME: (http://crbug.com/6818) This WebKit operation may run for longer
305 // than the timeout value, and is not interruptible as it is currently
306 // written. We may need to rewrite it with interruptibility in mind, or
307 // find an alternative.
308 EphemeralRangeTemplate<Strategy> result = findPlainText(EphemeralRangeTemplate<Strategy>(searchStart, searchEnd), searchText, options.matchCase ? 0 : CaseInsensitive);
309 if (result.isCollapsed()) {
310 // Not found.
311 break;
313 RefPtrWillBeRawPtr<Range> resultRange = Range::create(result.document(), toPositionInDOMTree(result.startPosition()), toPositionInDOMTree(result.endPosition()));
314 if (resultRange->collapsed()) {
315 // resultRange will be collapsed if the matched text spans over multiple TreeScopes.
316 // FIXME: Show such matches to users.
317 searchStart = result.endPosition();
318 continue;
321 ++matchCount;
323 // Catch a special case where Find found something but doesn't know what
324 // the bounding box for it is. In this case we set the first match we find
325 // as the active rect.
326 IntRect resultBounds = resultRange->boundingBox();
327 IntRect activeSelectionRect;
328 if (m_locatingActiveRect) {
329 activeSelectionRect = m_activeMatch.get() ?
330 m_activeMatch->boundingBox() : resultBounds;
333 // If the Find function found a match it will have stored where the
334 // match was found in m_activeSelectionRect on the current frame. If we
335 // find this rect during scoping it means we have found the active
336 // tickmark.
337 bool foundActiveMatch = false;
338 if (m_locatingActiveRect && (activeSelectionRect == resultBounds)) {
339 // We have found the active tickmark frame.
340 mainFrameImpl->ensureTextFinder().m_currentActiveMatchFrame = &ownerFrame();
341 foundActiveMatch = true;
342 // We also know which tickmark is active now.
343 m_activeMatchIndexInCurrentFrame = matchCount - 1;
344 // To stop looking for the active tickmark, we set this flag.
345 m_locatingActiveRect = false;
347 // Notify browser of new location for the selected rectangle.
348 reportFindInPageSelection(
349 ownerFrame().frameView()->contentsToRootFrame(resultBounds),
350 m_activeMatchIndexInCurrentFrame + 1,
351 identifier);
354 addMarker(resultRange.get(), foundActiveMatch);
356 m_findMatchesCache.append(FindMatch(resultRange.get(), m_lastMatchCount + matchCount));
358 // Set the new start for the search range to be the end of the previous
359 // result range. There is no need to use a VisiblePosition here,
360 // since findPlainText will use a TextIterator to go over the visible
361 // text nodes.
362 searchStart = result.endPosition();
364 m_resumeScopingFromRange = Range::create(result.document(), toPositionInDOMTree(result.endPosition()), toPositionInDOMTree(result.endPosition()));
365 timedOut = (currentTime() - startTime) >= maxScopingDuration;
366 } while (!timedOut);
368 // Remember what we search for last time, so we can skip searching if more
369 // letters are added to the search string (and last outcome was 0).
370 m_lastSearchString = searchText;
372 if (matchCount > 0) {
373 ownerFrame().frame()->editor().setMarkedTextMatchesAreHighlighted(true);
375 m_lastMatchCount += matchCount;
377 // Let the mainframe know how much we found during this pass.
378 mainFrameImpl->increaseMatchCount(matchCount, identifier);
381 if (timedOut) {
382 // If we found anything during this pass, we should redraw. However, we
383 // don't want to spam too much if the page is extremely long, so if we
384 // reach a certain point we start throttling the redraw requests.
385 if (matchCount > 0)
386 invalidateIfNecessary();
388 // Scoping effort ran out of time, lets ask for another time-slice.
389 scopeStringMatchesSoon(
390 identifier,
391 searchText,
392 options,
393 false); // don't reset.
394 return; // Done for now, resume work later.
397 finishCurrentScopingEffort(identifier);
400 void TextFinder::scopeStringMatches(int identifier, const WebString& searchText, const WebFindOptions& options, bool reset)
402 if (RuntimeEnabledFeatures::selectionForComposedTreeEnabled())
403 return scopeStringMatchesAlgorithm<EditingInComposedTreeStrategy>(identifier, searchText, options, reset);
404 scopeStringMatchesAlgorithm<EditingStrategy>(identifier, searchText, options, reset);
407 void TextFinder::flushCurrentScopingEffort(int identifier)
409 if (!ownerFrame().frame() || !ownerFrame().frame()->page())
410 return;
412 WebLocalFrameImpl* mainFrameImpl = ownerFrame().viewImpl()->mainFrameImpl();
413 mainFrameImpl->ensureTextFinder().decrementFramesScopingCount(identifier);
416 void TextFinder::finishCurrentScopingEffort(int identifier)
418 flushCurrentScopingEffort(identifier);
420 m_scopingInProgress = false;
421 m_lastFindRequestCompletedWithNoMatches = !m_lastMatchCount;
423 // This frame is done, so show any scrollbar tickmarks we haven't drawn yet.
424 ownerFrame().frameView()->invalidatePaintForTickmarks();
427 void TextFinder::cancelPendingScopingEffort()
429 #if ENABLE(OILPAN)
430 for (DeferredScopeStringMatches* deferredWork : m_deferredScopingWork)
431 deferredWork->dispose();
432 #endif
433 m_deferredScopingWork.clear();
435 m_activeMatchIndexInCurrentFrame = -1;
437 // Last request didn't complete.
438 if (m_scopingInProgress)
439 m_lastFindRequestCompletedWithNoMatches = false;
441 m_scopingInProgress = false;
444 void TextFinder::increaseMatchCount(int identifier, int count)
446 if (count)
447 ++m_findMatchMarkersVersion;
449 m_totalMatchCount += count;
451 // Update the UI with the latest findings.
452 if (ownerFrame().client())
453 ownerFrame().client()->reportFindInPageMatchCount(identifier, m_totalMatchCount, !m_framesScopingCount);
456 void TextFinder::reportFindInPageSelection(const WebRect& selectionRect, int activeMatchOrdinal, int identifier)
458 // Update the UI with the latest selection rect.
459 if (ownerFrame().client())
460 ownerFrame().client()->reportFindInPageSelection(identifier, ordinalOfFirstMatch() + activeMatchOrdinal, selectionRect);
462 // Update accessibility too, so if the user commits to this query
463 // we can move accessibility focus to this result.
464 reportFindInPageResultToAccessibility(identifier);
467 void TextFinder::resetMatchCount()
469 if (m_totalMatchCount > 0)
470 ++m_findMatchMarkersVersion;
472 m_totalMatchCount = 0;
473 m_framesScopingCount = 0;
476 void TextFinder::clearFindMatchesCache()
478 if (!m_findMatchesCache.isEmpty())
479 ownerFrame().viewImpl()->mainFrameImpl()->ensureTextFinder().m_findMatchMarkersVersion++;
481 m_findMatchesCache.clear();
482 m_findMatchRectsAreValid = false;
485 bool TextFinder::isActiveMatchFrameValid() const
487 WebLocalFrameImpl* mainFrameImpl = ownerFrame().viewImpl()->mainFrameImpl();
488 WebLocalFrameImpl* activeMatchFrame = mainFrameImpl->activeMatchFrame();
489 return activeMatchFrame && activeMatchFrame->activeMatch() && activeMatchFrame->frame()->tree().isDescendantOf(mainFrameImpl->frame());
492 void TextFinder::updateFindMatchRects()
494 IntSize currentContentsSize = ownerFrame().contentsSize();
495 if (m_contentsSizeForCurrentFindMatchRects != currentContentsSize) {
496 m_contentsSizeForCurrentFindMatchRects = currentContentsSize;
497 m_findMatchRectsAreValid = false;
500 size_t deadMatches = 0;
501 for (FindMatch& match : m_findMatchesCache) {
502 if (!match.m_range->boundaryPointsValid() || !match.m_range->startContainer()->inDocument())
503 match.m_rect = FloatRect();
504 else if (!m_findMatchRectsAreValid)
505 match.m_rect = findInPageRectFromRange(match.m_range.get());
507 if (match.m_rect.isEmpty())
508 ++deadMatches;
511 // Remove any invalid matches from the cache.
512 if (deadMatches) {
513 WillBeHeapVector<FindMatch> filteredMatches;
514 filteredMatches.reserveCapacity(m_findMatchesCache.size() - deadMatches);
516 for (const FindMatch& match : m_findMatchesCache) {
517 if (!match.m_rect.isEmpty())
518 filteredMatches.append(match);
521 m_findMatchesCache.swap(filteredMatches);
524 // Invalidate the rects in child frames. Will be updated later during traversal.
525 if (!m_findMatchRectsAreValid)
526 for (WebFrame* child = ownerFrame().firstChild(); child; child = child->nextSibling())
527 toWebLocalFrameImpl(child)->ensureTextFinder().m_findMatchRectsAreValid = false;
529 m_findMatchRectsAreValid = true;
532 WebFloatRect TextFinder::activeFindMatchRect()
534 if (!isActiveMatchFrameValid())
535 return WebFloatRect();
537 return WebFloatRect(findInPageRectFromRange(m_currentActiveMatchFrame->activeMatch()));
540 void TextFinder::findMatchRects(WebVector<WebFloatRect>& outputRects)
542 Vector<WebFloatRect> matchRects;
543 for (WebLocalFrameImpl* frame = &ownerFrame(); frame; frame = toWebLocalFrameImpl(frame->traverseNext(false)))
544 frame->ensureTextFinder().appendFindMatchRects(matchRects);
546 outputRects = matchRects;
549 void TextFinder::appendFindMatchRects(Vector<WebFloatRect>& frameRects)
551 updateFindMatchRects();
552 frameRects.reserveCapacity(frameRects.size() + m_findMatchesCache.size());
553 for (const FindMatch& match : m_findMatchesCache) {
554 ASSERT(!match.m_rect.isEmpty());
555 frameRects.append(match.m_rect);
559 int TextFinder::selectNearestFindMatch(const WebFloatPoint& point, WebRect* selectionRect)
561 TextFinder* bestFinder = nullptr;
562 int indexInBestFrame = -1;
563 float distanceInBestFrame = FLT_MAX;
565 for (WebLocalFrameImpl* frame = &ownerFrame(); frame; frame = toWebLocalFrameImpl(frame->traverseNext(false))) {
566 float distanceInFrame;
567 TextFinder& finder = frame->ensureTextFinder();
568 int indexInFrame = finder.nearestFindMatch(point, distanceInFrame);
569 if (distanceInFrame < distanceInBestFrame) {
570 bestFinder = &finder;
571 indexInBestFrame = indexInFrame;
572 distanceInBestFrame = distanceInFrame;
576 if (indexInBestFrame != -1)
577 return bestFinder->selectFindMatch(static_cast<unsigned>(indexInBestFrame), selectionRect);
579 return -1;
582 int TextFinder::nearestFindMatch(const FloatPoint& point, float& distanceSquared)
584 updateFindMatchRects();
586 int nearest = -1;
587 distanceSquared = FLT_MAX;
588 for (size_t i = 0; i < m_findMatchesCache.size(); ++i) {
589 ASSERT(!m_findMatchesCache[i].m_rect.isEmpty());
590 FloatSize offset = point - m_findMatchesCache[i].m_rect.center();
591 float width = offset.width();
592 float height = offset.height();
593 float currentDistanceSquared = width * width + height * height;
594 if (currentDistanceSquared < distanceSquared) {
595 nearest = i;
596 distanceSquared = currentDistanceSquared;
599 return nearest;
602 int TextFinder::selectFindMatch(unsigned index, WebRect* selectionRect)
604 ASSERT_WITH_SECURITY_IMPLICATION(index < m_findMatchesCache.size());
606 RefPtrWillBeRawPtr<Range> range = m_findMatchesCache[index].m_range;
607 if (!range->boundaryPointsValid() || !range->startContainer()->inDocument())
608 return -1;
610 // Check if the match is already selected.
611 TextFinder& mainFrameTextFinder = ownerFrame().viewImpl()->mainFrameImpl()->ensureTextFinder();
612 WebLocalFrameImpl* activeMatchFrame = mainFrameTextFinder.m_currentActiveMatchFrame;
613 if (&ownerFrame() != activeMatchFrame || !m_activeMatch || !areRangesEqual(m_activeMatch.get(), range.get())) {
614 if (isActiveMatchFrameValid())
615 activeMatchFrame->ensureTextFinder().setMatchMarkerActive(false);
617 m_activeMatchIndexInCurrentFrame = m_findMatchesCache[index].m_ordinal - 1;
619 // Set this frame as the active frame (the one with the active highlight).
620 mainFrameTextFinder.m_currentActiveMatchFrame = &ownerFrame();
621 ownerFrame().viewImpl()->setFocusedFrame(&ownerFrame());
623 m_activeMatch = range.release();
624 setMarkerActive(m_activeMatch.get(), true);
626 // Clear any user selection, to make sure Find Next continues on from the match we just activated.
627 ownerFrame().frame()->selection().clear();
629 // Make sure no node is focused. See http://crbug.com/38700.
630 ownerFrame().frame()->document()->setFocusedElement(nullptr);
633 IntRect activeMatchRect;
634 IntRect activeMatchBoundingBox = enclosingIntRect(LayoutObject::absoluteBoundingBoxRectForRange(m_activeMatch.get()));
636 if (!activeMatchBoundingBox.isEmpty()) {
637 if (m_activeMatch->firstNode() && m_activeMatch->firstNode()->layoutObject()) {
638 m_activeMatch->firstNode()->layoutObject()->scrollRectToVisible(
639 LayoutRect(activeMatchBoundingBox), ScrollAlignment::alignCenterIfNeeded, ScrollAlignment::alignCenterIfNeeded);
642 // Zoom to the active match.
643 activeMatchRect = ownerFrame().frameView()->contentsToRootFrame(activeMatchBoundingBox);
644 ownerFrame().viewImpl()->zoomToFindInPageRect(activeMatchRect);
647 if (selectionRect)
648 *selectionRect = activeMatchRect;
650 return ordinalOfFirstMatch() + m_activeMatchIndexInCurrentFrame + 1;
653 PassOwnPtrWillBeRawPtr<TextFinder> TextFinder::create(WebLocalFrameImpl& ownerFrame)
655 return adoptPtrWillBeNoop(new TextFinder(ownerFrame));
658 TextFinder::TextFinder(WebLocalFrameImpl& ownerFrame)
659 : m_ownerFrame(&ownerFrame)
660 , m_currentActiveMatchFrame(nullptr)
661 , m_activeMatchIndexInCurrentFrame(-1)
662 , m_resumeScopingFromRange(nullptr)
663 , m_lastMatchCount(-1)
664 , m_totalMatchCount(-1)
665 , m_framesScopingCount(-1)
666 , m_findRequestIdentifier(-1)
667 , m_nextInvalidateAfter(0)
668 , m_findMatchMarkersVersion(0)
669 , m_locatingActiveRect(false)
670 , m_scopingInProgress(false)
671 , m_lastFindRequestCompletedWithNoMatches(false)
672 , m_findMatchRectsAreValid(false)
676 TextFinder::~TextFinder()
678 #if !ENABLE(OILPAN)
679 cancelPendingScopingEffort();
680 #endif
683 void TextFinder::addMarker(Range* range, bool activeMatch)
685 ownerFrame().frame()->document()->markers().addTextMatchMarker(range, activeMatch);
688 void TextFinder::setMarkerActive(Range* range, bool active)
690 if (!range || range->collapsed())
691 return;
692 ownerFrame().frame()->document()->markers().setMarkersActive(range, active);
695 int TextFinder::ordinalOfFirstMatchForFrame(WebLocalFrameImpl* frame) const
697 int ordinal = 0;
698 WebLocalFrameImpl* mainFrameImpl = ownerFrame().viewImpl()->mainFrameImpl();
699 // Iterate from the main frame up to (but not including) |frame| and
700 // add up the number of matches found so far.
701 for (WebLocalFrameImpl* it = mainFrameImpl; it != frame; it = toWebLocalFrameImpl(it->traverseNext(true))) {
702 TextFinder& finder = it->ensureTextFinder();
703 if (finder.m_lastMatchCount > 0)
704 ordinal += finder.m_lastMatchCount;
706 return ordinal;
709 bool TextFinder::shouldScopeMatches(const String& searchText)
711 // Don't scope if we can't find a frame or a view.
712 // The user may have closed the tab/application, so abort.
713 // Also ignore detached frames, as many find operations report to the main frame.
714 LocalFrame* frame = ownerFrame().frame();
715 if (!frame || !frame->view() || !frame->page() || !ownerFrame().hasVisibleContent())
716 return false;
718 ASSERT(frame->document() && frame->view());
720 // If the frame completed the scoping operation and found 0 matches the last
721 // time it was searched, then we don't have to search it again if the user is
722 // just adding to the search string or sending the same search string again.
723 if (m_lastFindRequestCompletedWithNoMatches && !m_lastSearchString.isEmpty()) {
724 // Check to see if the search string prefixes match.
725 String previousSearchPrefix =
726 searchText.substring(0, m_lastSearchString.length());
728 if (previousSearchPrefix == m_lastSearchString)
729 return false; // Don't search this frame, it will be fruitless.
732 return true;
735 void TextFinder::scopeStringMatchesSoon(int identifier, const WebString& searchText, const WebFindOptions& options, bool reset)
737 m_deferredScopingWork.append(DeferredScopeStringMatches::create(this, identifier, searchText, options, reset));
740 void TextFinder::callScopeStringMatches(DeferredScopeStringMatches* caller, int identifier, const WebString& searchText, const WebFindOptions& options, bool reset)
742 size_t index = m_deferredScopingWork.find(caller);
743 #if !ENABLE(OILPAN)
744 // Finalization needs to be delayed as (m_)searchText is passed by reference.
745 OwnPtr<DeferredScopeStringMatches> item = index != kNotFound ? m_deferredScopingWork[index].release() : nullptr;
746 #endif
747 m_deferredScopingWork.remove(index);
749 scopeStringMatches(identifier, searchText, options, reset);
752 void TextFinder::invalidateIfNecessary()
754 if (m_lastMatchCount <= m_nextInvalidateAfter)
755 return;
757 // FIXME: (http://crbug.com/6819) Optimize the drawing of the tickmarks and
758 // remove this. This calculation sets a milestone for when next to
759 // invalidate the scrollbar and the content area. We do this so that we
760 // don't spend too much time drawing the scrollbar over and over again.
761 // Basically, up until the first 500 matches there is no throttle.
762 // After the first 500 matches, we set set the milestone further and
763 // further out (750, 1125, 1688, 2K, 3K).
764 static const int startSlowingDownAfter = 500;
765 static const int slowdown = 750;
767 int i = m_lastMatchCount / startSlowingDownAfter;
768 m_nextInvalidateAfter += i * slowdown;
769 ownerFrame().frameView()->invalidatePaintForTickmarks();
772 void TextFinder::flushCurrentScoping()
774 flushCurrentScopingEffort(m_findRequestIdentifier);
777 void TextFinder::setMatchMarkerActive(bool active)
779 setMarkerActive(m_activeMatch.get(), active);
782 void TextFinder::decrementFramesScopingCount(int identifier)
784 // This frame has no further scoping left, so it is done. Other frames might,
785 // of course, continue to scope matches.
786 --m_framesScopingCount;
788 // If this is the last frame to finish scoping we need to trigger the final
789 // update to be sent.
790 if (!m_framesScopingCount)
791 ownerFrame().increaseMatchCount(0, identifier);
794 int TextFinder::ordinalOfFirstMatch() const
796 return ordinalOfFirstMatchForFrame(m_ownerFrame.get());
799 DEFINE_TRACE(TextFinder)
801 #if ENABLE(OILPAN)
802 visitor->trace(m_ownerFrame);
803 visitor->trace(m_currentActiveMatchFrame);
804 visitor->trace(m_activeMatch);
805 visitor->trace(m_resumeScopingFromRange);
806 visitor->trace(m_deferredScopingWork);
807 visitor->trace(m_findMatchesCache);
808 #endif
811 } // namespace blink