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
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
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.
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"
62 TextFinder::FindMatch::FindMatch(PassRefPtrWillBeRawPtr
<Range
> range
, int ordinal
)
68 DEFINE_TRACE(TextFinder::FindMatch
)
70 visitor
->trace(m_range
);
73 class TextFinder::DeferredScopeStringMatches
: public NoBaseWillBeGarbageCollectedFinalized
<TextFinder::DeferredScopeStringMatches
> {
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
));
82 visitor
->trace(m_textFinder
);
87 if (m_timer
.isActive())
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
)
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
;
116 bool TextFinder::find(int identifier
, const WebString
& searchText
, const WebFindOptions
& options
, bool wrapWithinFrame
, WebRect
* selectionRect
)
118 if (!ownerFrame().frame() || !ownerFrame().frame()->page())
121 WebLocalFrameImpl
* mainFrameImpl
= ownerFrame().viewImpl()->mainFrameImpl();
123 if (!options
.findNext
)
124 ownerFrame().frame()->page()->unmarkAllTextMatches();
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();
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;
180 if (oldActiveFrame
!= &ownerFrame()) {
182 m_activeMatchIndexInCurrentFrame
= 0;
184 m_activeMatchIndexInCurrentFrame
= m_lastMatchCount
- 1;
187 ++m_activeMatchIndexInCurrentFrame
;
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;
197 *selectionRect
= ownerFrame().frameView()->contentsToRootFrame(m_activeMatch
->boundingBox());
198 reportFindInPageSelection(*selectionRect
, m_activeMatchIndexInCurrentFrame
+ 1, identifier
);
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());
224 AXObject
* startObject
= axObjectCache
->get(m_activeMatch
->startContainer());
225 AXObject
* endObject
= axObjectCache
->get(m_activeMatch
->endContainer());
226 if (!startObject
|| !endObject
)
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
)
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.
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
);
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())
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
300 bool timedOut
= false;
301 double startTime
= currentTime();
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()) {
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();
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
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,
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
362 searchStart
= result
.endPosition();
364 m_resumeScopingFromRange
= Range::create(result
.document(), toPositionInDOMTree(result
.endPosition()), toPositionInDOMTree(result
.endPosition()));
365 timedOut
= (currentTime() - startTime
) >= maxScopingDuration
;
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
);
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.
386 invalidateIfNecessary();
388 // Scoping effort ran out of time, lets ask for another time-slice.
389 scopeStringMatchesSoon(
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())
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()
430 for (DeferredScopeStringMatches
* deferredWork
: m_deferredScopingWork
)
431 deferredWork
->dispose();
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
)
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())
511 // Remove any invalid matches from the cache.
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
);
582 int TextFinder::nearestFindMatch(const FloatPoint
& point
, float& distanceSquared
)
584 updateFindMatchRects();
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
) {
596 distanceSquared
= currentDistanceSquared
;
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())
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
);
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()
679 cancelPendingScopingEffort();
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())
692 ownerFrame().frame()->document()->markers().setMarkersActive(range
, active
);
695 int TextFinder::ordinalOfFirstMatchForFrame(WebLocalFrameImpl
* frame
) const
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
;
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())
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.
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
);
744 // Finalization needs to be delayed as (m_)searchText is passed by reference.
745 OwnPtr
<DeferredScopeStringMatches
> item
= index
!= kNotFound
? m_deferredScopingWork
[index
].release() : nullptr;
747 m_deferredScopingWork
.remove(index
);
749 scopeStringMatches(identifier
, searchText
, options
, reset
);
752 void TextFinder::invalidateIfNecessary()
754 if (m_lastMatchCount
<= m_nextInvalidateAfter
)
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
)
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
);