Bug 1864861: part 2) Remove `aIsPreload` argument from `FontLoaderUtils::BuildChannel...
[gecko.git] / browser / base / content / browser-fullZoom.js
blob63e9586d2b83681fe05436958d7e4c42d34b6996
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 // This file is loaded into the browser window scope.
6 /* eslint-env mozilla/browser-window */
8 /**
9  * Controls the "full zoom" setting and its site-specific preferences.
10  */
11 var FullZoom = {
12   // Identifies the setting in the content prefs database.
13   name: "browser.content.full-zoom",
15   // browser.zoom.siteSpecific preference cache
16   _siteSpecificPref: undefined,
18   // browser.zoom.updateBackgroundTabs preference cache
19   updateBackgroundTabs: undefined,
21   // This maps the browser to monotonically increasing integer
22   // tokens. _browserTokenMap[browser] is increased each time the zoom is
23   // changed in the browser. See _getBrowserToken and _ignorePendingZoomAccesses.
24   _browserTokenMap: new WeakMap(),
26   // Stores initial locations if we receive onLocationChange
27   // events before we're initialized.
28   _initialLocations: new WeakMap(),
30   get siteSpecific() {
31     if (this._siteSpecificPref === undefined) {
32       this._siteSpecificPref = Services.prefs.getBoolPref(
33         "browser.zoom.siteSpecific"
34       );
35     }
36     return this._siteSpecificPref;
37   },
39   // nsISupports
41   QueryInterface: ChromeUtils.generateQI([
42     "nsIObserver",
43     "nsIContentPrefObserver",
44     "nsISupportsWeakReference",
45   ]),
47   // Initialization & Destruction
49   init: function FullZoom_init() {
50     gBrowser.addEventListener("DoZoomEnlargeBy10", this);
51     gBrowser.addEventListener("DoZoomReduceBy10", this);
52     window.addEventListener("MozScaleGestureComplete", this);
54     // Register ourselves with the service so we know when our pref changes.
55     this._cps2 = Cc["@mozilla.org/content-pref/service;1"].getService(
56       Ci.nsIContentPrefService2
57     );
58     this._cps2.addObserverForName(this.name, this);
60     this.updateBackgroundTabs = Services.prefs.getBoolPref(
61       "browser.zoom.updateBackgroundTabs"
62     );
64     // Listen for changes to the browser.zoom branch so we can enable/disable
65     // updating background tabs and per-site saving and restoring of zoom levels.
66     Services.prefs.addObserver("browser.zoom.", this, true);
68     // If we received onLocationChange events for any of the current browsers
69     // before we were initialized we want to replay those upon initialization.
70     for (let browser of gBrowser.browsers) {
71       if (this._initialLocations.has(browser)) {
72         this.onLocationChange(...this._initialLocations.get(browser), browser);
73       }
74     }
76     // This should be nulled after initialization.
77     this._initialLocations = null;
78   },
80   destroy: function FullZoom_destroy() {
81     Services.prefs.removeObserver("browser.zoom.", this);
82     this._cps2.removeObserverForName(this.name, this);
83     gBrowser.removeEventListener("DoZoomEnlargeBy10", this);
84     gBrowser.removeEventListener("DoZoomReduceBy10", this);
85     window.removeEventListener("MozScaleGestureComplete", this);
86   },
88   // Event Handlers
90   // EventListener
92   handleEvent: function FullZoom_handleEvent(event) {
93     switch (event.type) {
94       case "DoZoomEnlargeBy10":
95         this.changeZoomBy(this._getTargetedBrowser(event), 0.1);
96         break;
97       case "DoZoomReduceBy10":
98         this.changeZoomBy(this._getTargetedBrowser(event), -0.1);
99         break;
100       case "MozScaleGestureComplete": {
101         let nonDefaultScalingZoom = event.detail != 1.0;
102         this.updateCommands(nonDefaultScalingZoom);
103         break;
104       }
105     }
106   },
108   // nsIObserver
110   observe(aSubject, aTopic, aData) {
111     switch (aTopic) {
112       case "nsPref:changed":
113         switch (aData) {
114           case "browser.zoom.siteSpecific":
115             // Invalidate pref cache.
116             this._siteSpecificPref = undefined;
117             break;
118           case "browser.zoom.updateBackgroundTabs":
119             this.updateBackgroundTabs = Services.prefs.getBoolPref(
120               "browser.zoom.updateBackgroundTabs"
121             );
122             break;
123           case "browser.zoom.full": {
124             this.updateCommands();
125             break;
126           }
127         }
128         break;
129     }
130   },
132   // nsIContentPrefObserver
134   onContentPrefSet: function FullZoom_onContentPrefSet(
135     aGroup,
136     aName,
137     aValue,
138     aIsPrivate
139   ) {
140     this._onContentPrefChanged(aGroup, aValue, aIsPrivate);
141   },
143   onContentPrefRemoved: function FullZoom_onContentPrefRemoved(
144     aGroup,
145     aName,
146     aIsPrivate
147   ) {
148     this._onContentPrefChanged(aGroup, undefined, aIsPrivate);
149   },
151   /**
152    * Appropriately updates the zoom level after a content preference has
153    * changed.
154    *
155    * @param aGroup  The group of the changed preference.
156    * @param aValue  The new value of the changed preference.  Pass undefined to
157    *                indicate the preference's removal.
158    */
159   _onContentPrefChanged: function FullZoom__onContentPrefChanged(
160     aGroup,
161     aValue,
162     aIsPrivate
163   ) {
164     if (this._isNextContentPrefChangeInternal) {
165       // Ignore changes that FullZoom itself makes.  This works because the
166       // content pref service calls callbacks before notifying observers, and it
167       // does both in the same turn of the event loop.
168       delete this._isNextContentPrefChangeInternal;
169       return;
170     }
172     let browser = gBrowser.selectedBrowser;
173     if (!browser.currentURI) {
174       return;
175     }
177     if (this._isPDFViewer(browser)) {
178       return;
179     }
181     let ctxt = this._loadContextFromBrowser(browser);
182     let domain = this._cps2.extractDomain(browser.currentURI.spec);
183     if (aGroup) {
184       if (aGroup == domain && ctxt.usePrivateBrowsing == aIsPrivate) {
185         this._applyPrefToZoom(aValue, browser);
186       }
187       return;
188     }
190     // If the current page doesn't have a site-specific preference, then its
191     // zoom should be set to the new global preference now that the global
192     // preference has changed.
193     let hasPref = false;
194     let token = this._getBrowserToken(browser);
195     this._cps2.getByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
196       handleResult() {
197         hasPref = true;
198       },
199       handleCompletion: () => {
200         if (!hasPref && token.isCurrent) {
201           this._applyPrefToZoom(undefined, browser);
202         }
203       },
204     });
205   },
207   // location change observer
209   /**
210    * Called when the location of a tab changes.
211    * When that happens, we need to update the current zoom level if appropriate.
212    *
213    * @param aURI
214    *        A URI object representing the new location.
215    * @param aIsTabSwitch
216    *        Whether this location change has happened because of a tab switch.
217    * @param aBrowser
218    *        (optional) browser object displaying the document
219    */
220   onLocationChange: function FullZoom_onLocationChange(
221     aURI,
222     aIsTabSwitch,
223     aBrowser
224   ) {
225     let browser = aBrowser || gBrowser.selectedBrowser;
227     // If we haven't been initialized yet but receive an onLocationChange
228     // notification then let's store and replay it upon initialization.
229     if (this._initialLocations) {
230       this._initialLocations.set(browser, [aURI, aIsTabSwitch]);
231       return;
232     }
234     // Ignore all pending async zoom accesses in the browser.  Pending accesses
235     // that started before the location change will be prevented from applying
236     // to the new location.
237     this._ignorePendingZoomAccesses(browser);
239     if (!aURI || (aIsTabSwitch && !this._isSiteSpecific(browser))) {
240       this._notifyOnLocationChange(browser);
241       return;
242     }
244     if (aURI.spec == "about:blank") {
245       if (
246         !browser.contentPrincipal ||
247         browser.contentPrincipal.isNullPrincipal
248       ) {
249         // For an about:blank with a null principal, zooming any amount does not
250         // make any sense - so simply do 100%.
251         this._applyPrefToZoom(
252           1,
253           browser,
254           this._notifyOnLocationChange.bind(this, browser)
255         );
256       } else {
257         // If it's not a null principal, there may be content loaded into it,
258         // so use the global pref. This will avoid a cps2 roundtrip if we've
259         // already loaded the global pref once. Really, this should probably
260         // use the contentPrincipal's origin if it's an http(s) principal.
261         // (See bug 1457597)
262         this._applyPrefToZoom(
263           undefined,
264           browser,
265           this._notifyOnLocationChange.bind(this, browser)
266         );
267       }
268       return;
269     }
271     // Media documents should always start at 1, and are not affected by prefs.
272     if (!aIsTabSwitch && browser.isSyntheticDocument) {
273       ZoomManager.setZoomForBrowser(browser, 1);
274       // _ignorePendingZoomAccesses already called above, so no need here.
275       this._notifyOnLocationChange(browser);
276       return;
277     }
279     // The PDF viewer zooming isn't handled by `ZoomManager`, ensure that the
280     // browser zoom level always gets reset to 100% on load (to prevent the
281     // UI elements of the PDF viewer from being zoomed in/out on load).
282     if (this._isPDFViewer(browser)) {
283       this._applyPrefToZoom(
284         1,
285         browser,
286         this._notifyOnLocationChange.bind(this, browser)
287       );
288       return;
289     }
291     // See if the zoom pref is cached.
292     let ctxt = this._loadContextFromBrowser(browser);
293     let pref = this._cps2.getCachedByDomainAndName(aURI.spec, this.name, ctxt);
294     if (pref) {
295       this._applyPrefToZoom(
296         pref.value,
297         browser,
298         this._notifyOnLocationChange.bind(this, browser)
299       );
300       return;
301     }
303     // It's not cached, so we have to asynchronously fetch it.
304     let value = undefined;
305     let token = this._getBrowserToken(browser);
306     this._cps2.getByDomainAndName(aURI.spec, this.name, ctxt, {
307       handleResult(resultPref) {
308         value = resultPref.value;
309       },
310       handleCompletion: () => {
311         if (!token.isCurrent) {
312           this._notifyOnLocationChange(browser);
313           return;
314         }
315         this._applyPrefToZoom(
316           value,
317           browser,
318           this._notifyOnLocationChange.bind(this, browser)
319         );
320       },
321     });
322   },
324   // update state of zoom menu items
326   /**
327    * Updates the current windows Zoom commands for zooming in, zooming out
328    * and resetting the zoom level.
329    *
330    * @param {boolean} [forceResetEnabled=false]
331    *   Set to true if the zoom reset command should be enabled regardless of
332    *   whether or not the ZoomManager.zoom level is at 1.0. This is specifically
333    *   for when using scaling zoom via the pinch gesture which doesn't cause
334    *   the ZoomManager.zoom level to change.
335    * @returns Promise
336    * @resolves undefined
337    */
338   updateCommands: async function FullZoom_updateCommands(
339     forceResetEnabled = false
340   ) {
341     let zoomLevel = ZoomManager.zoom;
342     let defaultZoomLevel = await ZoomUI.getGlobalValue();
343     let reduceCmd = document.getElementById("cmd_fullZoomReduce");
344     if (zoomLevel == ZoomManager.MIN) {
345       reduceCmd.setAttribute("disabled", "true");
346     } else {
347       reduceCmd.removeAttribute("disabled");
348     }
350     let enlargeCmd = document.getElementById("cmd_fullZoomEnlarge");
351     if (zoomLevel == ZoomManager.MAX) {
352       enlargeCmd.setAttribute("disabled", "true");
353     } else {
354       enlargeCmd.removeAttribute("disabled");
355     }
357     let resetCmd = document.getElementById("cmd_fullZoomReset");
358     if (zoomLevel == defaultZoomLevel && !forceResetEnabled) {
359       resetCmd.setAttribute("disabled", "true");
360     } else {
361       resetCmd.removeAttribute("disabled");
362     }
364     let fullZoomCmd = document.getElementById("cmd_fullZoomToggle");
365     if (!ZoomManager.useFullZoom) {
366       fullZoomCmd.setAttribute("checked", "true");
367     } else {
368       fullZoomCmd.setAttribute("checked", "false");
369     }
370   },
372   // Setting & Pref Manipulation
374   sendMessageToPDFViewer(browser, name) {
375     try {
376       browser.sendMessageToActor(name, {}, "Pdfjs");
377     } catch (ex) {
378       console.error(ex);
379     }
380   },
382   /**
383    * If browser in reader mode sends message to reader in order to decrease font size,
384    * Otherwise reduces the zoom level of the page in the current browser.
385    */
386   async reduce() {
387     let browser = gBrowser.selectedBrowser;
388     if (browser.currentURI.spec.startsWith("about:reader")) {
389       browser.sendMessageToActor("Reader:ZoomOut", {}, "AboutReader");
390     } else if (this._isPDFViewer(browser)) {
391       this.sendMessageToPDFViewer(browser, "PDFJS:ZoomOut");
392     } else {
393       ZoomManager.reduce();
394       this._ignorePendingZoomAccesses(browser);
395       await this._applyZoomToPref(browser);
396     }
397   },
399   /**
400    * If browser in reader mode sends message to reader in order to increase font size,
401    * Otherwise enlarges the zoom level of the page in the current browser.
402    */
403   async enlarge() {
404     let browser = gBrowser.selectedBrowser;
405     if (browser.currentURI.spec.startsWith("about:reader")) {
406       browser.sendMessageToActor("Reader:ZoomIn", {}, "AboutReader");
407     } else if (this._isPDFViewer(browser)) {
408       this.sendMessageToPDFViewer(browser, "PDFJS:ZoomIn");
409     } else {
410       ZoomManager.enlarge();
411       this._ignorePendingZoomAccesses(browser);
412       await this._applyZoomToPref(browser);
413     }
414   },
416   /**
417    * If browser in reader mode sends message to reader in order to increase font size,
418    * Otherwise enlarges the zoom level of the page in the current browser.
419    * This function is not async like reduce/enlarge, because it is invoked by our
420    * event handler. This means that the call to _applyZoomToPref is not awaited and
421    * will happen asynchronously.
422    */
423   changeZoomBy(aBrowser, aValue) {
424     if (aBrowser.currentURI.spec.startsWith("about:reader")) {
425       const message = aValue > 0 ? "Reader::ZoomIn" : "Reader:ZoomOut";
426       aBrowser.sendMessageToActor(message, {}, "AboutReader");
427       return;
428     } else if (this._isPDFViewer(aBrowser)) {
429       const message = aValue > 0 ? "PDFJS::ZoomIn" : "PDFJS:ZoomOut";
430       this.sendMessageToPDFViewer(aBrowser, message);
431       return;
432     }
433     let zoom = ZoomManager.getZoomForBrowser(aBrowser);
434     zoom += aValue;
435     if (zoom < ZoomManager.MIN) {
436       zoom = ZoomManager.MIN;
437     } else if (zoom > ZoomManager.MAX) {
438       zoom = ZoomManager.MAX;
439     }
440     ZoomManager.setZoomForBrowser(aBrowser, zoom);
441     this._ignorePendingZoomAccesses(aBrowser);
442     this._applyZoomToPref(aBrowser);
443   },
445   /**
446    * Sets the zoom level for the given browser to the given floating
447    * point value, where 1 is the default zoom level.
448    */
449   setZoom(value, browser = gBrowser.selectedBrowser) {
450     if (this._isPDFViewer(browser)) {
451       return;
452     }
453     ZoomManager.setZoomForBrowser(browser, value);
454     this._ignorePendingZoomAccesses(browser);
455     this._applyZoomToPref(browser);
456   },
458   /**
459    * Sets the zoom level of the page in the given browser to the global zoom
460    * level.
461    *
462    * @return A promise which resolves when the zoom reset has been applied.
463    */
464   reset: function FullZoom_reset(browser = gBrowser.selectedBrowser) {
465     let forceValue;
466     if (browser.currentURI.spec.startsWith("about:reader")) {
467       browser.sendMessageToActor("Reader:ResetZoom", {}, "AboutReader");
468     } else if (this._isPDFViewer(browser)) {
469       this.sendMessageToPDFViewer(browser, "PDFJS:ZoomReset");
470       // Ensure that the UI elements of the PDF viewer won't be zoomed in/out
471       // on reset, even if/when browser default zoom value is not set to 100%.
472       forceValue = 1;
473     }
474     let token = this._getBrowserToken(browser);
475     let result = ZoomUI.getGlobalValue().then(value => {
476       if (token.isCurrent) {
477         ZoomManager.setZoomForBrowser(browser, forceValue || value);
478         this._ignorePendingZoomAccesses(browser);
479       }
480     });
481     this._removePref(browser);
482     return result;
483   },
485   /**
486    * Called from the URL bar's inline zoom reset indicator button.
487    *
488    * @param {Event} event the click/keyboard event that triggered the call.
489    */
490   resetFromURLBar(event) {
491     if (event.button > 0) {
492       return;
493     }
494     this.reset();
495     this.resetScalingZoom();
496   },
498   resetScalingZoom: function FullZoom_resetScaling(
499     browser = gBrowser.selectedBrowser
500   ) {
501     browser.browsingContext?.resetScalingZoom();
502   },
504   /**
505    * Set the zoom level for a given browser.
506    *
507    * Per nsPresContext::setFullZoom, we can set the zoom to its current value
508    * without significant impact on performance, as the setting is only applied
509    * if it differs from the current setting.  In fact getting the zoom and then
510    * checking ourselves if it differs costs more.
511    *
512    * And perhaps we should always set the zoom even if it was more expensive,
513    * since nsDocumentViewer::SetTextZoom claims that child documents can have
514    * a different text zoom (although it would be unusual), and it implies that
515    * those child text zooms should get updated when the parent zoom gets set,
516    * and perhaps the same is true for full zoom
517    * (although nsDocumentViewer::SetFullZoom doesn't mention it).
518    *
519    * So when we apply new zoom values to the browser, we simply set the zoom.
520    * We don't check first to see if the new value is the same as the current
521    * one.
522    *
523    * @param aValue     The zoom level value.
524    * @param aBrowser   The zoom is set in this browser.  Required.
525    * @param aCallback  If given, it's asynchronously called when complete.
526    */
527   _applyPrefToZoom: function FullZoom__applyPrefToZoom(
528     aValue,
529     aBrowser,
530     aCallback
531   ) {
532     // The browser is sometimes half-destroyed because this method is called
533     // by content pref service callbacks, which themselves can be called at any
534     // time, even after browsers are closed.
535     if (
536       !aBrowser.mInitialized ||
537       aBrowser.isSyntheticDocument ||
538       (!this._isSiteSpecific(aBrowser) && aBrowser.tabHasCustomZoom)
539     ) {
540       this._executeSoon(aCallback);
541       return;
542     }
544     if (aValue !== undefined && this._isSiteSpecific(aBrowser)) {
545       ZoomManager.setZoomForBrowser(aBrowser, this._ensureValid(aValue));
546       this._ignorePendingZoomAccesses(aBrowser);
547       this._executeSoon(aCallback);
548       return;
549     }
551     // Above, we check if site-specific zoom is enabled before setting
552     // the tab browser zoom, however global zoom should work independent
553     // of the site-specific pref, so we do no checks here.
554     let token = this._getBrowserToken(aBrowser);
555     ZoomUI.getGlobalValue().then(value => {
556       if (token.isCurrent) {
557         ZoomManager.setZoomForBrowser(aBrowser, value);
558         this._ignorePendingZoomAccesses(aBrowser);
559       }
560       this._executeSoon(aCallback);
561     });
562   },
564   /**
565    * Saves the zoom level of the page in the given browser to the content
566    * prefs store.
567    *
568    * @param browser  The zoom of this browser will be saved.  Required.
569    */
570   _applyZoomToPref: function FullZoom__applyZoomToPref(browser) {
571     if (!this._isSiteSpecific(browser) || browser.isSyntheticDocument) {
572       // If site-specific zoom is disabled, we have called this function
573       // to adjust our tab's zoom level. It is now considered "custom"
574       // and we mark that here.
575       browser.tabHasCustomZoom = !this._isSiteSpecific(browser);
576       return null;
577     }
579     return new Promise(resolve => {
580       this._cps2.set(
581         browser.currentURI.spec,
582         this.name,
583         ZoomManager.getZoomForBrowser(browser),
584         this._loadContextFromBrowser(browser),
585         {
586           handleCompletion: () => {
587             this._isNextContentPrefChangeInternal = true;
588             resolve();
589           },
590         }
591       );
592     });
593   },
595   /**
596    * Removes from the content prefs store the zoom level of the given browser.
597    *
598    * @param browser  The zoom of this browser will be removed.  Required.
599    */
600   _removePref: function FullZoom__removePref(browser) {
601     if (browser.isSyntheticDocument) {
602       return;
603     }
604     let ctxt = this._loadContextFromBrowser(browser);
605     this._cps2.removeByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
606       handleCompletion: () => {
607         this._isNextContentPrefChangeInternal = true;
608       },
609     });
610   },
612   // Utilities
614   /**
615    * Returns the zoom change token of the given browser.  Asynchronous
616    * operations that access the given browser's zoom should use this method to
617    * capture the token before starting and use token.isCurrent to determine if
618    * it's safe to access the zoom when done.  If token.isCurrent is false, then
619    * after the async operation started, either the browser's zoom was changed or
620    * the browser was destroyed, and depending on what the operation is doing, it
621    * may no longer be safe to set and get its zoom.
622    *
623    * @param browser  The token of this browser will be returned.
624    * @return  An object with an "isCurrent" getter.
625    */
626   _getBrowserToken: function FullZoom__getBrowserToken(browser) {
627     let map = this._browserTokenMap;
628     if (!map.has(browser)) {
629       map.set(browser, 0);
630     }
631     return {
632       token: map.get(browser),
633       get isCurrent() {
634         // At this point, the browser may have been destructed and unbound but
635         // its outer ID not removed from the map because outer-window-destroyed
636         // hasn't been received yet.  In that case, the browser is unusable, it
637         // has no properties, so return false.  Check for this case by getting a
638         // property, say, docShell.
639         return map.get(browser) === this.token && browser.mInitialized;
640       },
641     };
642   },
644   /**
645    * Returns the browser that the supplied zoom event is associated with.
646    * @param event  The zoom event.
647    * @return  The associated browser element, if one exists, otherwise null.
648    */
649   _getTargetedBrowser: function FullZoom__getTargetedBrowser(event) {
650     let target = event.originalTarget;
652     // With remote content browsers, the event's target is the browser
653     // we're looking for.
654     const XUL_NS =
655       "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
656     if (
657       window.XULElement.isInstance(target) &&
658       target.localName == "browser" &&
659       target.namespaceURI == XUL_NS
660     ) {
661       return target;
662     }
664     // With in-process content browsers, the event's target is the content
665     // document.
666     if (target.nodeType == Node.DOCUMENT_NODE) {
667       return target.ownerGlobal.docShell.chromeEventHandler;
668     }
670     throw new Error("Unexpected zoom event source");
671   },
673   /**
674    * Increments the zoom change token for the given browser so that pending
675    * async operations know that it may be unsafe to access they zoom when they
676    * finish.
677    *
678    * @param browser  Pending accesses in this browser will be ignored.
679    */
680   _ignorePendingZoomAccesses: function FullZoom__ignorePendingZoomAccesses(
681     browser
682   ) {
683     let map = this._browserTokenMap;
684     map.set(browser, (map.get(browser) || 0) + 1);
685   },
687   _ensureValid: function FullZoom__ensureValid(aValue) {
688     // Note that undefined is a valid value for aValue that indicates a known-
689     // not-to-exist value.
690     if (isNaN(aValue)) {
691       return 1;
692     }
694     if (aValue < ZoomManager.MIN) {
695       return ZoomManager.MIN;
696     }
698     if (aValue > ZoomManager.MAX) {
699       return ZoomManager.MAX;
700     }
702     return aValue;
703   },
705   // Whether to remember the site specific zoom level for this browser.
706   // This returns false when `browser.zoom.siteSpecific` is false or
707   // the browser has content loaded that should resist fingerprinting.
708   _isSiteSpecific(aBrowser) {
709     if (!this.siteSpecific) {
710       return false;
711     }
712     return (
713       !aBrowser?.browsingContext?.topWindowContext.shouldResistFingerprinting ||
714       !ChromeUtils.shouldResistFingerprinting(
715         "SiteSpecificZoom",
716         aBrowser?.browsingContext?.topWindowContext
717           .overriddenFingerprintingSettings
718       )
719     );
720   },
722   /**
723    * Gets the load context from the given Browser.
724    *
725    * @param Browser  The Browser whose load context will be returned.
726    * @return        The nsILoadContext of the given Browser.
727    */
728   _loadContextFromBrowser: function FullZoom__loadContextFromBrowser(browser) {
729     return browser.loadContext;
730   },
732   /**
733    * Asynchronously broadcasts "browser-fullZoom:location-change" so that
734    * listeners can be notified when the zoom levels on those pages change.
735    * The notification is always asynchronous so that observers are guaranteed a
736    * consistent behavior.
737    */
738   _notifyOnLocationChange: function FullZoom__notifyOnLocationChange(browser) {
739     this._executeSoon(function () {
740       Services.obs.notifyObservers(browser, "browser-fullZoom:location-change");
741     });
742   },
744   _executeSoon: function FullZoom__executeSoon(callback) {
745     if (!callback) {
746       return;
747     }
748     Services.tm.dispatchToMainThread(callback);
749   },
751   _isPDFViewer(browser) {
752     return !!(
753       browser.contentPrincipal.spec == "resource://pdf.js/web/viewer.html"
754     );
755   },