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