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