| /* |
| * Copyright (C) 2009 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "third_party/blink/renderer/core/editing/finder/text_finder.h" |
| |
| #include "third_party/blink/public/platform/task_type.h" |
| #include "third_party/blink/public/platform/web_float_rect.h" |
| #include "third_party/blink/public/platform/web_scroll_into_view_params.h" |
| #include "third_party/blink/public/platform/web_vector.h" |
| #include "third_party/blink/public/web/web_find_options.h" |
| #include "third_party/blink/public/web/web_local_frame_client.h" |
| #include "third_party/blink/public/web/web_view_client.h" |
| #include "third_party/blink/renderer/core/accessibility/ax_object_cache_base.h" |
| #include "third_party/blink/renderer/core/dom/idle_request_options.h" |
| #include "third_party/blink/renderer/core/dom/range.h" |
| #include "third_party/blink/renderer/core/dom/scripted_idle_task_controller.h" |
| #include "third_party/blink/renderer/core/dom/shadow_root.h" |
| #include "third_party/blink/renderer/core/editing/editor.h" |
| #include "third_party/blink/renderer/core/editing/ephemeral_range.h" |
| #include "third_party/blink/renderer/core/editing/finder/find_in_page_coordinates.h" |
| #include "third_party/blink/renderer/core/editing/finder/find_options.h" |
| #include "third_party/blink/renderer/core/editing/frame_selection.h" |
| #include "third_party/blink/renderer/core/editing/iterators/search_buffer.h" |
| #include "third_party/blink/renderer/core/editing/markers/document_marker.h" |
| #include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h" |
| #include "third_party/blink/renderer/core/editing/selection_template.h" |
| #include "third_party/blink/renderer/core/editing/visible_selection.h" |
| #include "third_party/blink/renderer/core/exported/web_view_impl.h" |
| #include "third_party/blink/renderer/core/frame/find_in_page.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_view.h" |
| #include "third_party/blink/renderer/core/frame/settings.h" |
| #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" |
| #include "third_party/blink/renderer/core/layout/layout_object.h" |
| #include "third_party/blink/renderer/core/layout/layout_view.h" |
| #include "third_party/blink/renderer/core/layout/text_autosizer.h" |
| #include "third_party/blink/renderer/core/page/page.h" |
| #include "third_party/blink/renderer/platform/timer.h" |
| #include "third_party/blink/renderer/platform/wtf/time.h" |
| |
| namespace blink { |
| |
| namespace { |
| constexpr TimeDelta kForcedInvocationDeadline = TimeDelta::FromSeconds(10); |
| } |
| |
| TextFinder::FindMatch::FindMatch(Range* range, int ordinal) |
| : range_(range), ordinal_(ordinal) {} |
| |
| void TextFinder::FindMatch::Trace(blink::Visitor* visitor) { |
| visitor->Trace(range_); |
| } |
| |
| class TextFinder::IdleScopeStringMatchesCallback |
| : public ScriptedIdleTaskController::IdleTask { |
| public: |
| static IdleScopeStringMatchesCallback* Create(TextFinder* text_finder, |
| int identifier, |
| const WebString& search_text, |
| const WebFindOptions& options) { |
| return new IdleScopeStringMatchesCallback(text_finder, identifier, |
| search_text, options); |
| } |
| |
| void Dispose() { |
| DCHECK_GT(callback_handle_, 0); |
| if (!text_finder_->GetFrame()) |
| return; |
| Document* document = text_finder_->GetFrame()->GetDocument(); |
| if (!document) |
| return; |
| document->CancelIdleCallback(callback_handle_); |
| } |
| |
| void Trace(blink::Visitor* visitor) override { |
| visitor->Trace(text_finder_); |
| ScriptedIdleTaskController::IdleTask::Trace(visitor); |
| } |
| |
| private: |
| IdleScopeStringMatchesCallback(TextFinder* text_finder, |
| int identifier, |
| const WebString& search_text, |
| const WebFindOptions& options) |
| : text_finder_(text_finder), |
| identifier_(identifier), |
| search_text_(search_text), |
| options_(options) { |
| callback_handle_ = |
| text_finder->GetFrame()->GetDocument()->RequestIdleCallback( |
| this, IdleRequestOptions()); |
| } |
| |
| void invoke(IdleDeadline* deadline) override { |
| text_finder_->ResumeScopingStringMatches(deadline, identifier_, |
| search_text_, options_); |
| } |
| |
| Member<TextFinder> text_finder_; |
| int callback_handle_ = 0; |
| const int identifier_; |
| const WebString search_text_; |
| const WebFindOptions options_; |
| }; |
| |
| static void ScrollToVisible(Range* match) { |
| const Node& first_node = *match->FirstNode(); |
| Settings* settings = first_node.GetDocument().GetSettings(); |
| bool smooth_find_enabled = |
| settings ? settings->GetSmoothScrollForFindEnabled() : false; |
| ScrollBehavior scroll_behavior = |
| smooth_find_enabled ? kScrollBehaviorSmooth : kScrollBehaviorAuto; |
| first_node.GetLayoutObject()->ScrollRectToVisible( |
| LayoutRect(match->BoundingBox()), |
| WebScrollIntoViewParams( |
| ScrollAlignment::kAlignCenterIfNeeded, |
| ScrollAlignment::kAlignCenterIfNeeded, kUserScroll, |
| true /* make_visible_in_visual_viewport */, scroll_behavior, |
| true /* is_for_scroll_sequence */)); |
| first_node.GetDocument().SetSequentialFocusNavigationStartingPoint( |
| const_cast<Node*>(&first_node)); |
| } |
| |
| bool TextFinder::Find(int identifier, |
| const WebString& search_text, |
| const WebFindOptions& options, |
| bool wrap_within_frame, |
| bool* active_now) { |
| if (!options.find_next) |
| UnmarkAllTextMatches(); |
| else |
| SetMarkerActive(active_match_.Get(), false); |
| |
| if (active_match_ && |
| &active_match_->OwnerDocument() != OwnerFrame().GetFrame()->GetDocument()) |
| active_match_ = nullptr; |
| |
| // If the user has selected something since the last Find operation we want |
| // to start from there. Otherwise, we start searching from where the last Find |
| // operation left off (either a Find or a FindNext operation). |
| // TODO(editing-dev): The use of VisibleSelection should be audited. See |
| // crbug.com/657237 for details. |
| VisibleSelection selection( |
| OwnerFrame().GetFrame()->Selection().ComputeVisibleSelectionInDOMTree()); |
| bool active_selection = !selection.IsNone(); |
| if (active_selection) { |
| active_match_ = CreateRange(FirstEphemeralRangeOf(selection)); |
| OwnerFrame().GetFrame()->Selection().Clear(); |
| } |
| |
| DCHECK(OwnerFrame().GetFrame()); |
| DCHECK(OwnerFrame().GetFrame()->View()); |
| const FindOptions find_options = |
| (options.forward ? 0 : kBackwards) | |
| (options.match_case ? 0 : kCaseInsensitive) | |
| (wrap_within_frame ? kWrapAround : 0) | |
| (options.find_next ? 0 : kStartInSelection); |
| active_match_ = Editor::FindRangeOfString( |
| *OwnerFrame().GetFrame()->GetDocument(), search_text, |
| EphemeralRangeInFlatTree(active_match_.Get()), find_options); |
| |
| if (!active_match_) { |
| // If we're finding next the next active match might not be in the current |
| // frame. In this case we don't want to clear the matches cache. |
| if (!options.find_next) |
| ClearFindMatchesCache(); |
| |
| InvalidatePaintForTickmarks(); |
| return false; |
| } |
| ScrollToVisible(active_match_); |
| |
| // If the user is browsing a page with autosizing, adjust the zoom to the |
| // column where the next hit has been found. Doing this when autosizing is |
| // not set will result in a zoom reset on small devices. |
| if (OwnerFrame() |
| .GetFrame() |
| ->GetDocument() |
| ->GetTextAutosizer() |
| ->PageNeedsAutosizing()) { |
| OwnerFrame().ViewImpl()->ZoomToFindInPageRect( |
| OwnerFrame().GetFrameView()->ConvertToRootFrame( |
| EnclosingIntRect(LayoutObject::AbsoluteBoundingBoxRectForRange( |
| EphemeralRange(active_match_.Get()))))); |
| } |
| |
| bool was_active_frame = current_active_match_frame_; |
| current_active_match_frame_ = true; |
| |
| bool is_active = SetMarkerActive(active_match_.Get(), true); |
| if (active_now) |
| *active_now = is_active; |
| |
| // Make sure no node is focused. See http://crbug.com/38700. |
| OwnerFrame().GetFrame()->GetDocument()->ClearFocusedElement(); |
| |
| // Set this frame as focused. |
| OwnerFrame().ViewImpl()->SetFocusedFrame(&OwnerFrame()); |
| |
| if (!options.find_next || active_selection || !is_active) { |
| // This is either an initial Find operation, a Find-next from a new |
| // start point due to a selection, or new matches were found during |
| // Find-next due to DOM alteration (that couldn't be set as active), so |
| // we set the flag to ask the scoping effort to find the active rect for |
| // us and report it back to the UI. |
| locating_active_rect_ = true; |
| } else { |
| if (!was_active_frame) { |
| if (options.forward) |
| active_match_index_ = 0; |
| else |
| active_match_index_ = last_match_count_ - 1; |
| } else { |
| if (options.forward) |
| ++active_match_index_; |
| else |
| --active_match_index_; |
| |
| if (active_match_index_ + 1 > last_match_count_) |
| active_match_index_ = 0; |
| else if (active_match_index_ < 0) |
| active_match_index_ = last_match_count_ - 1; |
| } |
| WebRect selection_rect = OwnerFrame().GetFrameView()->ConvertToRootFrame( |
| active_match_->BoundingBox()); |
| ReportFindInPageSelection(selection_rect, active_match_index_ + 1, |
| identifier); |
| } |
| |
| // We found something, so the result of the previous scoping may be outdated. |
| last_find_request_completed_with_no_matches_ = false; |
| |
| return true; |
| } |
| |
| void TextFinder::ClearActiveFindMatch() { |
| current_active_match_frame_ = false; |
| SetMarkerActive(active_match_.Get(), false); |
| ResetActiveMatch(); |
| } |
| |
| LocalFrame* TextFinder::GetFrame() const { |
| return OwnerFrame().GetFrame(); |
| } |
| |
| void TextFinder::SetFindEndstateFocusAndSelection() { |
| if (!ActiveMatchFrame()) |
| return; |
| |
| Range* active_match = ActiveMatch(); |
| if (!active_match) |
| return; |
| |
| // If the user has set the selection since the match was found, we |
| // don't focus anything. |
| if (!GetFrame()->Selection().GetSelectionInDOMTree().IsNone()) |
| return; |
| |
| // Need to clean out style and layout state before querying |
| // Element::isFocusable(). |
| GetFrame()->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| // Try to find the first focusable node up the chain, which will, for |
| // example, focus links if we have found text within the link. |
| Node* node = active_match->FirstNode(); |
| if (node && node->IsInShadowTree()) { |
| if (Node* host = node->OwnerShadowHost()) { |
| if (IsHTMLInputElement(*host) || IsHTMLTextAreaElement(*host)) |
| node = host; |
| } |
| } |
| const EphemeralRange active_match_range(active_match); |
| if (node) { |
| for (Node& runner : NodeTraversal::InclusiveAncestorsOf(*node)) { |
| if (!runner.IsElementNode()) |
| continue; |
| Element& element = ToElement(runner); |
| if (element.IsFocusable()) { |
| // Found a focusable parent node. Set the active match as the |
| // selection and focus to the focusable node. |
| GetFrame()->Selection().SetSelectionAndEndTyping( |
| SelectionInDOMTree::Builder() |
| .SetBaseAndExtent(active_match_range) |
| .Build()); |
| GetFrame()->GetDocument()->SetFocusedElement( |
| &element, FocusParams(SelectionBehaviorOnFocus::kNone, |
| kWebFocusTypeNone, nullptr)); |
| return; |
| } |
| } |
| } |
| |
| // Iterate over all the nodes in the range until we find a focusable node. |
| // This, for example, sets focus to the first link if you search for |
| // text and text that is within one or more links. |
| for (Node& runner : active_match_range.Nodes()) { |
| if (!runner.IsElementNode()) |
| continue; |
| Element& element = ToElement(runner); |
| if (element.IsFocusable()) { |
| GetFrame()->GetDocument()->SetFocusedElement( |
| &element, FocusParams(SelectionBehaviorOnFocus::kNone, |
| kWebFocusTypeNone, nullptr)); |
| return; |
| } |
| } |
| |
| // No node related to the active match was focusable, so set the |
| // active match as the selection (so that when you end the Find session, |
| // you'll have the last thing you found highlighted) and make sure that |
| // we have nothing focused (otherwise you might have text selected but |
| // a link focused, which is weird). |
| GetFrame()->Selection().SetSelectionAndEndTyping( |
| SelectionInDOMTree::Builder() |
| .SetBaseAndExtent(active_match_range) |
| .Build()); |
| GetFrame()->GetDocument()->ClearFocusedElement(); |
| |
| // Finally clear the active match, for two reasons: |
| // We just finished the find 'session' and we don't want future (potentially |
| // unrelated) find 'sessions' operations to start at the same place. |
| // The WebLocalFrameImpl could get reused and the activeMatch could end up |
| // pointing to a document that is no longer valid. Keeping an invalid |
| // reference around is just asking for trouble. |
| ResetActiveMatch(); |
| } |
| |
| void TextFinder::StopFindingAndClearSelection() { |
| CancelPendingScopingEffort(); |
| |
| // Remove all markers for matches found and turn off the highlighting. |
| OwnerFrame().GetFrame()->GetDocument()->Markers().RemoveMarkersOfTypes( |
| DocumentMarker::MarkerTypes::TextMatch()); |
| OwnerFrame().GetFrame()->GetEditor().SetMarkedTextMatchesAreHighlighted( |
| false); |
| ClearFindMatchesCache(); |
| ResetActiveMatch(); |
| |
| // Let the frame know that we don't want tickmarks anymore. |
| InvalidatePaintForTickmarks(); |
| } |
| |
| void TextFinder::ReportFindInPageResultToAccessibility(int identifier) { |
| if (!active_match_) |
| return; |
| |
| AXObjectCacheBase* ax_object_cache = ToAXObjectCacheBase( |
| OwnerFrame().GetFrame()->GetDocument()->ExistingAXObjectCache()); |
| if (!ax_object_cache) |
| return; |
| |
| Node* start_node = active_match_->startContainer(); |
| Node* end_node = active_match_->endContainer(); |
| ax_object_cache->HandleTextMarkerDataAdded(start_node, end_node); |
| |
| if (OwnerFrame().Client()) { |
| OwnerFrame().Client()->HandleAccessibilityFindInPageResult( |
| identifier, active_match_index_ + 1, blink::WebNode(start_node), |
| active_match_->startOffset(), blink::WebNode(end_node), |
| active_match_->endOffset()); |
| } |
| } |
| |
| void TextFinder::StartScopingStringMatches(int identifier, |
| const WebString& search_text, |
| const WebFindOptions& options) { |
| CancelPendingScopingEffort(); |
| |
| // This is a brand new search, so we need to reset everything. |
| // Scoping is just about to begin. |
| scoping_in_progress_ = true; |
| |
| // Need to keep the current identifier locally in order to finish the |
| // request in case the frame is detached during the process. |
| find_request_identifier_ = identifier; |
| |
| // Clear highlighting for this frame. |
| UnmarkAllTextMatches(); |
| |
| // Clear the tickmarks and results cache. |
| ClearFindMatchesCache(); |
| |
| // Clear the total match count and increment markers version. |
| ResetMatchCount(); |
| |
| // Clear the counters from last operation. |
| last_match_count_ = 0; |
| next_invalidate_after_ = 0; |
| |
| // The view might be null on detached frames. |
| LocalFrame* frame = OwnerFrame().GetFrame(); |
| if (frame && frame->GetPage()) |
| frame_scoping_ = true; |
| |
| // Now, defer scoping until later to allow find operation to finish quickly. |
| ScopeStringMatchesSoon(identifier, search_text, options); |
| } |
| |
| void TextFinder::ScopeStringMatches(IdleDeadline* deadline, |
| int identifier, |
| const WebString& search_text, |
| const WebFindOptions& options) { |
| if (!ShouldScopeMatches(search_text, options)) { |
| FinishCurrentScopingEffort(identifier); |
| return; |
| } |
| |
| PositionInFlatTree search_start = PositionInFlatTree::FirstPositionInNode( |
| *OwnerFrame().GetFrame()->GetDocument()); |
| PositionInFlatTree search_end = PositionInFlatTree::LastPositionInNode( |
| *OwnerFrame().GetFrame()->GetDocument()); |
| DCHECK_EQ(search_start.GetDocument(), search_end.GetDocument()); |
| |
| if (resume_scoping_from_range_) { |
| // This is a continuation of a scoping operation that timed out and didn't |
| // complete last time around, so we should start from where we left off. |
| DCHECK(resume_scoping_from_range_->collapsed()); |
| search_start = FromPositionInDOMTree<EditingInFlatTreeStrategy>( |
| resume_scoping_from_range_->EndPosition()); |
| if (search_start.GetDocument() != search_end.GetDocument()) |
| return; |
| } |
| |
| // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets |
| // needs to be audited. see http://crbug.com/590369 for more details. |
| search_start.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| int match_count = 0; |
| bool full_range_searched = false; |
| PositionInFlatTree next_scoping_start; |
| do { |
| // Find next occurrence of the search string. |
| // FIXME: (http://crbug.com/6818) This WebKit operation may run for longer |
| // than the timeout value, and is not interruptible as it is currently |
| // written. We may need to rewrite it with interruptibility in mind, or |
| // find an alternative. |
| const EphemeralRangeInFlatTree result = |
| FindPlainText(EphemeralRangeInFlatTree(search_start, search_end), |
| search_text, options.match_case ? 0 : kCaseInsensitive); |
| if (result.IsCollapsed()) { |
| // Not found. |
| full_range_searched = true; |
| break; |
| } |
| Range* result_range = Range::Create( |
| result.GetDocument(), ToPositionInDOMTree(result.StartPosition()), |
| ToPositionInDOMTree(result.EndPosition())); |
| if (result_range->collapsed()) { |
| // resultRange will be collapsed if the matched text spans over multiple |
| // TreeScopes. FIXME: Show such matches to users. |
| search_start = result.EndPosition(); |
| if (deadline->timeRemaining() > 0) |
| continue; |
| break; |
| } |
| ++match_count; |
| |
| // Catch a special case where Find found something but doesn't know what |
| // the bounding box for it is. In this case we set the first match we find |
| // as the active rect. |
| IntRect result_bounds = result_range->BoundingBox(); |
| IntRect active_selection_rect; |
| if (locating_active_rect_) { |
| active_selection_rect = |
| active_match_.Get() ? active_match_->BoundingBox() : result_bounds; |
| } |
| |
| // If the Find function found a match it will have stored where the |
| // match was found in m_activeSelectionRect on the current frame. If we |
| // find this rect during scoping it means we have found the active |
| // tickmark. |
| bool found_active_match = false; |
| if (locating_active_rect_ && (active_selection_rect == result_bounds)) { |
| // We have found the active tickmark frame. |
| current_active_match_frame_ = true; |
| found_active_match = true; |
| // We also know which tickmark is active now. |
| active_match_index_ = total_match_count_ + match_count - 1; |
| // To stop looking for the active tickmark, we set this flag. |
| locating_active_rect_ = false; |
| |
| // Notify browser of new location for the selected rectangle. |
| ReportFindInPageSelection( |
| OwnerFrame().GetFrameView()->ConvertToRootFrame(result_bounds), |
| active_match_index_ + 1, identifier); |
| } |
| |
| OwnerFrame().GetFrame()->GetDocument()->Markers().AddTextMatchMarker( |
| EphemeralRange(result_range), |
| found_active_match ? TextMatchMarker::MatchStatus::kActive |
| : TextMatchMarker::MatchStatus::kInactive); |
| |
| find_matches_cache_.push_back( |
| FindMatch(result_range, last_match_count_ + match_count)); |
| |
| // Set the new start for the search range to be the end of the previous |
| // result range. There is no need to use a VisiblePosition here, |
| // since findPlainText will use a TextIterator to go over the visible |
| // text nodes. |
| search_start = result.EndPosition(); |
| |
| next_scoping_start = search_start; |
| } while (deadline->timeRemaining() > 0); |
| |
| if (next_scoping_start.IsNotNull()) { |
| resume_scoping_from_range_ = |
| Range::Create(*next_scoping_start.GetDocument(), |
| ToPositionInDOMTree(next_scoping_start), |
| ToPositionInDOMTree(next_scoping_start)); |
| } |
| |
| // Remember what we search for last time, so we can skip searching if more |
| // letters are added to the search string (and last outcome was 0). |
| last_search_string_ = search_text; |
| |
| if (match_count > 0) { |
| OwnerFrame().GetFrame()->GetEditor().SetMarkedTextMatchesAreHighlighted( |
| true); |
| |
| last_match_count_ += match_count; |
| |
| // Let the frame know how many matches we found during this pass. |
| IncreaseMatchCount(identifier, match_count); |
| } |
| |
| if (!full_range_searched) { |
| // If we found anything during this pass, we should redraw. However, we |
| // don't want to spam too much if the page is extremely long, so if we |
| // reach a certain point we start throttling the redraw requests. |
| if (match_count > 0) |
| InvalidateIfNecessary(); |
| |
| // Scoping effort ran out of time, lets ask for another time-slice. |
| ScopeStringMatchesSoon(identifier, search_text, options); |
| return; // Done for now, resume work later. |
| } |
| |
| FinishCurrentScopingEffort(identifier); |
| } |
| |
| void TextFinder::FlushCurrentScopingEffort(int identifier) { |
| if (!OwnerFrame().GetFrame() || !OwnerFrame().GetFrame()->GetPage()) |
| return; |
| |
| frame_scoping_ = false; |
| IncreaseMatchCount(identifier, 0); |
| } |
| |
| void TextFinder::FinishCurrentScopingEffort(int identifier) { |
| if (!total_match_count_) |
| OwnerFrame().GetFrame()->Selection().Clear(); |
| |
| FlushCurrentScopingEffort(identifier); |
| |
| scoping_in_progress_ = false; |
| last_find_request_completed_with_no_matches_ = !last_match_count_; |
| |
| // This frame is done, so show any scrollbar tickmarks we haven't drawn yet. |
| InvalidatePaintForTickmarks(); |
| } |
| |
| void TextFinder::CancelPendingScopingEffort() { |
| if (idle_scoping_callback_) { |
| idle_scoping_callback_->Dispose(); |
| idle_scoping_callback_.Clear(); |
| } |
| |
| active_match_index_ = -1; |
| |
| // Last request didn't complete. |
| if (scoping_in_progress_) |
| last_find_request_completed_with_no_matches_ = false; |
| |
| scoping_in_progress_ = false; |
| |
| resume_scoping_from_range_ = nullptr; |
| } |
| |
| void TextFinder::IncreaseMatchCount(int identifier, int count) { |
| if (count) |
| ++find_match_markers_version_; |
| |
| total_match_count_ += count; |
| |
| // Update the UI with the latest findings. |
| OwnerFrame().GetFindInPage()->ReportFindInPageMatchCount( |
| identifier, total_match_count_, !frame_scoping_ || !total_match_count_); |
| } |
| |
| void TextFinder::ReportFindInPageSelection(const WebRect& selection_rect, |
| int active_match_ordinal, |
| int identifier) { |
| // Update the UI with the latest selection rect. |
| OwnerFrame().GetFindInPage()->ReportFindInPageSelection( |
| identifier, active_match_ordinal, selection_rect, |
| false /* final_update */); |
| // Update accessibility too, so if the user commits to this query |
| // we can move accessibility focus to this result. |
| ReportFindInPageResultToAccessibility(identifier); |
| } |
| |
| void TextFinder::ResetMatchCount() { |
| if (total_match_count_ > 0) |
| ++find_match_markers_version_; |
| |
| total_match_count_ = 0; |
| frame_scoping_ = false; |
| } |
| |
| void TextFinder::ClearFindMatchesCache() { |
| if (!find_matches_cache_.IsEmpty()) |
| ++find_match_markers_version_; |
| |
| find_matches_cache_.clear(); |
| find_match_rects_are_valid_ = false; |
| } |
| |
| void TextFinder::UpdateFindMatchRects() { |
| IntSize current_document_size = OwnerFrame().DocumentSize(); |
| if (document_size_for_current_find_match_rects_ != current_document_size) { |
| document_size_for_current_find_match_rects_ = current_document_size; |
| find_match_rects_are_valid_ = false; |
| } |
| |
| size_t dead_matches = 0; |
| for (FindMatch& match : find_matches_cache_) { |
| if (!match.range_->BoundaryPointsValid() || |
| !match.range_->startContainer()->isConnected()) |
| match.rect_ = FloatRect(); |
| else if (!find_match_rects_are_valid_) |
| match.rect_ = FindInPageRectFromRange(EphemeralRange(match.range_.Get())); |
| |
| if (match.rect_.IsEmpty()) |
| ++dead_matches; |
| } |
| |
| // Remove any invalid matches from the cache. |
| if (dead_matches) { |
| HeapVector<FindMatch> filtered_matches; |
| filtered_matches.ReserveCapacity(find_matches_cache_.size() - dead_matches); |
| |
| for (const FindMatch& match : find_matches_cache_) { |
| if (!match.rect_.IsEmpty()) |
| filtered_matches.push_back(match); |
| } |
| |
| find_matches_cache_.swap(filtered_matches); |
| } |
| |
| find_match_rects_are_valid_ = true; |
| } |
| |
| WebFloatRect TextFinder::ActiveFindMatchRect() { |
| if (!current_active_match_frame_ || !active_match_) |
| return WebFloatRect(); |
| |
| return WebFloatRect(FindInPageRectFromRange(EphemeralRange(ActiveMatch()))); |
| } |
| |
| Vector<WebFloatRect> TextFinder::FindMatchRects() { |
| UpdateFindMatchRects(); |
| |
| Vector<WebFloatRect> match_rects; |
| match_rects.ReserveCapacity(match_rects.size() + find_matches_cache_.size()); |
| for (const FindMatch& match : find_matches_cache_) { |
| DCHECK(!match.rect_.IsEmpty()); |
| match_rects.push_back(match.rect_); |
| } |
| |
| return match_rects; |
| } |
| |
| int TextFinder::SelectNearestFindMatch(const WebFloatPoint& point, |
| WebRect* selection_rect) { |
| int index = NearestFindMatch(point, nullptr); |
| if (index != -1) |
| return SelectFindMatch(static_cast<unsigned>(index), selection_rect); |
| |
| return -1; |
| } |
| |
| int TextFinder::NearestFindMatch(const FloatPoint& point, |
| float* distance_squared) { |
| UpdateFindMatchRects(); |
| |
| int nearest = -1; |
| float nearest_distance_squared = FLT_MAX; |
| for (size_t i = 0; i < find_matches_cache_.size(); ++i) { |
| DCHECK(!find_matches_cache_[i].rect_.IsEmpty()); |
| FloatSize offset = point - find_matches_cache_[i].rect_.Center(); |
| float width = offset.Width(); |
| float height = offset.Height(); |
| float current_distance_squared = width * width + height * height; |
| if (current_distance_squared < nearest_distance_squared) { |
| nearest = i; |
| nearest_distance_squared = current_distance_squared; |
| } |
| } |
| |
| if (distance_squared) |
| *distance_squared = nearest_distance_squared; |
| |
| return nearest; |
| } |
| |
| int TextFinder::SelectFindMatch(unsigned index, WebRect* selection_rect) { |
| SECURITY_DCHECK(index < find_matches_cache_.size()); |
| |
| Range* range = find_matches_cache_[index].range_; |
| if (!range->BoundaryPointsValid() || !range->startContainer()->isConnected()) |
| return -1; |
| |
| // Check if the match is already selected. |
| if (!current_active_match_frame_ || !active_match_ || |
| !AreRangesEqual(active_match_.Get(), range)) { |
| active_match_index_ = find_matches_cache_[index].ordinal_ - 1; |
| |
| // Set this frame as the active frame (the one with the active highlight). |
| current_active_match_frame_ = true; |
| OwnerFrame().ViewImpl()->SetFocusedFrame(&OwnerFrame()); |
| |
| if (active_match_) |
| SetMarkerActive(active_match_.Get(), false); |
| active_match_ = range; |
| SetMarkerActive(active_match_.Get(), true); |
| |
| // Clear any user selection, to make sure Find Next continues on from the |
| // match we just activated. |
| OwnerFrame().GetFrame()->Selection().Clear(); |
| |
| // Make sure no node is focused. See http://crbug.com/38700. |
| OwnerFrame().GetFrame()->GetDocument()->ClearFocusedElement(); |
| } |
| |
| IntRect active_match_rect; |
| IntRect active_match_bounding_box = |
| EnclosingIntRect(LayoutObject::AbsoluteBoundingBoxRectForRange( |
| EphemeralRange(active_match_.Get()))); |
| |
| if (!active_match_bounding_box.IsEmpty()) { |
| if (active_match_->FirstNode() && |
| active_match_->FirstNode()->GetLayoutObject()) { |
| active_match_->FirstNode()->GetLayoutObject()->ScrollRectToVisible( |
| LayoutRect(active_match_bounding_box), |
| WebScrollIntoViewParams(ScrollAlignment::kAlignCenterIfNeeded, |
| ScrollAlignment::kAlignCenterIfNeeded, |
| kUserScroll)); |
| |
| // Absolute coordinates are scroll-variant so the bounding box will change |
| // if the page is scrolled by ScrollRectToVisible above. Recompute the |
| // bounding box so we have the updated location for the zoom below. |
| // TODO(bokan): This should really use the return value from |
| // ScrollRectToVisible which returns the updated position of the |
| // scrolled rect. However, this was recently added and this is a fix |
| // that needs to be merged to a release branch. |
| // https://crbug.com/823365. |
| active_match_bounding_box = |
| EnclosingIntRect(LayoutObject::AbsoluteBoundingBoxRectForRange( |
| EphemeralRange(active_match_.Get()))); |
| } |
| |
| // Zoom to the active match. |
| active_match_rect = OwnerFrame().GetFrameView()->ConvertToRootFrame( |
| active_match_bounding_box); |
| OwnerFrame().ViewImpl()->ZoomToFindInPageRect(active_match_rect); |
| } |
| |
| if (selection_rect) |
| *selection_rect = active_match_rect; |
| |
| return active_match_index_ + 1; |
| } |
| |
| TextFinder* TextFinder::Create(WebLocalFrameImpl& owner_frame) { |
| return new TextFinder(owner_frame); |
| } |
| |
| TextFinder::TextFinder(WebLocalFrameImpl& owner_frame) |
| : owner_frame_(&owner_frame), |
| current_active_match_frame_(false), |
| active_match_index_(-1), |
| resume_scoping_from_range_(nullptr), |
| last_match_count_(-1), |
| total_match_count_(-1), |
| frame_scoping_(false), |
| find_request_identifier_(-1), |
| next_invalidate_after_(0), |
| find_match_markers_version_(0), |
| locating_active_rect_(false), |
| scoping_in_progress_(false), |
| last_find_request_completed_with_no_matches_(false), |
| find_match_rects_are_valid_(false) {} |
| |
| TextFinder::~TextFinder() = default; |
| |
| bool TextFinder::SetMarkerActive(Range* range, bool active) { |
| if (!range || range->collapsed()) |
| return false; |
| return OwnerFrame() |
| .GetFrame() |
| ->GetDocument() |
| ->Markers() |
| .SetTextMatchMarkersActive(EphemeralRange(range), active); |
| } |
| |
| void TextFinder::UnmarkAllTextMatches() { |
| LocalFrame* frame = OwnerFrame().GetFrame(); |
| if (frame && frame->GetPage() && |
| frame->GetEditor().MarkedTextMatchesAreHighlighted()) { |
| frame->GetDocument()->Markers().RemoveMarkersOfTypes( |
| DocumentMarker::MarkerTypes::TextMatch()); |
| } |
| } |
| |
| bool TextFinder::ShouldScopeMatches(const String& search_text, |
| const WebFindOptions& options) { |
| // Don't scope if we can't find a frame or a view. |
| // The user may have closed the tab/application, so abort. |
| LocalFrame* frame = OwnerFrame().GetFrame(); |
| if (!frame || !frame->View() || !frame->GetPage()) |
| return false; |
| |
| DCHECK(frame->GetDocument()); |
| DCHECK(frame->View()); |
| |
| if (options.force) |
| return true; |
| |
| if (!OwnerFrame().HasVisibleContent()) |
| return false; |
| |
| // If the frame completed the scoping operation and found 0 matches the last |
| // time it was searched, then we don't have to search it again if the user is |
| // just adding to the search string or sending the same search string again. |
| if (last_find_request_completed_with_no_matches_ && |
| !last_search_string_.IsEmpty()) { |
| // Check to see if the search string prefixes match. |
| String previous_search_prefix = |
| search_text.Substring(0, last_search_string_.length()); |
| |
| if (previous_search_prefix == last_search_string_) |
| return false; // Don't search this frame, it will be fruitless. |
| } |
| |
| return true; |
| } |
| |
| void TextFinder::ScopeStringMatchesSoon(int identifier, |
| const WebString& search_text, |
| const WebFindOptions& options) { |
| DCHECK_EQ(idle_scoping_callback_, nullptr); |
| // If it's for testing, run the scoping immediately. |
| // TODO(rakina): Change to use general solution when it's available. |
| // https://crbug.com/875203 |
| if (options.run_synchronously_for_testing) { |
| ScopeStringMatches( |
| IdleDeadline::Create(CurrentTimeTicks() + kForcedInvocationDeadline, |
| IdleDeadline::CallbackType::kCalledWhenIdle), |
| identifier, search_text, options); |
| } else { |
| idle_scoping_callback_ = IdleScopeStringMatchesCallback::Create( |
| this, identifier, search_text, options); |
| } |
| } |
| |
| void TextFinder::ResumeScopingStringMatches(IdleDeadline* deadline, |
| int identifier, |
| const WebString& search_text, |
| const WebFindOptions& options) { |
| idle_scoping_callback_.Clear(); |
| |
| ScopeStringMatches(deadline, identifier, search_text, options); |
| } |
| |
| void TextFinder::InvalidateIfNecessary() { |
| if (last_match_count_ <= next_invalidate_after_) |
| return; |
| |
| // FIXME: (http://crbug.com/6819) Optimize the drawing of the tickmarks and |
| // remove this. This calculation sets a milestone for when next to |
| // invalidate the scrollbar and the content area. We do this so that we |
| // don't spend too much time drawing the scrollbar over and over again. |
| // Basically, up until the first 500 matches there is no throttle. |
| // After the first 500 matches, we set set the milestone further and |
| // further out (750, 1125, 1688, 2K, 3K). |
| static const int kStartSlowingDownAfter = 500; |
| static const int kSlowdown = 750; |
| |
| int i = last_match_count_ / kStartSlowingDownAfter; |
| next_invalidate_after_ += i * kSlowdown; |
| InvalidatePaintForTickmarks(); |
| } |
| |
| void TextFinder::FlushCurrentScoping() { |
| FlushCurrentScopingEffort(find_request_identifier_); |
| } |
| |
| void TextFinder::InvalidatePaintForTickmarks() { |
| OwnerFrame().GetFrame()->ContentLayoutObject()->InvalidatePaintForTickmarks(); |
| } |
| |
| void TextFinder::Trace(blink::Visitor* visitor) { |
| visitor->Trace(owner_frame_); |
| visitor->Trace(active_match_); |
| visitor->Trace(resume_scoping_from_range_); |
| visitor->Trace(idle_scoping_callback_); |
| visitor->Trace(find_matches_cache_); |
| } |
| |
| } // namespace blink |