/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsCOMPtr.h" #include "nsMemory.h" #include "nsIServiceManager.h" #include "mozilla/ModuleUtils.h" #include "mozilla/Services.h" #include "nsIWebBrowserChrome.h" #include "nsCURILoader.h" #include "nsCycleCollectionParticipant.h" #include "nsNetUtil.h" #include "nsIURL.h" #include "nsIURI.h" #include "nsIDocShell.h" #include "nsIDocShellTreeOwner.h" #include "nsISimpleEnumerator.h" #include "nsPIDOMWindow.h" #include "nsIPrefBranch.h" #include "nsIPrefService.h" #include "nsString.h" #include "nsCRT.h" #include "nsIDOMNode.h" #include "mozilla/dom/Element.h" #include "nsIFrame.h" #include "nsFrameTraversal.h" #include "nsIImageDocument.h" #include "nsIDOMHTMLDocument.h" #include "nsIDOMHTMLElement.h" #include "nsIDocument.h" #include "nsISelection.h" #include "nsTextFragment.h" #include "nsIDOMNSEditableElement.h" #include "nsIEditor.h" #include "nsIDocShellTreeItem.h" #include "nsIWebNavigation.h" #include "nsIInterfaceRequestor.h" #include "nsIInterfaceRequestorUtils.h" #include "nsContentCID.h" #include "nsLayoutCID.h" #include "nsWidgetsCID.h" #include "nsIFormControl.h" #include "nsNameSpaceManager.h" #include "nsIWindowWatcher.h" #include "nsIObserverService.h" #include "nsFocusManager.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/Link.h" #include "nsRange.h" #include "nsXBLBinding.h" #include "nsTypeAheadFind.h" NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTypeAheadFind) NS_INTERFACE_MAP_ENTRY(nsITypeAheadFind) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITypeAheadFind) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTypeAheadFind) NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTypeAheadFind) NS_IMPL_CYCLE_COLLECTION(nsTypeAheadFind, mFoundLink, mFoundEditable, mCurrentWindow, mStartFindRange, mSearchRange, mStartPointRange, mEndPointRange, mSoundInterface, mFind, mFoundRange) static NS_DEFINE_CID(kFrameTraversalCID, NS_FRAMETRAVERSAL_CID); #define NS_FIND_CONTRACTID "@mozilla.org/embedcomp/rangefind;1" nsTypeAheadFind::nsTypeAheadFind(): mStartLinksOnlyPref(false), mCaretBrowsingOn(false), mDidAddObservers(false), mLastFindLength(0), mIsSoundInitialized(false), mCaseSensitive(false) { } nsTypeAheadFind::~nsTypeAheadFind() { nsCOMPtr prefInternal(do_GetService(NS_PREFSERVICE_CONTRACTID)); if (prefInternal) { prefInternal->RemoveObserver("accessibility.typeaheadfind", this); prefInternal->RemoveObserver("accessibility.browsewithcaret", this); } } nsresult nsTypeAheadFind::Init(nsIDocShell* aDocShell) { nsCOMPtr prefInternal(do_GetService(NS_PREFSERVICE_CONTRACTID)); mSearchRange = nullptr; mStartPointRange = nullptr; mEndPointRange = nullptr; if (!prefInternal || !EnsureFind()) return NS_ERROR_FAILURE; SetDocShell(aDocShell); if (!mDidAddObservers) { mDidAddObservers = true; // ----------- Listen to prefs ------------------ nsresult rv = prefInternal->AddObserver("accessibility.browsewithcaret", this, true); NS_ENSURE_SUCCESS(rv, rv); // ----------- Get initial preferences ---------- PrefsReset(); nsCOMPtr os = mozilla::services::GetObserverService(); if (os) { os->AddObserver(this, DOM_WINDOW_DESTROYED_TOPIC, true); } } return NS_OK; } nsresult nsTypeAheadFind::PrefsReset() { nsCOMPtr prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID)); NS_ENSURE_TRUE(prefBranch, NS_ERROR_FAILURE); prefBranch->GetBoolPref("accessibility.typeaheadfind.startlinksonly", &mStartLinksOnlyPref); bool isSoundEnabled = true; prefBranch->GetBoolPref("accessibility.typeaheadfind.enablesound", &isSoundEnabled); nsXPIDLCString soundStr; if (isSoundEnabled) prefBranch->GetCharPref("accessibility.typeaheadfind.soundURL", getter_Copies(soundStr)); mNotFoundSoundURL = soundStr; prefBranch->GetBoolPref("accessibility.browsewithcaret", &mCaretBrowsingOn); return NS_OK; } NS_IMETHODIMP nsTypeAheadFind::SetCaseSensitive(bool isCaseSensitive) { mCaseSensitive = isCaseSensitive; if (mFind) { mFind->SetCaseSensitive(mCaseSensitive); } return NS_OK; } NS_IMETHODIMP nsTypeAheadFind::GetCaseSensitive(bool* isCaseSensitive) { *isCaseSensitive = mCaseSensitive; return NS_OK; } NS_IMETHODIMP nsTypeAheadFind::SetDocShell(nsIDocShell* aDocShell) { mDocShell = do_GetWeakReference(aDocShell); mWebBrowserFind = do_GetInterface(aDocShell); NS_ENSURE_TRUE(mWebBrowserFind, NS_ERROR_FAILURE); nsCOMPtr presShell; presShell = aDocShell->GetPresShell(); mPresShell = do_GetWeakReference(presShell); ReleaseStrongMemberVariables(); return NS_OK; } void nsTypeAheadFind::ReleaseStrongMemberVariables() { mStartFindRange = nullptr; mStartPointRange = nullptr; mSearchRange = nullptr; mEndPointRange = nullptr; mFoundLink = nullptr; mFoundEditable = nullptr; mFoundRange = nullptr; mCurrentWindow = nullptr; mSelectionController = nullptr; mFind = nullptr; } NS_IMETHODIMP nsTypeAheadFind::SetSelectionModeAndRepaint(int16_t aToggle) { nsCOMPtr selectionController = do_QueryReferent(mSelectionController); if (!selectionController) { return NS_OK; } selectionController->SetDisplaySelection(aToggle); selectionController->RepaintSelection(nsISelectionController::SELECTION_NORMAL); return NS_OK; } NS_IMETHODIMP nsTypeAheadFind::CollapseSelection() { nsCOMPtr selectionController = do_QueryReferent(mSelectionController); if (!selectionController) { return NS_OK; } nsCOMPtr selection; selectionController->GetSelection(nsISelectionController::SELECTION_NORMAL, getter_AddRefs(selection)); if (selection) selection->CollapseToStart(); return NS_OK; } NS_IMETHODIMP nsTypeAheadFind::Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) { if (!nsCRT::strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { return PrefsReset(); } else if (!nsCRT::strcmp(aTopic, DOM_WINDOW_DESTROYED_TOPIC) && SameCOMIdentity(aSubject, mCurrentWindow)) { ReleaseStrongMemberVariables(); } return NS_OK; } void nsTypeAheadFind::SaveFind() { if (mWebBrowserFind) mWebBrowserFind->SetSearchString(mTypeAheadBuffer.get()); // save the length of this find for "not found" sound mLastFindLength = mTypeAheadBuffer.Length(); } void nsTypeAheadFind::PlayNotFoundSound() { if (mNotFoundSoundURL.IsEmpty()) // no sound return; if (!mSoundInterface) mSoundInterface = do_CreateInstance("@mozilla.org/sound;1"); if (mSoundInterface) { mIsSoundInitialized = true; if (mNotFoundSoundURL.EqualsLiteral("beep")) { mSoundInterface->Beep(); return; } nsCOMPtr soundURI; if (mNotFoundSoundURL.EqualsLiteral("default")) NS_NewURI(getter_AddRefs(soundURI), NS_LITERAL_CSTRING(TYPEAHEADFIND_NOTFOUND_WAV_URL)); else NS_NewURI(getter_AddRefs(soundURI), mNotFoundSoundURL); nsCOMPtr soundURL(do_QueryInterface(soundURI)); if (soundURL) mSoundInterface->Play(soundURL); } } nsresult nsTypeAheadFind::FindItNow(nsIPresShell *aPresShell, bool aIsLinksOnly, bool aIsFirstVisiblePreferred, bool aFindPrev, uint16_t* aResult) { *aResult = FIND_NOTFOUND; mFoundLink = nullptr; mFoundEditable = nullptr; mFoundRange = nullptr; mCurrentWindow = nullptr; nsCOMPtr startingPresShell (GetPresShell()); if (!startingPresShell) { nsCOMPtr ds = do_QueryReferent(mDocShell); NS_ENSURE_TRUE(ds, NS_ERROR_FAILURE); startingPresShell = ds->GetPresShell(); mPresShell = do_GetWeakReference(startingPresShell); } nsCOMPtr presShell(aPresShell); if (!presShell) { presShell = startingPresShell; // this is the current document if (!presShell) return NS_ERROR_FAILURE; } RefPtr presContext = presShell->GetPresContext(); if (!presContext) return NS_ERROR_FAILURE; nsCOMPtr selection; nsCOMPtr selectionController = do_QueryReferent(mSelectionController); if (!selectionController) { GetSelection(presShell, getter_AddRefs(selectionController), getter_AddRefs(selection)); // cache for reuse mSelectionController = do_GetWeakReference(selectionController); } else { selectionController->GetSelection( nsISelectionController::SELECTION_NORMAL, getter_AddRefs(selection)); } nsCOMPtr startingDocShell(presContext->GetDocShell()); NS_ASSERTION(startingDocShell, "Bug 175321 Crashes with Type Ahead Find [@ nsTypeAheadFind::FindItNow]"); if (!startingDocShell) return NS_ERROR_FAILURE; nsCOMPtr rootContentTreeItem; nsCOMPtr currentDocShell; startingDocShell->GetSameTypeRootTreeItem(getter_AddRefs(rootContentTreeItem)); nsCOMPtr rootContentDocShell = do_QueryInterface(rootContentTreeItem); if (!rootContentDocShell) return NS_ERROR_FAILURE; nsCOMPtr docShellEnumerator; rootContentDocShell->GetDocShellEnumerator(nsIDocShellTreeItem::typeContent, nsIDocShell::ENUMERATE_FORWARDS, getter_AddRefs(docShellEnumerator)); // Default: can start at the current document nsCOMPtr currentContainer = do_QueryInterface(rootContentDocShell); // Iterate up to current shell, if there's more than 1 that we're // dealing with bool hasMoreDocShells; while (NS_SUCCEEDED(docShellEnumerator->HasMoreElements(&hasMoreDocShells)) && hasMoreDocShells) { docShellEnumerator->GetNext(getter_AddRefs(currentContainer)); currentDocShell = do_QueryInterface(currentContainer); if (!currentDocShell || currentDocShell == startingDocShell || aIsFirstVisiblePreferred) break; } // ------------ Get ranges ready ---------------- nsCOMPtr returnRange; nsCOMPtr focusedPS; if (NS_FAILED(GetSearchContainers(currentContainer, (!aIsFirstVisiblePreferred || mStartFindRange) ? selectionController.get() : nullptr, aIsFirstVisiblePreferred, aFindPrev, getter_AddRefs(presShell), getter_AddRefs(presContext)))) { return NS_ERROR_FAILURE; } int16_t rangeCompareResult = 0; if (!mStartPointRange) { mStartPointRange = new nsRange(presShell->GetDocument()); } mStartPointRange->CompareBoundaryPoints(nsIDOMRange::START_TO_START, mSearchRange, &rangeCompareResult); // No need to wrap find in doc if starting at beginning bool hasWrapped = (rangeCompareResult < 0); if (mTypeAheadBuffer.IsEmpty() || !EnsureFind()) return NS_ERROR_FAILURE; mFind->SetFindBackwards(aFindPrev); while (true) { // ----- Outer while loop: go through all docs ----- while (true) { // === Inner while loop: go through a single doc === mFind->Find(mTypeAheadBuffer, mSearchRange, mStartPointRange, mEndPointRange, getter_AddRefs(returnRange)); if (!returnRange) break; // Nothing found in this doc, go to outer loop (try next doc) // ------- Test resulting found range for success conditions ------ bool isInsideLink = false, isStartingLink = false; if (aIsLinksOnly) { // Don't check if inside link when searching all text RangeStartsInsideLink(returnRange, presShell, &isInsideLink, &isStartingLink); } bool usesIndependentSelection; if (!IsRangeVisible(presShell, presContext, returnRange, aIsFirstVisiblePreferred, false, getter_AddRefs(mStartPointRange), &usesIndependentSelection) || (aIsLinksOnly && !isInsideLink) || (mStartLinksOnlyPref && aIsLinksOnly && !isStartingLink)) { // ------ Failure ------ // At this point mStartPointRange got updated to the first // visible range in the viewport. We _may_ be able to just // start there, if it's not taking us in the wrong direction. if (aFindPrev) { // We can continue at the end of mStartPointRange if its end is before // the start of returnRange or coincides with it. Otherwise, we need // to continue at the start of returnRange. int16_t compareResult; nsresult rv = mStartPointRange->CompareBoundaryPoints(nsIDOMRange::START_TO_END, returnRange, &compareResult); if (NS_SUCCEEDED(rv) && compareResult <= 0) { // OK to start at the end of mStartPointRange mStartPointRange->Collapse(false); } else { // Start at the beginning of returnRange returnRange->CloneRange(getter_AddRefs(mStartPointRange)); mStartPointRange->Collapse(true); } } else { // We can continue at the start of mStartPointRange if its start is // after the end of returnRange or coincides with it. Otherwise, we // need to continue at the end of returnRange. int16_t compareResult; nsresult rv = mStartPointRange->CompareBoundaryPoints(nsIDOMRange::END_TO_START, returnRange, &compareResult); if (NS_SUCCEEDED(rv) && compareResult >= 0) { // OK to start at the start of mStartPointRange mStartPointRange->Collapse(true); } else { // Start at the end of returnRange returnRange->CloneRange(getter_AddRefs(mStartPointRange)); mStartPointRange->Collapse(false); } } continue; } mFoundRange = returnRange; // ------ Success! ------- // Hide old selection (new one may be on a different controller) if (selection) { selection->CollapseToStart(); SetSelectionModeAndRepaint(nsISelectionController::SELECTION_ON); } // Make sure new document is selected if (presShell != startingPresShell) { // We are in a new document (because of frames/iframes) mPresShell = do_GetWeakReference(presShell); } nsCOMPtr document = do_QueryInterface(presShell->GetDocument()); NS_ASSERTION(document, "Wow, presShell doesn't have document!"); if (!document) return NS_ERROR_UNEXPECTED; nsCOMPtr window = document->GetInnerWindow(); NS_ASSERTION(window, "document has no window"); if (!window) return NS_ERROR_UNEXPECTED; nsCOMPtr fm = do_GetService(FOCUSMANAGER_CONTRACTID); if (usesIndependentSelection) { /* If a search result is found inside an editable element, we'll focus * the element only if focus is in our content window, i.e. * |if (focusedWindow.top == ourWindow.top)| */ bool shouldFocusEditableElement = false; if (fm) { nsCOMPtr focusedWindow; nsresult rv = fm->GetFocusedWindow(getter_AddRefs(focusedWindow)); if (NS_SUCCEEDED(rv)) { nsCOMPtr fwPI(do_QueryInterface(focusedWindow, &rv)); if (NS_SUCCEEDED(rv)) { nsCOMPtr fwTreeItem (do_QueryInterface(fwPI->GetDocShell(), &rv)); if (NS_SUCCEEDED(rv)) { nsCOMPtr fwRootTreeItem; rv = fwTreeItem->GetSameTypeRootTreeItem(getter_AddRefs(fwRootTreeItem)); if (NS_SUCCEEDED(rv) && fwRootTreeItem == rootContentTreeItem) shouldFocusEditableElement = true; } } } } // We may be inside an editable element, and therefore the selection // may be controlled by a different selection controller. Walk up the // chain of parent nodes to see if we find one. nsCOMPtr node; returnRange->GetStartContainer(getter_AddRefs(node)); while (node) { nsCOMPtr editable = do_QueryInterface(node); if (editable) { // Inside an editable element. Get the correct selection // controller and selection. nsCOMPtr editor; editable->GetEditor(getter_AddRefs(editor)); NS_ASSERTION(editor, "Editable element has no editor!"); if (!editor) { break; } editor->GetSelectionController( getter_AddRefs(selectionController)); if (selectionController) { selectionController->GetSelection( nsISelectionController::SELECTION_NORMAL, getter_AddRefs(selection)); } mFoundEditable = do_QueryInterface(node); if (!shouldFocusEditableElement) break; // Otherwise move focus/caret to editable element if (fm) fm->SetFocus(mFoundEditable, 0); break; } nsIDOMNode* tmp = node; tmp->GetParentNode(getter_AddRefs(node)); } // If we reach here without setting mFoundEditable, then something // besides editable elements can cause us to have an independent // selection controller. I don't know whether this is possible. // Currently, we simply fall back to grabbing the document's selection // controller in this case. Perhaps we should reject this find match // and search again. NS_ASSERTION(mFoundEditable, "Independent selection controller on " "non-editable element!"); } if (!mFoundEditable) { // Not using a separate selection controller, so just get the // document's controller and selection. GetSelection(presShell, getter_AddRefs(selectionController), getter_AddRefs(selection)); } mSelectionController = do_GetWeakReference(selectionController); // Select the found text if (selection) { selection->RemoveAllRanges(); selection->AddRange(returnRange); } if (!mFoundEditable && fm) { nsCOMPtr win = do_QueryInterface(window); fm->MoveFocus(win, nullptr, nsIFocusManager::MOVEFOCUS_CARET, nsIFocusManager::FLAG_NOSCROLL | nsIFocusManager::FLAG_NOSWITCHFRAME, getter_AddRefs(mFoundLink)); } // Change selection color to ATTENTION and scroll to it. Careful: we // must wait until after we goof with focus above before changing to // ATTENTION, or when we MoveFocus() and the selection is not on a // link, we'll blur, which will lose the ATTENTION. if (selectionController) { // Beware! This may flush notifications via synchronous // ScrollSelectionIntoView. SetSelectionModeAndRepaint(nsISelectionController::SELECTION_ATTENTION); selectionController->ScrollSelectionIntoView( nsISelectionController::SELECTION_NORMAL, nsISelectionController::SELECTION_WHOLE_SELECTION, nsISelectionController::SCROLL_CENTER_VERTICALLY | nsISelectionController::SCROLL_SYNCHRONOUS); } mCurrentWindow = window; *aResult = hasWrapped ? FIND_WRAPPED : FIND_FOUND; return NS_OK; } // ======= end-inner-while (go through a single document) ========== // ---------- Nothing found yet, try next document ------------- bool hasTriedFirstDoc = false; do { // ==== Second inner loop - get another while ==== if (NS_SUCCEEDED(docShellEnumerator->HasMoreElements(&hasMoreDocShells)) && hasMoreDocShells) { docShellEnumerator->GetNext(getter_AddRefs(currentContainer)); NS_ASSERTION(currentContainer, "HasMoreElements lied to us!"); currentDocShell = do_QueryInterface(currentContainer); if (currentDocShell) break; } else if (hasTriedFirstDoc) // Avoid potential infinite loop return NS_ERROR_FAILURE; // No content doc shells // Reached last doc shell, loop around back to first doc shell rootContentDocShell->GetDocShellEnumerator(nsIDocShellTreeItem::typeContent, nsIDocShell::ENUMERATE_FORWARDS, getter_AddRefs(docShellEnumerator)); hasTriedFirstDoc = true; } while (docShellEnumerator); // ==== end second inner while === bool continueLoop = false; if (currentDocShell != startingDocShell) continueLoop = true; // Try next document else if (!hasWrapped || aIsFirstVisiblePreferred) { // Finished searching through docshells: // If aFirstVisiblePreferred == true, we may need to go through all // docshells twice -once to look for visible matches, the second time // for any match aIsFirstVisiblePreferred = false; hasWrapped = true; continueLoop = true; // Go through all docs again } if (continueLoop) { if (NS_FAILED(GetSearchContainers(currentContainer, nullptr, aIsFirstVisiblePreferred, aFindPrev, getter_AddRefs(presShell), getter_AddRefs(presContext)))) { continue; } if (aFindPrev) { // Reverse mode: swap start and end points, so that we start // at end of document and go to beginning nsCOMPtr tempRange; mStartPointRange->CloneRange(getter_AddRefs(tempRange)); if (!mEndPointRange) { mEndPointRange = new nsRange(presShell->GetDocument()); } mStartPointRange = mEndPointRange; mEndPointRange = tempRange; } continue; } // ------------- Failed -------------- break; } // end-outer-while: go through all docs return NS_ERROR_FAILURE; } NS_IMETHODIMP nsTypeAheadFind::GetSearchString(nsAString& aSearchString) { aSearchString = mTypeAheadBuffer; return NS_OK; } NS_IMETHODIMP nsTypeAheadFind::GetFoundLink(nsIDOMElement** aFoundLink) { NS_ENSURE_ARG_POINTER(aFoundLink); *aFoundLink = mFoundLink; NS_IF_ADDREF(*aFoundLink); return NS_OK; } NS_IMETHODIMP nsTypeAheadFind::GetFoundEditable(nsIDOMElement** aFoundEditable) { NS_ENSURE_ARG_POINTER(aFoundEditable); *aFoundEditable = mFoundEditable; NS_IF_ADDREF(*aFoundEditable); return NS_OK; } NS_IMETHODIMP nsTypeAheadFind::GetCurrentWindow(nsIDOMWindow** aCurrentWindow) { NS_ENSURE_ARG_POINTER(aCurrentWindow); *aCurrentWindow = mCurrentWindow; NS_IF_ADDREF(*aCurrentWindow); return NS_OK; } nsresult nsTypeAheadFind::GetSearchContainers(nsISupports *aContainer, nsISelectionController *aSelectionController, bool aIsFirstVisiblePreferred, bool aFindPrev, nsIPresShell **aPresShell, nsPresContext **aPresContext) { NS_ENSURE_ARG_POINTER(aContainer); NS_ENSURE_ARG_POINTER(aPresShell); NS_ENSURE_ARG_POINTER(aPresContext); *aPresShell = nullptr; *aPresContext = nullptr; nsCOMPtr docShell(do_QueryInterface(aContainer)); if (!docShell) return NS_ERROR_FAILURE; nsCOMPtr presShell = docShell->GetPresShell(); RefPtr presContext; docShell->GetPresContext(getter_AddRefs(presContext)); if (!presShell || !presContext) return NS_ERROR_FAILURE; nsIDocument* doc = presShell->GetDocument(); if (!doc) return NS_ERROR_FAILURE; nsCOMPtr rootContent; nsCOMPtr htmlDoc(do_QueryInterface(doc)); if (htmlDoc) { nsCOMPtr bodyEl; htmlDoc->GetBody(getter_AddRefs(bodyEl)); rootContent = do_QueryInterface(bodyEl); } if (!rootContent) rootContent = doc->GetRootElement(); nsCOMPtr rootNode(do_QueryInterface(rootContent)); if (!rootNode) return NS_ERROR_FAILURE; if (!mSearchRange) { mSearchRange = new nsRange(doc); } nsCOMPtr searchRootNode = rootNode; // Hack for XMLPrettyPrinter. nsFind can't handle complex anonymous content. // If the root node has an XBL binding then there's not much we can do in // in general, but we can try searching the binding's first child, which // in the case of XMLPrettyPrinter contains the visible pretty-printed // content. nsXBLBinding* binding = rootContent->GetXBLBinding(); if (binding) { nsIContent* anonContent = binding->GetAnonymousContent(); if (anonContent) { searchRootNode = do_QueryInterface(anonContent->GetFirstChild()); } } mSearchRange->SelectNodeContents(searchRootNode); if (!mStartPointRange) { mStartPointRange = new nsRange(doc); } mStartPointRange->SetStart(searchRootNode, 0); mStartPointRange->Collapse(true); // collapse to start if (!mEndPointRange) { mEndPointRange = new nsRange(doc); } nsCOMPtr searchRootTmp = do_QueryInterface(searchRootNode); mEndPointRange->SetEnd(searchRootNode, searchRootTmp->Length()); mEndPointRange->Collapse(false); // collapse to end // Consider current selection as null if // it's not in the currently focused document nsCOMPtr currentSelectionRange; nsCOMPtr selectionPresShell = GetPresShell(); if (aSelectionController && selectionPresShell && selectionPresShell == presShell) { nsCOMPtr selection; aSelectionController->GetSelection( nsISelectionController::SELECTION_NORMAL, getter_AddRefs(selection)); if (selection) selection->GetRangeAt(0, getter_AddRefs(currentSelectionRange)); } if (!currentSelectionRange) { // Ensure visible range, move forward if necessary // This uses ignores the return value, but usese the side effect of // IsRangeVisible. It returns the first visible range after searchRange IsRangeVisible(presShell, presContext, mSearchRange, aIsFirstVisiblePreferred, true, getter_AddRefs(mStartPointRange), nullptr); } else { int32_t startOffset; nsCOMPtr startNode; if (aFindPrev) { currentSelectionRange->GetStartContainer(getter_AddRefs(startNode)); currentSelectionRange->GetStartOffset(&startOffset); } else { currentSelectionRange->GetEndContainer(getter_AddRefs(startNode)); currentSelectionRange->GetEndOffset(&startOffset); } if (!startNode) startNode = rootNode; // We need to set the start point this way, other methods haven't worked mStartPointRange->SelectNode(startNode); mStartPointRange->SetStart(startNode, startOffset); } mStartPointRange->Collapse(true); // collapse to start presShell.forget(aPresShell); presContext.forget(aPresContext); return NS_OK; } void nsTypeAheadFind::RangeStartsInsideLink(nsIDOMRange *aRange, nsIPresShell *aPresShell, bool *aIsInsideLink, bool *aIsStartingLink) { *aIsInsideLink = false; *aIsStartingLink = true; // ------- Get nsIContent to test ------- nsCOMPtr startNode; nsCOMPtr startContent, origContent; aRange->GetStartContainer(getter_AddRefs(startNode)); int32_t startOffset; aRange->GetStartOffset(&startOffset); startContent = do_QueryInterface(startNode); if (!startContent) { NS_NOTREACHED("startContent should never be null"); return; } origContent = startContent; if (startContent->IsElement()) { nsIContent *childContent = startContent->GetChildAt(startOffset); if (childContent) { startContent = childContent; } } else if (startOffset > 0) { const nsTextFragment *textFrag = startContent->GetText(); if (textFrag) { // look for non whitespace character before start offset for (int32_t index = 0; index < startOffset; index++) { // FIXME: take content language into account when deciding whitespace. if (!mozilla::dom::IsSpaceCharacter(textFrag->CharAt(index))) { *aIsStartingLink = false; // not at start of a node break; } } } } // ------- Check to see if inside link --------- // We now have the correct start node for the range // Search for links, starting with startNode, and going up parent chain nsCOMPtr tag, hrefAtom(do_GetAtom("href")); nsCOMPtr typeAtom(do_GetAtom("type")); while (true) { // Keep testing while startContent is equal to something, // eventually we'll run out of ancestors if (startContent->IsHTMLElement()) { nsCOMPtr link(do_QueryInterface(startContent)); if (link) { // Check to see if inside HTML link *aIsInsideLink = startContent->HasAttr(kNameSpaceID_None, hrefAtom); return; } } else { // Any xml element can be an xlink *aIsInsideLink = startContent->HasAttr(kNameSpaceID_XLink, hrefAtom); if (*aIsInsideLink) { if (!startContent->AttrValueIs(kNameSpaceID_XLink, typeAtom, NS_LITERAL_STRING("simple"), eCaseMatters)) { *aIsInsideLink = false; // Xlink must be type="simple" } return; } } // Get the parent nsCOMPtr parent = startContent->GetParent(); if (!parent) break; nsIContent* parentsFirstChild = parent->GetFirstChild(); // We don't want to look at a whitespace-only first child if (parentsFirstChild && parentsFirstChild->TextIsOnlyWhitespace()) { parentsFirstChild = parentsFirstChild->GetNextSibling(); } if (parentsFirstChild != startContent) { // startContent wasn't a first child, so we conclude that // if this is inside a link, it's not at the beginning of it *aIsStartingLink = false; } startContent = parent; } *aIsStartingLink = false; } /* Find another match in the page. */ NS_IMETHODIMP nsTypeAheadFind::FindAgain(bool aFindBackwards, bool aLinksOnly, uint16_t* aResult) { *aResult = FIND_NOTFOUND; if (!mTypeAheadBuffer.IsEmpty()) // Beware! This may flush notifications via synchronous // ScrollSelectionIntoView. FindItNow(nullptr, aLinksOnly, false, aFindBackwards, aResult); return NS_OK; } NS_IMETHODIMP nsTypeAheadFind::Find(const nsAString& aSearchString, bool aLinksOnly, uint16_t* aResult) { *aResult = FIND_NOTFOUND; nsCOMPtr presShell (GetPresShell()); if (!presShell) { nsCOMPtr ds (do_QueryReferent(mDocShell)); NS_ENSURE_TRUE(ds, NS_ERROR_FAILURE); presShell = ds->GetPresShell(); NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE); mPresShell = do_GetWeakReference(presShell); } nsCOMPtr selection; nsCOMPtr selectionController = do_QueryReferent(mSelectionController); if (!selectionController) { GetSelection(presShell, getter_AddRefs(selectionController), getter_AddRefs(selection)); // cache for reuse mSelectionController = do_GetWeakReference(selectionController); } else { selectionController->GetSelection( nsISelectionController::SELECTION_NORMAL, getter_AddRefs(selection)); } if (selection) selection->CollapseToStart(); if (aSearchString.IsEmpty()) { mTypeAheadBuffer.Truncate(); // These will be initialized to their true values after the first character // is typed mStartFindRange = nullptr; mSelectionController = nullptr; *aResult = FIND_FOUND; return NS_OK; } bool atEnd = false; if (mTypeAheadBuffer.Length()) { const nsAString& oldStr = Substring(mTypeAheadBuffer, 0, mTypeAheadBuffer.Length()); const nsAString& newStr = Substring(aSearchString, 0, mTypeAheadBuffer.Length()); if (oldStr.Equals(newStr)) atEnd = true; const nsAString& newStr2 = Substring(aSearchString, 0, aSearchString.Length()); const nsAString& oldStr2 = Substring(mTypeAheadBuffer, 0, aSearchString.Length()); if (oldStr2.Equals(newStr2)) atEnd = true; if (!atEnd) mStartFindRange = nullptr; } if (!mIsSoundInitialized && !mNotFoundSoundURL.IsEmpty()) { // This makes sure system sound library is loaded so that // there's no lag before the first sound is played // by waiting for the first keystroke, we still get the startup time benefits. mIsSoundInitialized = true; mSoundInterface = do_CreateInstance("@mozilla.org/sound;1"); if (mSoundInterface && !mNotFoundSoundURL.EqualsLiteral("beep")) { mSoundInterface->Init(); } } #ifdef XP_WIN // After each keystroke, ensure sound object is destroyed, to free up memory // allocated for error sound, otherwise Windows' nsISound impl // holds onto the last played sound, using up memory. mSoundInterface = nullptr; #endif int32_t bufferLength = mTypeAheadBuffer.Length(); mTypeAheadBuffer = aSearchString; bool isFirstVisiblePreferred = false; // --------- Initialize find if 1st char ---------- if (bufferLength == 0) { // If you can see the selection (not collapsed or thru caret browsing), // or if already focused on a page element, start there. // Otherwise we're going to start at the first visible element bool isSelectionCollapsed = true; if (selection) selection->GetIsCollapsed(&isSelectionCollapsed); // If true, we will scan from top left of visible area // If false, we will scan from start of selection isFirstVisiblePreferred = !atEnd && !mCaretBrowsingOn && isSelectionCollapsed; if (isFirstVisiblePreferred) { // Get the focused content. If there is a focused node, ensure the // selection is at that point. Otherwise, we will just want to start // from the caret position or the beginning of the document. nsPresContext* presContext = presShell->GetPresContext(); NS_ENSURE_TRUE(presContext, NS_OK); nsCOMPtr document = do_QueryInterface(presShell->GetDocument()); if (!document) return NS_ERROR_UNEXPECTED; nsCOMPtr window = do_QueryInterface(document->GetWindow()); nsCOMPtr fm = do_GetService(FOCUSMANAGER_CONTRACTID); if (fm) { nsCOMPtr focusedElement; nsCOMPtr focusedWindow; fm->GetFocusedElementForWindow(window, false, getter_AddRefs(focusedWindow), getter_AddRefs(focusedElement)); // If the root element is focused, then it's actually the document // that has the focus, so ignore this. if (focusedElement && !SameCOMIdentity(focusedElement, document->GetRootElement())) { fm->MoveCaretToFocus(window); isFirstVisiblePreferred = false; } } } } // ----------- Find the text! --------------------- // Beware! This may flush notifications via synchronous // ScrollSelectionIntoView. nsresult rv = FindItNow(nullptr, aLinksOnly, isFirstVisiblePreferred, false, aResult); // ---------Handle success or failure --------------- if (NS_SUCCEEDED(rv)) { if (mTypeAheadBuffer.Length() == 1) { // If first letter, store where the first find succeeded // (mStartFindRange) mStartFindRange = nullptr; if (selection) { nsCOMPtr startFindRange; selection->GetRangeAt(0, getter_AddRefs(startFindRange)); if (startFindRange) startFindRange->CloneRange(getter_AddRefs(mStartFindRange)); } } } else { // Error sound if (mTypeAheadBuffer.Length() > mLastFindLength) PlayNotFoundSound(); } SaveFind(); return NS_OK; } void nsTypeAheadFind::GetSelection(nsIPresShell *aPresShell, nsISelectionController **aSelCon, nsISelection **aDOMSel) { if (!aPresShell) return; // if aCurrentNode is nullptr, get selection for document *aDOMSel = nullptr; nsPresContext* presContext = aPresShell->GetPresContext(); nsIFrame *frame = aPresShell->GetRootFrame(); if (presContext && frame) { frame->GetSelectionController(presContext, aSelCon); if (*aSelCon) { (*aSelCon)->GetSelection(nsISelectionController::SELECTION_NORMAL, aDOMSel); } } } NS_IMETHODIMP nsTypeAheadFind::GetFoundRange(nsIDOMRange** aFoundRange) { NS_ENSURE_ARG_POINTER(aFoundRange); if (mFoundRange == nullptr) { *aFoundRange = nullptr; return NS_OK; } mFoundRange->CloneRange(aFoundRange); return NS_OK; } NS_IMETHODIMP nsTypeAheadFind::IsRangeVisible(nsIDOMRange *aRange, bool aMustBeInViewPort, bool *aResult) { // Jump through hoops to extract the docShell from the range. nsCOMPtr node; aRange->GetStartContainer(getter_AddRefs(node)); nsCOMPtr document; node->GetOwnerDocument(getter_AddRefs(document)); nsCOMPtr window; document->GetDefaultView(getter_AddRefs(window)); nsCOMPtr navNav (do_GetInterface(window)); nsCOMPtr docShell (do_GetInterface(navNav)); // Set up the arguments needed to check if a range is visible. nsCOMPtr presShell (docShell->GetPresShell()); RefPtr presContext = presShell->GetPresContext(); nsCOMPtr startPointRange = new nsRange(presShell->GetDocument()); *aResult = IsRangeVisible(presShell, presContext, aRange, aMustBeInViewPort, false, getter_AddRefs(startPointRange), nullptr); return NS_OK; } bool nsTypeAheadFind::IsRangeVisible(nsIPresShell *aPresShell, nsPresContext *aPresContext, nsIDOMRange *aRange, bool aMustBeInViewPort, bool aGetTopVisibleLeaf, nsIDOMRange **aFirstVisibleRange, bool *aUsesIndependentSelection) { NS_ASSERTION(aPresShell && aPresContext && aRange && aFirstVisibleRange, "params are invalid"); // We need to know if the range start is visible. // Otherwise, return the first visible range start // in aFirstVisibleRange aRange->CloneRange(aFirstVisibleRange); nsCOMPtr node; aRange->GetStartContainer(getter_AddRefs(node)); nsCOMPtr content(do_QueryInterface(node)); if (!content) return false; nsIFrame *frame = content->GetPrimaryFrame(); if (!frame) return false; // No frame! Not visible then. if (!frame->StyleVisibility()->IsVisible()) return false; // Detect if we are _inside_ a text control, or something else with its own // selection controller. if (aUsesIndependentSelection) { *aUsesIndependentSelection = (frame->GetStateBits() & NS_FRAME_INDEPENDENT_SELECTION); } // ---- We have a frame ---- if (!aMustBeInViewPort) return true; // Don't need it to be on screen, just in rendering tree // Get the next in flow frame that contains the range start int32_t startRangeOffset, startFrameOffset, endFrameOffset; aRange->GetStartOffset(&startRangeOffset); while (true) { frame->GetOffsets(startFrameOffset, endFrameOffset); if (startRangeOffset < endFrameOffset) break; nsIFrame *nextContinuationFrame = frame->GetNextContinuation(); if (nextContinuationFrame) frame = nextContinuationFrame; else break; } // Set up the variables we need, return true if we can't get at them all const uint16_t kMinPixels = 12; nscoord minDistance = nsPresContext::CSSPixelsToAppUnits(kMinPixels); // Get the bounds of the current frame, relative to the current view. // We don't use the more accurate AccGetBounds, because that is // more expensive and the STATE_OFFSCREEN flag that this is used // for only needs to be a rough indicator nsRectVisibility rectVisibility = nsRectVisibility_kAboveViewport; if (!aGetTopVisibleLeaf && !frame->GetRect().IsEmpty()) { rectVisibility = aPresShell->GetRectVisibility(frame, nsRect(nsPoint(0,0), frame->GetSize()), minDistance); if (rectVisibility != nsRectVisibility_kAboveViewport) { return true; } } // We know that the target range isn't usable because it's not in the // view port. Move range forward to first visible point, // this speeds us up a lot in long documents nsCOMPtr frameTraversal; nsCOMPtr trav(do_CreateInstance(kFrameTraversalCID)); if (trav) trav->NewFrameTraversal(getter_AddRefs(frameTraversal), aPresContext, frame, eLeaf, false, // aVisual false, // aLockInScrollView false, // aFollowOOFs false // aSkipPopupChecks ); if (!frameTraversal) return false; while (rectVisibility == nsRectVisibility_kAboveViewport) { frameTraversal->Next(); frame = frameTraversal->CurrentItem(); if (!frame) return false; if (!frame->GetRect().IsEmpty()) { rectVisibility = aPresShell->GetRectVisibility(frame, nsRect(nsPoint(0,0), frame->GetSize()), minDistance); } } if (frame) { nsCOMPtr firstVisibleNode = do_QueryInterface(frame->GetContent()); if (firstVisibleNode) { frame->GetOffsets(startFrameOffset, endFrameOffset); (*aFirstVisibleRange)->SetStart(firstVisibleNode, startFrameOffset); (*aFirstVisibleRange)->SetEnd(firstVisibleNode, endFrameOffset); } } return false; } already_AddRefed nsTypeAheadFind::GetPresShell() { if (!mPresShell) return nullptr; nsCOMPtr shell = do_QueryReferent(mPresShell); if (shell) { nsPresContext *pc = shell->GetPresContext(); if (!pc || !pc->GetContainerWeak()) { return nullptr; } } return shell.forget(); }