1 /*
2 * Copyright (C) 2009 Google 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 are
6 * met:
7 *
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.
17 *
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.
29 */
30
31
32 #include "config.h"
33 #include "web/TextFinder.h"
34
35 #include "core/dom/DocumentMarker.h"
36 #include "core/dom/DocumentMarkerController.h"
37 #include "core/dom/Range.h"
38 #include "core/dom/shadow/ShadowRoot.h"
39 #include "core/editing/Editor.h"
40 #include "core/editing/TextIterator.h"
41 #include "core/editing/VisibleSelection.h"
42 #include "core/frame/FrameView.h"
43 #include "platform/Timer.h"
44 #include "public/platform/WebVector.h"
45 #include "public/web/WebFindOptions.h"
46 #include "public/web/WebFrameClient.h"
47 #include "public/web/WebViewClient.h"
48 #include "web/FindInPageCoordinates.h"
49 #include "web/WebLocalFrameImpl.h"
50 #include "web/WebViewImpl.h"
51 #include "wtf/CurrentTime.h"
52
53 using namespace WebCore;
54
55 namespace blink {
56
FindMatch(PassRefPtrWillBeRawPtr<Range> range,int ordinal)57 TextFinder::FindMatch::FindMatch(PassRefPtrWillBeRawPtr<Range> range, int ordinal)
58 : m_range(range)
59 , m_ordinal(ordinal)
60 {
61 }
62
trace(WebCore::Visitor * visitor)63 void TextFinder::FindMatch::trace(WebCore::Visitor* visitor)
64 {
65 visitor->trace(m_range);
66 }
67
68 class TextFinder::DeferredScopeStringMatches {
69 public:
DeferredScopeStringMatches(TextFinder * textFinder,int identifier,const WebString & searchText,const WebFindOptions & options,bool reset)70 DeferredScopeStringMatches(TextFinder* textFinder, int identifier, const WebString& searchText, const WebFindOptions& options, bool reset)
71 : m_timer(this, &DeferredScopeStringMatches::doTimeout)
72 , m_textFinder(textFinder)
73 , m_identifier(identifier)
74 , m_searchText(searchText)
75 , m_options(options)
76 , m_reset(reset)
77 {
78 m_timer.startOneShot(0.0, FROM_HERE);
79 }
80
81 private:
doTimeout(Timer<DeferredScopeStringMatches> *)82 void doTimeout(Timer<DeferredScopeStringMatches>*)
83 {
84 m_textFinder->callScopeStringMatches(this, m_identifier, m_searchText, m_options, m_reset);
85 }
86
87 Timer<DeferredScopeStringMatches> m_timer;
88 TextFinder* m_textFinder;
89 const int m_identifier;
90 const WebString m_searchText;
91 const WebFindOptions m_options;
92 const bool m_reset;
93 };
94
find(int identifier,const WebString & searchText,const WebFindOptions & options,bool wrapWithinFrame,WebRect * selectionRect)95 bool TextFinder::find(int identifier, const WebString& searchText, const WebFindOptions& options, bool wrapWithinFrame, WebRect* selectionRect)
96 {
97 if (!m_ownerFrame.frame() || !m_ownerFrame.frame()->page())
98 return false;
99
100 WebLocalFrameImpl* mainFrameImpl = m_ownerFrame.viewImpl()->mainFrameImpl();
101
102 if (!options.findNext)
103 m_ownerFrame.frame()->page()->unmarkAllTextMatches();
104 else
105 setMarkerActive(m_activeMatch.get(), false);
106
107 if (m_activeMatch && &m_activeMatch->ownerDocument() != m_ownerFrame.frame()->document())
108 m_activeMatch = nullptr;
109
110 // If the user has selected something since the last Find operation we want
111 // to start from there. Otherwise, we start searching from where the last Find
112 // operation left off (either a Find or a FindNext operation).
113 VisibleSelection selection(m_ownerFrame.frame()->selection().selection());
114 bool activeSelection = !selection.isNone();
115 if (activeSelection) {
116 m_activeMatch = selection.firstRange().get();
117 m_ownerFrame.frame()->selection().clear();
118 }
119
120 ASSERT(m_ownerFrame.frame() && m_ownerFrame.frame()->view());
121 const FindOptions findOptions = (options.forward ? 0 : Backwards)
122 | (options.matchCase ? 0 : CaseInsensitive)
123 | (wrapWithinFrame ? WrapAround : 0)
124 | (options.wordStart ? AtWordStarts : 0)
125 | (options.medialCapitalAsWordStart ? TreatMedialCapitalAsWordStart : 0)
126 | (options.findNext ? 0 : StartInSelection);
127 m_activeMatch = m_ownerFrame.frame()->editor().findStringAndScrollToVisible(searchText, m_activeMatch.get(), findOptions);
128
129 if (!m_activeMatch) {
130 // If we're finding next the next active match might not be in the current frame.
131 // In this case we don't want to clear the matches cache.
132 if (!options.findNext)
133 clearFindMatchesCache();
134
135 m_ownerFrame.invalidateAll();
136 return false;
137 }
138
139 #if OS(ANDROID)
140 m_ownerFrame.viewImpl()->zoomToFindInPageRect(m_ownerFrame.frameView()->contentsToWindow(enclosingIntRect(RenderObject::absoluteBoundingBoxRectForRange(m_activeMatch.get()))));
141 #endif
142
143 setMarkerActive(m_activeMatch.get(), true);
144 WebLocalFrameImpl* oldActiveFrame = mainFrameImpl->ensureTextFinder().m_currentActiveMatchFrame;
145 mainFrameImpl->ensureTextFinder().m_currentActiveMatchFrame = &m_ownerFrame;
146
147 // Make sure no node is focused. See http://crbug.com/38700.
148 m_ownerFrame.frame()->document()->setFocusedElement(nullptr);
149
150 if (!options.findNext || activeSelection) {
151 // This is either a Find operation or a Find-next from a new start point
152 // due to a selection, so we set the flag to ask the scoping effort
153 // to find the active rect for us and report it back to the UI.
154 m_locatingActiveRect = true;
155 } else {
156 if (oldActiveFrame != &m_ownerFrame) {
157 if (options.forward)
158 m_activeMatchIndexInCurrentFrame = 0;
159 else
160 m_activeMatchIndexInCurrentFrame = m_lastMatchCount - 1;
161 } else {
162 if (options.forward)
163 ++m_activeMatchIndexInCurrentFrame;
164 else
165 --m_activeMatchIndexInCurrentFrame;
166
167 if (m_activeMatchIndexInCurrentFrame + 1 > m_lastMatchCount)
168 m_activeMatchIndexInCurrentFrame = 0;
169 if (m_activeMatchIndexInCurrentFrame == -1)
170 m_activeMatchIndexInCurrentFrame = m_lastMatchCount - 1;
171 }
172 if (selectionRect) {
173 *selectionRect = m_ownerFrame.frameView()->contentsToWindow(m_activeMatch->boundingBox());
174 reportFindInPageSelection(*selectionRect, m_activeMatchIndexInCurrentFrame + 1, identifier);
175 }
176 }
177
178 return true;
179 }
180
stopFindingAndClearSelection()181 void TextFinder::stopFindingAndClearSelection()
182 {
183 cancelPendingScopingEffort();
184
185 // Remove all markers for matches found and turn off the highlighting.
186 m_ownerFrame.frame()->document()->markers().removeMarkers(DocumentMarker::TextMatch);
187 m_ownerFrame.frame()->editor().setMarkedTextMatchesAreHighlighted(false);
188 clearFindMatchesCache();
189
190 // Let the frame know that we don't want tickmarks or highlighting anymore.
191 m_ownerFrame.invalidateAll();
192 }
193
scopeStringMatches(int identifier,const WebString & searchText,const WebFindOptions & options,bool reset)194 void TextFinder::scopeStringMatches(int identifier, const WebString& searchText, const WebFindOptions& options, bool reset)
195 {
196 if (reset) {
197 // This is a brand new search, so we need to reset everything.
198 // Scoping is just about to begin.
199 m_scopingInProgress = true;
200
201 // Need to keep the current identifier locally in order to finish the
202 // request in case the frame is detached during the process.
203 m_findRequestIdentifier = identifier;
204
205 // Clear highlighting for this frame.
206 LocalFrame* frame = m_ownerFrame.frame();
207 if (frame && frame->page() && frame->editor().markedTextMatchesAreHighlighted())
208 frame->page()->unmarkAllTextMatches();
209
210 // Clear the tickmarks and results cache.
211 clearFindMatchesCache();
212
213 // Clear the counters from last operation.
214 m_lastMatchCount = 0;
215 m_nextInvalidateAfter = 0;
216 m_resumeScopingFromRange = nullptr;
217
218 // The view might be null on detached frames.
219 if (frame && frame->page())
220 m_ownerFrame.viewImpl()->mainFrameImpl()->ensureTextFinder().m_framesScopingCount++;
221
222 // Now, defer scoping until later to allow find operation to finish quickly.
223 scopeStringMatchesSoon(identifier, searchText, options, false); // false means just reset, so don't do it again.
224 return;
225 }
226
227 if (!shouldScopeMatches(searchText)) {
228 // Note that we want to defer the final update when resetting even if shouldScopeMatches returns false.
229 // This is done in order to prevent sending a final message based only on the results of the first frame
230 // since m_framesScopingCount would be 0 as other frames have yet to reset.
231 finishCurrentScopingEffort(identifier);
232 return;
233 }
234
235 WebLocalFrameImpl* mainFrameImpl = m_ownerFrame.viewImpl()->mainFrameImpl();
236 Position searchStart = firstPositionInNode(m_ownerFrame.frame()->document());
237 Position searchEnd = lastPositionInNode(m_ownerFrame.frame()->document());
238 ASSERT(searchStart.document() == searchEnd.document());
239
240 if (m_resumeScopingFromRange) {
241 // This is a continuation of a scoping operation that timed out and didn't
242 // complete last time around, so we should start from where we left off.
243 ASSERT(m_resumeScopingFromRange->collapsed());
244 searchStart = m_resumeScopingFromRange->startPosition().next();
245 if (searchStart.document() != searchEnd.document())
246 return;
247 }
248
249 // This timeout controls how long we scope before releasing control. This
250 // value does not prevent us from running for longer than this, but it is
251 // periodically checked to see if we have exceeded our allocated time.
252 const double maxScopingDuration = 0.1; // seconds
253
254 int matchCount = 0;
255 bool timedOut = false;
256 double startTime = currentTime();
257 do {
258 // Find next occurrence of the search string.
259 // FIXME: (http://crbug.com/6818) This WebKit operation may run for longer
260 // than the timeout value, and is not interruptible as it is currently
261 // written. We may need to rewrite it with interruptibility in mind, or
262 // find an alternative.
263 Position resultStart;
264 Position resultEnd;
265 findPlainText(searchStart, searchEnd, searchText, options.matchCase ? 0 : CaseInsensitive, resultStart, resultEnd);
266 if (resultStart == resultEnd) {
267 // Not found.
268 break;
269 }
270
271 RefPtrWillBeRawPtr<Range> resultRange = Range::create(*resultStart.document(), resultStart, resultEnd);
272 if (resultRange->collapsed()) {
273 // resultRange will be collapsed if the matched text spans over multiple TreeScopes.
274 // FIXME: Show such matches to users.
275 searchStart = resultStart.next();
276 continue;
277 }
278
279 ++matchCount;
280
281 // Catch a special case where Find found something but doesn't know what
282 // the bounding box for it is. In this case we set the first match we find
283 // as the active rect.
284 IntRect resultBounds = resultRange->boundingBox();
285 IntRect activeSelectionRect;
286 if (m_locatingActiveRect) {
287 activeSelectionRect = m_activeMatch.get() ?
288 m_activeMatch->boundingBox() : resultBounds;
289 }
290
291 // If the Find function found a match it will have stored where the
292 // match was found in m_activeSelectionRect on the current frame. If we
293 // find this rect during scoping it means we have found the active
294 // tickmark.
295 bool foundActiveMatch = false;
296 if (m_locatingActiveRect && (activeSelectionRect == resultBounds)) {
297 // We have found the active tickmark frame.
298 mainFrameImpl->ensureTextFinder().m_currentActiveMatchFrame = &m_ownerFrame;
299 foundActiveMatch = true;
300 // We also know which tickmark is active now.
301 m_activeMatchIndexInCurrentFrame = matchCount - 1;
302 // To stop looking for the active tickmark, we set this flag.
303 m_locatingActiveRect = false;
304
305 // Notify browser of new location for the selected rectangle.
306 reportFindInPageSelection(
307 m_ownerFrame.frameView()->contentsToWindow(resultBounds),
308 m_activeMatchIndexInCurrentFrame + 1,
309 identifier);
310 }
311
312 addMarker(resultRange.get(), foundActiveMatch);
313
314 m_findMatchesCache.append(FindMatch(resultRange.get(), m_lastMatchCount + matchCount));
315
316 // Set the new start for the search range to be the end of the previous
317 // result range. There is no need to use a VisiblePosition here,
318 // since findPlainText will use a TextIterator to go over the visible
319 // text nodes.
320 searchStart = resultStart.next();
321
322 m_resumeScopingFromRange = Range::create(*resultStart.document(), resultStart, resultStart);
323 timedOut = (currentTime() - startTime) >= maxScopingDuration;
324 } while (!timedOut);
325
326 // Remember what we search for last time, so we can skip searching if more
327 // letters are added to the search string (and last outcome was 0).
328 m_lastSearchString = searchText;
329
330 if (matchCount > 0) {
331 m_ownerFrame.frame()->editor().setMarkedTextMatchesAreHighlighted(true);
332
333 m_lastMatchCount += matchCount;
334
335 // Let the mainframe know how much we found during this pass.
336 mainFrameImpl->increaseMatchCount(matchCount, identifier);
337 }
338
339 if (timedOut) {
340 // If we found anything during this pass, we should redraw. However, we
341 // don't want to spam too much if the page is extremely long, so if we
342 // reach a certain point we start throttling the redraw requests.
343 if (matchCount > 0)
344 invalidateIfNecessary();
345
346 // Scoping effort ran out of time, lets ask for another time-slice.
347 scopeStringMatchesSoon(
348 identifier,
349 searchText,
350 options,
351 false); // don't reset.
352 return; // Done for now, resume work later.
353 }
354
355 finishCurrentScopingEffort(identifier);
356 }
357
flushCurrentScopingEffort(int identifier)358 void TextFinder::flushCurrentScopingEffort(int identifier)
359 {
360 if (!m_ownerFrame.frame() || !m_ownerFrame.frame()->page())
361 return;
362
363 WebLocalFrameImpl* mainFrameImpl = m_ownerFrame.viewImpl()->mainFrameImpl();
364 mainFrameImpl->ensureTextFinder().decrementFramesScopingCount(identifier);
365 }
366
finishCurrentScopingEffort(int identifier)367 void TextFinder::finishCurrentScopingEffort(int identifier)
368 {
369 flushCurrentScopingEffort(identifier);
370
371 m_scopingInProgress = false;
372 m_lastFindRequestCompletedWithNoMatches = !m_lastMatchCount;
373
374 // This frame is done, so show any scrollbar tickmarks we haven't drawn yet.
375 m_ownerFrame.invalidateScrollbar();
376 }
377
cancelPendingScopingEffort()378 void TextFinder::cancelPendingScopingEffort()
379 {
380 deleteAllValues(m_deferredScopingWork);
381 m_deferredScopingWork.clear();
382
383 m_activeMatchIndexInCurrentFrame = -1;
384
385 // Last request didn't complete.
386 if (m_scopingInProgress)
387 m_lastFindRequestCompletedWithNoMatches = false;
388
389 m_scopingInProgress = false;
390 }
391
increaseMatchCount(int identifier,int count)392 void TextFinder::increaseMatchCount(int identifier, int count)
393 {
394 if (count)
395 ++m_findMatchMarkersVersion;
396
397 m_totalMatchCount += count;
398
399 // Update the UI with the latest findings.
400 if (m_ownerFrame.client())
401 m_ownerFrame.client()->reportFindInPageMatchCount(identifier, m_totalMatchCount, !m_framesScopingCount);
402 }
403
reportFindInPageSelection(const WebRect & selectionRect,int activeMatchOrdinal,int identifier)404 void TextFinder::reportFindInPageSelection(const WebRect& selectionRect, int activeMatchOrdinal, int identifier)
405 {
406 // Update the UI with the latest selection rect.
407 if (m_ownerFrame.client())
408 m_ownerFrame.client()->reportFindInPageSelection(identifier, ordinalOfFirstMatch() + activeMatchOrdinal, selectionRect);
409 }
410
resetMatchCount()411 void TextFinder::resetMatchCount()
412 {
413 if (m_totalMatchCount > 0)
414 ++m_findMatchMarkersVersion;
415
416 m_totalMatchCount = 0;
417 m_framesScopingCount = 0;
418 }
419
clearFindMatchesCache()420 void TextFinder::clearFindMatchesCache()
421 {
422 if (!m_findMatchesCache.isEmpty())
423 m_ownerFrame.viewImpl()->mainFrameImpl()->ensureTextFinder().m_findMatchMarkersVersion++;
424
425 m_findMatchesCache.clear();
426 m_findMatchRectsAreValid = false;
427 }
428
isActiveMatchFrameValid() const429 bool TextFinder::isActiveMatchFrameValid() const
430 {
431 WebLocalFrameImpl* mainFrameImpl = m_ownerFrame.viewImpl()->mainFrameImpl();
432 WebLocalFrameImpl* activeMatchFrame = mainFrameImpl->activeMatchFrame();
433 return activeMatchFrame && activeMatchFrame->activeMatch() && activeMatchFrame->frame()->tree().isDescendantOf(mainFrameImpl->frame());
434 }
435
updateFindMatchRects()436 void TextFinder::updateFindMatchRects()
437 {
438 IntSize currentContentsSize = m_ownerFrame.contentsSize();
439 if (m_contentsSizeForCurrentFindMatchRects != currentContentsSize) {
440 m_contentsSizeForCurrentFindMatchRects = currentContentsSize;
441 m_findMatchRectsAreValid = false;
442 }
443
444 size_t deadMatches = 0;
445 for (Vector<FindMatch>::iterator it = m_findMatchesCache.begin(); it != m_findMatchesCache.end(); ++it) {
446 if (!it->m_range->boundaryPointsValid() || !it->m_range->startContainer()->inDocument())
447 it->m_rect = FloatRect();
448 else if (!m_findMatchRectsAreValid)
449 it->m_rect = findInPageRectFromRange(it->m_range.get());
450
451 if (it->m_rect.isEmpty())
452 ++deadMatches;
453 }
454
455 // Remove any invalid matches from the cache.
456 if (deadMatches) {
457 WillBeHeapVector<FindMatch> filteredMatches;
458 filteredMatches.reserveCapacity(m_findMatchesCache.size() - deadMatches);
459
460 for (Vector<FindMatch>::const_iterator it = m_findMatchesCache.begin(); it != m_findMatchesCache.end(); ++it) {
461 if (!it->m_rect.isEmpty())
462 filteredMatches.append(*it);
463 }
464
465 m_findMatchesCache.swap(filteredMatches);
466 }
467
468 // Invalidate the rects in child frames. Will be updated later during traversal.
469 if (!m_findMatchRectsAreValid)
470 for (WebFrame* child = m_ownerFrame.firstChild(); child; child = child->nextSibling())
471 toWebLocalFrameImpl(child)->ensureTextFinder().m_findMatchRectsAreValid = false;
472
473 m_findMatchRectsAreValid = true;
474 }
475
activeFindMatchRect()476 WebFloatRect TextFinder::activeFindMatchRect()
477 {
478 if (!isActiveMatchFrameValid())
479 return WebFloatRect();
480
481 return WebFloatRect(findInPageRectFromRange(m_currentActiveMatchFrame->activeMatch()));
482 }
483
findMatchRects(WebVector<WebFloatRect> & outputRects)484 void TextFinder::findMatchRects(WebVector<WebFloatRect>& outputRects)
485 {
486 Vector<WebFloatRect> matchRects;
487 for (WebLocalFrameImpl* frame = &m_ownerFrame; frame; frame = toWebLocalFrameImpl(frame->traverseNext(false)))
488 frame->ensureTextFinder().appendFindMatchRects(matchRects);
489
490 outputRects = matchRects;
491 }
492
appendFindMatchRects(Vector<WebFloatRect> & frameRects)493 void TextFinder::appendFindMatchRects(Vector<WebFloatRect>& frameRects)
494 {
495 updateFindMatchRects();
496 frameRects.reserveCapacity(frameRects.size() + m_findMatchesCache.size());
497 for (Vector<FindMatch>::const_iterator it = m_findMatchesCache.begin(); it != m_findMatchesCache.end(); ++it) {
498 ASSERT(!it->m_rect.isEmpty());
499 frameRects.append(it->m_rect);
500 }
501 }
502
selectNearestFindMatch(const WebFloatPoint & point,WebRect * selectionRect)503 int TextFinder::selectNearestFindMatch(const WebFloatPoint& point, WebRect* selectionRect)
504 {
505 TextFinder* bestFinder = 0;
506 int indexInBestFrame = -1;
507 float distanceInBestFrame = FLT_MAX;
508
509 for (WebLocalFrameImpl* frame = &m_ownerFrame; frame; frame = toWebLocalFrameImpl(frame->traverseNext(false))) {
510 float distanceInFrame;
511 TextFinder& finder = frame->ensureTextFinder();
512 int indexInFrame = finder.nearestFindMatch(point, distanceInFrame);
513 if (distanceInFrame < distanceInBestFrame) {
514 bestFinder = &finder;
515 indexInBestFrame = indexInFrame;
516 distanceInBestFrame = distanceInFrame;
517 }
518 }
519
520 if (indexInBestFrame != -1)
521 return bestFinder->selectFindMatch(static_cast<unsigned>(indexInBestFrame), selectionRect);
522
523 return -1;
524 }
525
nearestFindMatch(const FloatPoint & point,float & distanceSquared)526 int TextFinder::nearestFindMatch(const FloatPoint& point, float& distanceSquared)
527 {
528 updateFindMatchRects();
529
530 int nearest = -1;
531 distanceSquared = FLT_MAX;
532 for (size_t i = 0; i < m_findMatchesCache.size(); ++i) {
533 ASSERT(!m_findMatchesCache[i].m_rect.isEmpty());
534 FloatSize offset = point - m_findMatchesCache[i].m_rect.center();
535 float width = offset.width();
536 float height = offset.height();
537 float currentDistanceSquared = width * width + height * height;
538 if (currentDistanceSquared < distanceSquared) {
539 nearest = i;
540 distanceSquared = currentDistanceSquared;
541 }
542 }
543 return nearest;
544 }
545
selectFindMatch(unsigned index,WebRect * selectionRect)546 int TextFinder::selectFindMatch(unsigned index, WebRect* selectionRect)
547 {
548 ASSERT_WITH_SECURITY_IMPLICATION(index < m_findMatchesCache.size());
549
550 RefPtrWillBeRawPtr<Range> range = m_findMatchesCache[index].m_range;
551 if (!range->boundaryPointsValid() || !range->startContainer()->inDocument())
552 return -1;
553
554 // Check if the match is already selected.
555 TextFinder& mainFrameTextFinder = m_ownerFrame.viewImpl()->mainFrameImpl()->ensureTextFinder();
556 WebLocalFrameImpl* activeMatchFrame = mainFrameTextFinder.m_currentActiveMatchFrame;
557 if (&m_ownerFrame != activeMatchFrame || !m_activeMatch || !areRangesEqual(m_activeMatch.get(), range.get())) {
558 if (isActiveMatchFrameValid())
559 activeMatchFrame->ensureTextFinder().setMatchMarkerActive(false);
560
561 m_activeMatchIndexInCurrentFrame = m_findMatchesCache[index].m_ordinal - 1;
562
563 // Set this frame as the active frame (the one with the active highlight).
564 mainFrameTextFinder.m_currentActiveMatchFrame = &m_ownerFrame;
565 m_ownerFrame.viewImpl()->setFocusedFrame(&m_ownerFrame);
566
567 m_activeMatch = range.release();
568 setMarkerActive(m_activeMatch.get(), true);
569
570 // Clear any user selection, to make sure Find Next continues on from the match we just activated.
571 m_ownerFrame.frame()->selection().clear();
572
573 // Make sure no node is focused. See http://crbug.com/38700.
574 m_ownerFrame.frame()->document()->setFocusedElement(nullptr);
575 }
576
577 IntRect activeMatchRect;
578 IntRect activeMatchBoundingBox = enclosingIntRect(RenderObject::absoluteBoundingBoxRectForRange(m_activeMatch.get()));
579
580 if (!activeMatchBoundingBox.isEmpty()) {
581 if (m_activeMatch->firstNode() && m_activeMatch->firstNode()->renderer()) {
582 m_activeMatch->firstNode()->renderer()->scrollRectToVisible(
583 activeMatchBoundingBox, ScrollAlignment::alignCenterIfNeeded, ScrollAlignment::alignCenterIfNeeded);
584 }
585
586 // Zoom to the active match.
587 activeMatchRect = m_ownerFrame.frameView()->contentsToWindow(activeMatchBoundingBox);
588 m_ownerFrame.viewImpl()->zoomToFindInPageRect(activeMatchRect);
589 }
590
591 if (selectionRect)
592 *selectionRect = activeMatchRect;
593
594 return ordinalOfFirstMatch() + m_activeMatchIndexInCurrentFrame + 1;
595 }
596
create(WebLocalFrameImpl & ownerFrame)597 PassOwnPtr<TextFinder> TextFinder::create(WebLocalFrameImpl& ownerFrame)
598 {
599 return adoptPtr(new TextFinder(ownerFrame));
600 }
601
TextFinder(WebLocalFrameImpl & ownerFrame)602 TextFinder::TextFinder(WebLocalFrameImpl& ownerFrame)
603 : m_ownerFrame(ownerFrame)
604 , m_currentActiveMatchFrame(0)
605 , m_activeMatchIndexInCurrentFrame(-1)
606 , m_resumeScopingFromRange(nullptr)
607 , m_lastMatchCount(-1)
608 , m_totalMatchCount(-1)
609 , m_framesScopingCount(-1)
610 , m_findRequestIdentifier(-1)
611 , m_nextInvalidateAfter(0)
612 , m_findMatchMarkersVersion(0)
613 , m_locatingActiveRect(false)
614 , m_scopingInProgress(false)
615 , m_lastFindRequestCompletedWithNoMatches(false)
616 , m_findMatchRectsAreValid(false)
617 {
618 }
619
~TextFinder()620 TextFinder::~TextFinder()
621 {
622 cancelPendingScopingEffort();
623 }
624
addMarker(Range * range,bool activeMatch)625 void TextFinder::addMarker(Range* range, bool activeMatch)
626 {
627 m_ownerFrame.frame()->document()->markers().addTextMatchMarker(range, activeMatch);
628 }
629
setMarkerActive(Range * range,bool active)630 void TextFinder::setMarkerActive(Range* range, bool active)
631 {
632 if (!range || range->collapsed())
633 return;
634 m_ownerFrame.frame()->document()->markers().setMarkersActive(range, active);
635 }
636
ordinalOfFirstMatchForFrame(WebLocalFrameImpl * frame) const637 int TextFinder::ordinalOfFirstMatchForFrame(WebLocalFrameImpl* frame) const
638 {
639 int ordinal = 0;
640 WebLocalFrameImpl* mainFrameImpl = m_ownerFrame.viewImpl()->mainFrameImpl();
641 // Iterate from the main frame up to (but not including) |frame| and
642 // add up the number of matches found so far.
643 for (WebLocalFrameImpl* it = mainFrameImpl; it != frame; it = toWebLocalFrameImpl(it->traverseNext(true))) {
644 TextFinder& finder = it->ensureTextFinder();
645 if (finder.m_lastMatchCount > 0)
646 ordinal += finder.m_lastMatchCount;
647 }
648 return ordinal;
649 }
650
shouldScopeMatches(const String & searchText)651 bool TextFinder::shouldScopeMatches(const String& searchText)
652 {
653 // Don't scope if we can't find a frame or a view.
654 // The user may have closed the tab/application, so abort.
655 // Also ignore detached frames, as many find operations report to the main frame.
656 LocalFrame* frame = m_ownerFrame.frame();
657 if (!frame || !frame->view() || !frame->page() || !m_ownerFrame.hasVisibleContent())
658 return false;
659
660 ASSERT(frame->document() && frame->view());
661
662 // If the frame completed the scoping operation and found 0 matches the last
663 // time it was searched, then we don't have to search it again if the user is
664 // just adding to the search string or sending the same search string again.
665 if (m_lastFindRequestCompletedWithNoMatches && !m_lastSearchString.isEmpty()) {
666 // Check to see if the search string prefixes match.
667 String previousSearchPrefix =
668 searchText.substring(0, m_lastSearchString.length());
669
670 if (previousSearchPrefix == m_lastSearchString)
671 return false; // Don't search this frame, it will be fruitless.
672 }
673
674 return true;
675 }
676
scopeStringMatchesSoon(int identifier,const WebString & searchText,const WebFindOptions & options,bool reset)677 void TextFinder::scopeStringMatchesSoon(int identifier, const WebString& searchText, const WebFindOptions& options, bool reset)
678 {
679 m_deferredScopingWork.append(new DeferredScopeStringMatches(this, identifier, searchText, options, reset));
680 }
681
callScopeStringMatches(DeferredScopeStringMatches * caller,int identifier,const WebString & searchText,const WebFindOptions & options,bool reset)682 void TextFinder::callScopeStringMatches(DeferredScopeStringMatches* caller, int identifier, const WebString& searchText, const WebFindOptions& options, bool reset)
683 {
684 m_deferredScopingWork.remove(m_deferredScopingWork.find(caller));
685 scopeStringMatches(identifier, searchText, options, reset);
686
687 // This needs to happen last since searchText is passed by reference.
688 delete caller;
689 }
690
invalidateIfNecessary()691 void TextFinder::invalidateIfNecessary()
692 {
693 if (m_lastMatchCount <= m_nextInvalidateAfter)
694 return;
695
696 // FIXME: (http://b/1088165) Optimize the drawing of the tickmarks and
697 // remove this. This calculation sets a milestone for when next to
698 // invalidate the scrollbar and the content area. We do this so that we
699 // don't spend too much time drawing the scrollbar over and over again.
700 // Basically, up until the first 500 matches there is no throttle.
701 // After the first 500 matches, we set set the milestone further and
702 // further out (750, 1125, 1688, 2K, 3K).
703 static const int startSlowingDownAfter = 500;
704 static const int slowdown = 750;
705
706 int i = m_lastMatchCount / startSlowingDownAfter;
707 m_nextInvalidateAfter += i * slowdown;
708 m_ownerFrame.invalidateScrollbar();
709 }
710
flushCurrentScoping()711 void TextFinder::flushCurrentScoping()
712 {
713 flushCurrentScopingEffort(m_findRequestIdentifier);
714 }
715
setMatchMarkerActive(bool active)716 void TextFinder::setMatchMarkerActive(bool active)
717 {
718 setMarkerActive(m_activeMatch.get(), active);
719 }
720
decrementFramesScopingCount(int identifier)721 void TextFinder::decrementFramesScopingCount(int identifier)
722 {
723 // This frame has no further scoping left, so it is done. Other frames might,
724 // of course, continue to scope matches.
725 --m_framesScopingCount;
726
727 // If this is the last frame to finish scoping we need to trigger the final
728 // update to be sent.
729 if (!m_framesScopingCount)
730 m_ownerFrame.increaseMatchCount(0, identifier);
731 }
732
ordinalOfFirstMatch() const733 int TextFinder::ordinalOfFirstMatch() const
734 {
735 return ordinalOfFirstMatchForFrame(&m_ownerFrame);
736 }
737
738 } // namespace blink
739