summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/FinderParent.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/FinderParent.sys.mjs')
-rw-r--r--toolkit/modules/FinderParent.sys.mjs654
1 files changed, 654 insertions, 0 deletions
diff --git a/toolkit/modules/FinderParent.sys.mjs b/toolkit/modules/FinderParent.sys.mjs
new file mode 100644
index 0000000000..8c8437f5e9
--- /dev/null
+++ b/toolkit/modules/FinderParent.sys.mjs
@@ -0,0 +1,654 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+// vim: set ts=2 sw=2 sts=2 et tw=80: */
+// 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/.
+
+const kModalHighlightPref = "findbar.modalHighlight";
+const kSoundEnabledPref = "accessibility.typeaheadfind.enablesound";
+const kNotFoundSoundPref = "accessibility.typeaheadfind.soundURL";
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ GetClipboardSearchString: "resource://gre/modules/Finder.sys.mjs",
+ RFPHelper: "resource://gre/modules/RFPHelper.sys.mjs",
+ Rect: "resource://gre/modules/Geometry.sys.mjs",
+});
+
+const kPrefLetterboxing = "privacy.resistFingerprinting.letterboxing";
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "isLetterboxingEnabled",
+ kPrefLetterboxing,
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "isSoundEnabled",
+ kSoundEnabledPref,
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "notFoundSoundURL",
+ kNotFoundSoundPref,
+ ""
+);
+
+export function FinderParent(browser) {
+ this._listeners = new Set();
+ this._searchString = "";
+ this._foundSearchString = null;
+ this._lastFoundBrowsingContext = null;
+
+ // The correct states of these will be updated when the findbar is opened.
+ this._caseSensitive = false;
+ this._entireWord = false;
+ this._matchDiacritics = false;
+
+ this.swapBrowser(browser);
+}
+
+let gSound = null;
+
+FinderParent.prototype = {
+ get browsingContext() {
+ return this._browser.browsingContext;
+ },
+
+ get useRemoteSubframes() {
+ return this._browser.ownerGlobal.docShell.nsILoadContext.useRemoteSubframes;
+ },
+
+ swapBrowser(aBrowser) {
+ this._browser = aBrowser;
+ // Ideally listeners would have removed themselves but that doesn't happen
+ // right now
+ this._listeners.clear();
+ },
+
+ addResultListener(aListener) {
+ this._listeners.add(aListener);
+ },
+
+ removeResultListener(aListener) {
+ this._listeners.delete(aListener);
+ },
+
+ callListeners(aCallback, aArgs) {
+ for (let l of this._listeners) {
+ // Don't let one callback throwing stop us calling the rest
+ try {
+ l[aCallback].apply(l, aArgs);
+ } catch (e) {
+ if (!l[aCallback]) {
+ console.error(
+ `Missing ${aCallback} callback on RemoteFinderListener`
+ );
+ } else {
+ console.error(e);
+ }
+ }
+ }
+ },
+
+ getLastFoundBrowsingContext(aList) {
+ // If a search was already performed, returned the last
+ // browsing context where the result was found. However,
+ // ensure that this browsing context is still valid, and
+ // if not, return null.
+ if (
+ aList.includes(this._lastFoundBrowsingContext) &&
+ !this._lastFoundBrowsingContext.isUnderHiddenEmbedderElement
+ ) {
+ return this._lastFoundBrowsingContext;
+ }
+
+ this._lastFoundBrowsingContext = null;
+ return null;
+ },
+
+ sendMessageToContext(aMessageName, aArgs = {}) {
+ // If there is a last found browsing context, use that. Otherwise,
+ // use the top-level browsing context.
+ let browsingContext = null;
+ if (this._lastFoundBrowsingContext) {
+ let list = this.gatherBrowsingContexts(this.browsingContext);
+ let lastBrowsingContext = this.getLastFoundBrowsingContext(list);
+ if (lastBrowsingContext) {
+ browsingContext = lastBrowsingContext;
+ }
+ }
+
+ if (!browsingContext) {
+ browsingContext = this.browsingContext;
+ }
+
+ let windowGlobal = browsingContext.currentWindowGlobal;
+ if (windowGlobal) {
+ let actor = windowGlobal.getActor("Finder");
+ actor.sendAsyncMessage(aMessageName, aArgs);
+ }
+ },
+
+ sendQueryToContext(aMessageName, aArgs, aBrowsingContext) {
+ let windowGlobal = aBrowsingContext.currentWindowGlobal;
+ if (windowGlobal) {
+ let actor = windowGlobal.getActor("Finder");
+ return actor.sendQuery(aMessageName, aArgs).then(
+ result => result,
+ r => {}
+ );
+ }
+
+ return Promise.resolve({});
+ },
+
+ sendMessageToAllContexts(aMessageName, aArgs = {}) {
+ let list = this.gatherBrowsingContexts(this.browsingContext);
+ for (let browsingContext of list) {
+ let windowGlobal = browsingContext.currentWindowGlobal;
+ if (windowGlobal) {
+ let actor = windowGlobal.getActor("Finder");
+ actor.sendAsyncMessage(aMessageName, aArgs);
+ }
+ }
+ },
+
+ gatherBrowsingContexts(aBrowsingContext) {
+ if (aBrowsingContext.isUnderHiddenEmbedderElement) {
+ return [];
+ }
+
+ let list = [aBrowsingContext];
+
+ for (let child of aBrowsingContext.children) {
+ list.push(...this.gatherBrowsingContexts(child));
+ }
+
+ return list;
+ },
+
+ // If the modal highlighter is on, and there are no out-of-process child
+ // frames, send a message only to the top-level frame and set the useSubFrames
+ // flag, so that the finder iterator iterates over subframes. If there is
+ // an out-of-process subframe, modal highlighting is disabled.
+ needSubFrameSearch(aList) {
+ let useSubFrames = false;
+
+ let useModalHighlighter = Services.prefs.getBoolPref(kModalHighlightPref);
+ let hasOutOfProcessChild = false;
+ if (useModalHighlighter) {
+ if (this.useRemoteSubframes) {
+ return false;
+ }
+
+ for (let browsingContext of aList) {
+ if (
+ browsingContext != this.browsingContext &&
+ browsingContext.currentWindowGlobal.isProcessRoot
+ ) {
+ hasOutOfProcessChild = true;
+ }
+ }
+
+ if (!hasOutOfProcessChild) {
+ aList.splice(0);
+ aList.push(this.browsingContext);
+ useSubFrames = true;
+ }
+ }
+
+ return useSubFrames;
+ },
+
+ onResultFound(aResponse) {
+ this._foundSearchString = aResponse.searchString;
+ // The rect stops being a Geometry.sys.mjs:Rect over IPC.
+ if (aResponse.rect) {
+ aResponse.rect = lazy.Rect.fromRect(aResponse.rect);
+ }
+
+ this.callListeners("onFindResult", [aResponse]);
+ },
+
+ get searchString() {
+ return this._foundSearchString;
+ },
+
+ get clipboardSearchString() {
+ return lazy.GetClipboardSearchString(this._browser.loadContext);
+ },
+
+ set caseSensitive(aSensitive) {
+ this._caseSensitive = aSensitive;
+ this.sendMessageToAllContexts("Finder:CaseSensitive", {
+ caseSensitive: aSensitive,
+ });
+ },
+
+ set entireWord(aEntireWord) {
+ this._entireWord = aEntireWord;
+ this.sendMessageToAllContexts("Finder:EntireWord", {
+ entireWord: aEntireWord,
+ });
+ },
+
+ set matchDiacritics(aMatchDiacritics) {
+ this._matchDiacritics = aMatchDiacritics;
+ this.sendMessageToAllContexts("Finder:MatchDiacritics", {
+ matchDiacritics: aMatchDiacritics,
+ });
+ },
+
+ async setSearchStringToSelection() {
+ return this.setToSelection("Finder:SetSearchStringToSelection", false);
+ },
+
+ async getInitialSelection() {
+ return this.setToSelection("Finder:GetInitialSelection", true);
+ },
+
+ async setToSelection(aMessage, aInitial) {
+ let browsingContext = this.browsingContext;
+
+ // Iterate over focused subframe descendants until one is found
+ // that has the selection.
+ let result;
+ do {
+ result = await this.sendQueryToContext(aMessage, {}, browsingContext);
+ if (!result || !result.focusedChildBrowserContextId) {
+ break;
+ }
+
+ browsingContext = BrowsingContext.get(
+ result.focusedChildBrowserContextId
+ );
+ } while (browsingContext);
+
+ if (result) {
+ this.callListeners("onCurrentSelection", [result.selectedText, aInitial]);
+ }
+
+ return result;
+ },
+
+ async doFind(aFindNext, aArgs) {
+ let rootBC = this.browsingContext;
+ let highlightList = this.gatherBrowsingContexts(rootBC);
+
+ let canPlayNotFoundSound =
+ aArgs.searchString.length > this._searchString.length;
+
+ this._searchString = aArgs.searchString;
+
+ let initialBC = this.getLastFoundBrowsingContext(highlightList);
+ if (!initialBC) {
+ initialBC = rootBC;
+ aFindNext = false;
+ }
+
+ // Make a copy of the list starting from the
+ // browsing context that was last searched from. The original
+ // list will be used for the highlighter where the search
+ // order doesn't matter.
+ let searchList = [];
+ for (let c = 0; c < highlightList.length; c++) {
+ if (highlightList[c] == initialBC) {
+ searchList = highlightList.slice(c);
+ searchList.push(...highlightList.slice(0, c));
+ break;
+ }
+ }
+
+ let mode = Ci.nsITypeAheadFind.FIND_INITIAL;
+ if (aFindNext) {
+ mode = aArgs.findBackwards
+ ? Ci.nsITypeAheadFind.FIND_PREVIOUS
+ : Ci.nsITypeAheadFind.FIND_NEXT;
+ }
+ aArgs.findAgain = aFindNext;
+
+ aArgs.caseSensitive = this._caseSensitive;
+ aArgs.matchDiacritics = this._matchDiacritics;
+ aArgs.entireWord = this._entireWord;
+
+ aArgs.useSubFrames = this.needSubFrameSearch(searchList);
+ if (aArgs.useSubFrames) {
+ // Use the single frame for the highlight list as well.
+ highlightList = searchList;
+ // The typeaheadfind component will play the sound in this case.
+ canPlayNotFoundSound = false;
+ }
+
+ if (canPlayNotFoundSound) {
+ this.initNotFoundSound();
+ }
+
+ // Add the initial browsing context twice to allow looping around.
+ searchList = [...searchList, initialBC];
+
+ if (aArgs.findBackwards) {
+ searchList.reverse();
+ }
+
+ let response = null;
+ let wrapped = false;
+ let foundBC = null;
+
+ for (let c = 0; c < searchList.length; c++) {
+ let currentBC = searchList[c];
+ aArgs.mode = mode;
+
+ // A search has started for a different string, so
+ // ignore further searches of the old string.
+ if (this._searchString != aArgs.searchString) {
+ return;
+ }
+
+ response = await this.sendQueryToContext("Finder:Find", aArgs, currentBC);
+
+ // This can happen if the tab is closed while the find is in progress.
+ if (!response) {
+ break;
+ }
+
+ // If the search term was found, stop iterating.
+ if (response.result != Ci.nsITypeAheadFind.FIND_NOTFOUND) {
+ if (
+ this._lastFoundBrowsingContext &&
+ this._lastFoundBrowsingContext != currentBC
+ ) {
+ // If the new result is in a different frame than the previous result,
+ // clear the result from the old frame. If it is the same frame, the
+ // previous result will be cleared by the find component.
+ this.removeSelection(true);
+ }
+ this._lastFoundBrowsingContext = currentBC;
+
+ // Set the wrapped result flag if needed.
+ if (wrapped) {
+ response.result = Ci.nsITypeAheadFind.FIND_WRAPPED;
+ }
+
+ foundBC = currentBC;
+ break;
+ }
+
+ if (aArgs.findBackwards && currentBC == rootBC) {
+ wrapped = true;
+ } else if (
+ !aArgs.findBackwards &&
+ c + 1 < searchList.length &&
+ searchList[c + 1] == rootBC
+ ) {
+ wrapped = true;
+ }
+
+ mode = aArgs.findBackwards
+ ? Ci.nsITypeAheadFind.FIND_LAST
+ : Ci.nsITypeAheadFind.FIND_FIRST;
+ }
+
+ if (response) {
+ response.useSubFrames = aArgs.useSubFrames;
+ // Update the highlight in all browsing contexts. This needs to happen separately
+ // once it is clear whether a match was found or not.
+ this.updateHighlightAndMatchCount({
+ list: highlightList,
+ message: "Finder:UpdateHighlightAndMatchCount",
+ args: response,
+ foundBrowsingContextId: foundBC ? foundBC.id : -1,
+ doHighlight: true,
+ doMatchCount: true,
+ });
+
+ // Use the last result found.
+ this.onResultFound(response);
+
+ if (
+ canPlayNotFoundSound &&
+ response.result == Ci.nsITypeAheadFind.FIND_NOTFOUND &&
+ !aFindNext &&
+ !response.entireWord
+ ) {
+ this.playNotFoundSound();
+ }
+ }
+ },
+
+ fastFind(aSearchString, aLinksOnly, aDrawOutline) {
+ this.doFind(false, {
+ searchString: aSearchString,
+ findBackwards: false,
+ linksOnly: aLinksOnly,
+ drawOutline: aDrawOutline,
+ });
+ },
+
+ findAgain(aSearchString, aFindBackwards, aLinksOnly, aDrawOutline) {
+ this.doFind(true, {
+ searchString: aSearchString,
+ findBackwards: aFindBackwards,
+ linksOnly: aLinksOnly,
+ drawOutline: aDrawOutline,
+ });
+ },
+
+ highlight(aHighlight, aWord, aLinksOnly) {
+ let list = this.gatherBrowsingContexts(this.browsingContext);
+ let args = {
+ highlight: aHighlight,
+ linksOnly: aLinksOnly,
+ searchString: aWord,
+ };
+
+ args.useSubFrames = this.needSubFrameSearch(list);
+
+ let lastBrowsingContext = this.getLastFoundBrowsingContext(list);
+ this.updateHighlightAndMatchCount({
+ list,
+ message: "Finder:Highlight",
+ args,
+ foundBrowsingContextId: lastBrowsingContext ? lastBrowsingContext.id : -1,
+ doHighlight: true,
+ doMatchCount: false,
+ });
+ },
+
+ requestMatchesCount(aSearchString, aLinksOnly) {
+ let list = this.gatherBrowsingContexts(this.browsingContext);
+ let args = { searchString: aSearchString, linksOnly: aLinksOnly };
+
+ args.useSubFrames = this.needSubFrameSearch(list);
+
+ let lastBrowsingContext = this.getLastFoundBrowsingContext(list);
+ this.updateHighlightAndMatchCount({
+ list,
+ message: "Finder:MatchesCount",
+ args,
+ foundBrowsingContextId: lastBrowsingContext ? lastBrowsingContext.id : -1,
+ doHighlight: false,
+ doMatchCount: true,
+ });
+ },
+
+ updateHighlightAndMatchCount(options) {
+ let promises = [];
+ let found = options.args.result != Ci.nsITypeAheadFind.FIND_NOTFOUND;
+ for (let browsingContext of options.list) {
+ options.args.foundInThisFrame =
+ options.foundBrowsingContextId != -1 &&
+ found &&
+ browsingContext.id == options.foundBrowsingContextId;
+
+ // Don't wait for the result
+ let promise = this.sendQueryToContext(
+ options.message,
+ options.args,
+ browsingContext
+ );
+ promises.push(promise);
+ }
+
+ Promise.all(promises).then(responses => {
+ if (options.doHighlight) {
+ let sendNotification = false;
+ let highlight = false;
+ let found = false;
+ for (let response of responses) {
+ if (!response) {
+ break;
+ }
+
+ sendNotification = true;
+ if (response.found) {
+ found = true;
+ }
+ highlight = response.highlight;
+ }
+
+ if (sendNotification) {
+ this.callListeners("onHighlightFinished", [
+ { searchString: options.args.searchString, highlight, found },
+ ]);
+ }
+ }
+
+ if (options.doMatchCount) {
+ let sendNotification = false;
+ let current = 0;
+ let total = 0;
+ let limit = 0;
+ for (let response of responses) {
+ // A null response can happen if another search was started
+ // and this one became invalid.
+ if (!response || !("total" in response)) {
+ break;
+ }
+
+ sendNotification = true;
+
+ if (
+ options.args.useSubFrames ||
+ (options.foundBrowsingContextId >= 0 &&
+ response.browsingContextId == options.foundBrowsingContextId)
+ ) {
+ current = total + response.current;
+ }
+ total += response.total;
+ limit = response.limit;
+ }
+
+ if (sendNotification) {
+ this.callListeners("onMatchesCountResult", [
+ { searchString: options.args.searchString, current, total, limit },
+ ]);
+ }
+ }
+ });
+ },
+
+ enableSelection() {
+ this.sendMessageToContext("Finder:EnableSelection");
+ },
+
+ removeSelection(aKeepHighlight) {
+ this.sendMessageToContext("Finder:RemoveSelection", {
+ keepHighlight: aKeepHighlight,
+ });
+ },
+
+ focusContent() {
+ // Allow Finder listeners to cancel focusing the content.
+ for (let l of this._listeners) {
+ try {
+ if ("shouldFocusContent" in l && !l.shouldFocusContent()) {
+ return;
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+
+ this._browser.focus();
+ this.sendMessageToContext("Finder:FocusContent");
+ },
+
+ onFindbarClose() {
+ this._lastFoundBrowsingContext = null;
+ this.sendMessageToAllContexts("Finder:FindbarClose");
+
+ if (lazy.isLetterboxingEnabled) {
+ let window = this._browser.ownerGlobal;
+ lazy.RFPHelper.contentSizeUpdated(window);
+ }
+ },
+
+ onFindbarOpen() {
+ this.sendMessageToAllContexts("Finder:FindbarOpen");
+
+ if (lazy.isLetterboxingEnabled) {
+ let window = this._browser.ownerGlobal;
+ lazy.RFPHelper.contentSizeUpdated(window);
+ }
+ },
+
+ onModalHighlightChange(aUseModalHighlight) {
+ this.sendMessageToAllContexts("Finder:ModalHighlightChange", {
+ useModalHighlight: aUseModalHighlight,
+ });
+ },
+
+ onHighlightAllChange(aHighlightAll) {
+ this.sendMessageToAllContexts("Finder:HighlightAllChange", {
+ highlightAll: aHighlightAll,
+ });
+ },
+
+ keyPress(aEvent) {
+ this.sendMessageToContext("Finder:KeyPress", {
+ keyCode: aEvent.keyCode,
+ ctrlKey: aEvent.ctrlKey,
+ metaKey: aEvent.metaKey,
+ altKey: aEvent.altKey,
+ shiftKey: aEvent.shiftKey,
+ });
+ },
+
+ initNotFoundSound() {
+ if (!gSound && lazy.isSoundEnabled && lazy.notFoundSoundURL) {
+ try {
+ gSound = Cc["@mozilla.org/sound;1"].getService(Ci.nsISound);
+ gSound.init();
+ } catch (ex) {}
+ }
+ },
+
+ playNotFoundSound() {
+ if (!lazy.isSoundEnabled || !lazy.notFoundSoundURL) {
+ return;
+ }
+
+ this.initNotFoundSound();
+ if (!gSound) {
+ return;
+ }
+
+ let soundUrl = lazy.notFoundSoundURL;
+ if (soundUrl == "beep") {
+ gSound.beep();
+ } else {
+ if (soundUrl == "default") {
+ soundUrl = "chrome://global/content/notfound.wav";
+ }
+ gSound.play(Services.io.newURI(soundUrl));
+ }
+ },
+};