browser/base/content/browser-fullZoom.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /*
     2 #ifdef 0
     3  * This Source Code Form is subject to the terms of the Mozilla Public
     4  * License, v. 2.0. If a copy of the MPL was not distributed with this
     5  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
     6 #endif
     7  */
     9 /**
    10  * Controls the "full zoom" setting and its site-specific preferences.
    11  */
    12 var FullZoom = {
    13   // Identifies the setting in the content prefs database.
    14   name: "browser.content.full-zoom",
    16   // browser.zoom.siteSpecific preference cache
    17   _siteSpecificPref: undefined,
    19   // browser.zoom.updateBackgroundTabs preference cache
    20   updateBackgroundTabs: undefined,
    22   // One of the possible values for the mousewheel.* preferences.
    23   // From EventStateManager.h.
    24   ACTION_ZOOM: 3,
    26   // This maps the browser to monotonically increasing integer
    27   // tokens. _browserTokenMap[browser] is increased each time the zoom is
    28   // changed in the browser. See _getBrowserToken and _ignorePendingZoomAccesses.
    29   _browserTokenMap: new WeakMap(),
    31   get siteSpecific() {
    32     return this._siteSpecificPref;
    33   },
    35   //**************************************************************************//
    36   // nsISupports
    38   QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
    39                                          Ci.nsIObserver,
    40                                          Ci.nsIContentPrefObserver,
    41                                          Ci.nsISupportsWeakReference,
    42                                          Ci.nsISupports]),
    44   //**************************************************************************//
    45   // Initialization & Destruction
    47   init: function FullZoom_init() {
    48     // Listen for scrollwheel events so we can save scrollwheel-based changes.
    49     window.addEventListener("DOMMouseScroll", this, false);
    51     // Register ourselves with the service so we know when our pref changes.
    52     this._cps2 = Cc["@mozilla.org/content-pref/service;1"].
    53                  getService(Ci.nsIContentPrefService2);
    54     this._cps2.addObserverForName(this.name, this);
    56     this._siteSpecificPref =
    57       gPrefService.getBoolPref("browser.zoom.siteSpecific");
    58     this.updateBackgroundTabs =
    59       gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs");
    60     // Listen for changes to the browser.zoom branch so we can enable/disable
    61     // updating background tabs and per-site saving and restoring of zoom levels.
    62     gPrefService.addObserver("browser.zoom.", this, true);
    63   },
    65   destroy: function FullZoom_destroy() {
    66     gPrefService.removeObserver("browser.zoom.", this);
    67     this._cps2.removeObserverForName(this.name, this);
    68     window.removeEventListener("DOMMouseScroll", this, false);
    69   },
    72   //**************************************************************************//
    73   // Event Handlers
    75   // nsIDOMEventListener
    77   handleEvent: function FullZoom_handleEvent(event) {
    78     switch (event.type) {
    79       case "DOMMouseScroll":
    80         this._handleMouseScrolled(event);
    81         break;
    82     }
    83   },
    85   _handleMouseScrolled: function FullZoom__handleMouseScrolled(event) {
    86     // Construct the "mousewheel action" pref key corresponding to this event.
    87     // Based on EventStateManager::WheelPrefs::GetBasePrefName().
    88     var pref = "mousewheel.";
    90     var pressedModifierCount = event.shiftKey + event.ctrlKey + event.altKey +
    91                                  event.metaKey + event.getModifierState("OS");
    92     if (pressedModifierCount != 1) {
    93       pref += "default.";
    94     } else if (event.shiftKey) {
    95       pref += "with_shift.";
    96     } else if (event.ctrlKey) {
    97       pref += "with_control.";
    98     } else if (event.altKey) {
    99       pref += "with_alt.";
   100     } else if (event.metaKey) {
   101       pref += "with_meta.";
   102     } else {
   103       pref += "with_win.";
   104     }
   106     pref += "action";
   108     // Don't do anything if this isn't a "zoom" scroll event.
   109     var isZoomEvent = false;
   110     try {
   111       isZoomEvent = (gPrefService.getIntPref(pref) == this.ACTION_ZOOM);
   112     } catch (e) {}
   113     if (!isZoomEvent)
   114       return;
   116     // XXX Lazily cache all the possible action prefs so we don't have to get
   117     // them anew from the pref service for every scroll event?  We'd have to
   118     // make sure to observe them so we can update the cache when they change.
   120     // We have to call _applyZoomToPref in a timeout because we handle the
   121     // event before the event state manager has a chance to apply the zoom
   122     // during EventStateManager::PostHandleEvent.
   123     let browser = gBrowser.selectedBrowser;
   124     let token = this._getBrowserToken(browser);
   125     window.setTimeout(function () {
   126       if (token.isCurrent)
   127         this._applyZoomToPref(browser);
   128     }.bind(this), 0);
   129   },
   131   // nsIObserver
   133   observe: function (aSubject, aTopic, aData) {
   134     switch (aTopic) {
   135       case "nsPref:changed":
   136         switch (aData) {
   137           case "browser.zoom.siteSpecific":
   138             this._siteSpecificPref =
   139               gPrefService.getBoolPref("browser.zoom.siteSpecific");
   140             break;
   141           case "browser.zoom.updateBackgroundTabs":
   142             this.updateBackgroundTabs =
   143               gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs");
   144             break;
   145         }
   146         break;
   147     }
   148   },
   150   // nsIContentPrefObserver
   152   onContentPrefSet: function FullZoom_onContentPrefSet(aGroup, aName, aValue) {
   153     this._onContentPrefChanged(aGroup, aValue);
   154   },
   156   onContentPrefRemoved: function FullZoom_onContentPrefRemoved(aGroup, aName) {
   157     this._onContentPrefChanged(aGroup, undefined);
   158   },
   160   /**
   161    * Appropriately updates the zoom level after a content preference has
   162    * changed.
   163    *
   164    * @param aGroup  The group of the changed preference.
   165    * @param aValue  The new value of the changed preference.  Pass undefined to
   166    *                indicate the preference's removal.
   167    */
   168   _onContentPrefChanged: function FullZoom__onContentPrefChanged(aGroup, aValue) {
   169     if (this._isNextContentPrefChangeInternal) {
   170       // Ignore changes that FullZoom itself makes.  This works because the
   171       // content pref service calls callbacks before notifying observers, and it
   172       // does both in the same turn of the event loop.
   173       delete this._isNextContentPrefChangeInternal;
   174       return;
   175     }
   177     let browser = gBrowser.selectedBrowser;
   178     if (!browser.currentURI)
   179       return;
   181     let domain = this._cps2.extractDomain(browser.currentURI.spec);
   182     if (aGroup) {
   183       if (aGroup == domain)
   184         this._applyPrefToZoom(aValue, browser);
   185       return;
   186     }
   188     this._globalValue = aValue === undefined ? aValue :
   189                           this._ensureValid(aValue);
   191     // If the current page doesn't have a site-specific preference, then its
   192     // zoom should be set to the new global preference now that the global
   193     // preference has changed.
   194     let hasPref = false;
   195     let ctxt = this._loadContextFromBrowser(browser);
   196     let token = this._getBrowserToken(browser);
   197     this._cps2.getByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
   198       handleResult: function () hasPref = true,
   199       handleCompletion: function () {
   200         if (!hasPref && token.isCurrent)
   201           this._applyPrefToZoom(undefined, browser);
   202       }.bind(this)
   203     });
   204   },
   206   // location change observer
   208   /**
   209    * Called when the location of a tab changes.
   210    * When that happens, we need to update the current zoom level if appropriate.
   211    *
   212    * @param aURI
   213    *        A URI object representing the new location.
   214    * @param aIsTabSwitch
   215    *        Whether this location change has happened because of a tab switch.
   216    * @param aBrowser
   217    *        (optional) browser object displaying the document
   218    */
   219   onLocationChange: function FullZoom_onLocationChange(aURI, aIsTabSwitch, aBrowser) {
   220     // Ignore all pending async zoom accesses in the browser.  Pending accesses
   221     // that started before the location change will be prevented from applying
   222     // to the new location.
   223     let browser = aBrowser || gBrowser.selectedBrowser;
   224     this._ignorePendingZoomAccesses(browser);
   226     if (!aURI || (aIsTabSwitch && !this.siteSpecific)) {
   227       this._notifyOnLocationChange();
   228       return;
   229     }
   231     // Avoid the cps roundtrip and apply the default/global pref.
   232     if (aURI.spec == "about:blank") {
   233       this._applyPrefToZoom(undefined, browser,
   234                             this._notifyOnLocationChange.bind(this));
   235       return;
   236     }
   238     // Media documents should always start at 1, and are not affected by prefs.
   239     if (!aIsTabSwitch && browser.isSyntheticDocument) {
   240       ZoomManager.setZoomForBrowser(browser, 1);
   241       // _ignorePendingZoomAccesses already called above, so no need here.
   242       this._notifyOnLocationChange();
   243       return;
   244     }
   246     // See if the zoom pref is cached.
   247     let ctxt = this._loadContextFromBrowser(browser);
   248     let pref = this._cps2.getCachedByDomainAndName(aURI.spec, this.name, ctxt);
   249     if (pref) {
   250       this._applyPrefToZoom(pref.value, browser,
   251                             this._notifyOnLocationChange.bind(this));
   252       return;
   253     }
   255     // It's not cached, so we have to asynchronously fetch it.
   256     let value = undefined;
   257     let token = this._getBrowserToken(browser);
   258     this._cps2.getByDomainAndName(aURI.spec, this.name, ctxt, {
   259       handleResult: function (resultPref) value = resultPref.value,
   260       handleCompletion: function () {
   261         if (!token.isCurrent) {
   262           this._notifyOnLocationChange();
   263           return;
   264         }
   265         this._applyPrefToZoom(value, browser,
   266                               this._notifyOnLocationChange.bind(this));
   267       }.bind(this)
   268     });
   269   },
   271   // update state of zoom type menu item
   273   updateMenu: function FullZoom_updateMenu() {
   274     var menuItem = document.getElementById("toggle_zoom");
   276     menuItem.setAttribute("checked", !ZoomManager.useFullZoom);
   277   },
   279   //**************************************************************************//
   280   // Setting & Pref Manipulation
   282   /**
   283    * Reduces the zoom level of the page in the current browser.
   284    */
   285   reduce: function FullZoom_reduce() {
   286     ZoomManager.reduce();
   287     let browser = gBrowser.selectedBrowser;
   288     this._ignorePendingZoomAccesses(browser);
   289     this._applyZoomToPref(browser);
   290   },
   292   /**
   293    * Enlarges the zoom level of the page in the current browser.
   294    */
   295   enlarge: function FullZoom_enlarge() {
   296     ZoomManager.enlarge();
   297     let browser = gBrowser.selectedBrowser;
   298     this._ignorePendingZoomAccesses(browser);
   299     this._applyZoomToPref(browser);
   300   },
   302   /**
   303    * Sets the zoom level of the page in the current browser to the global zoom
   304    * level.
   305    */
   306   reset: function FullZoom_reset() {
   307     let browser = gBrowser.selectedBrowser;
   308     let token = this._getBrowserToken(browser);
   309     this._getGlobalValue(browser, function (value) {
   310       if (token.isCurrent) {
   311         ZoomManager.setZoomForBrowser(browser, value === undefined ? 1 : value);
   312         this._ignorePendingZoomAccesses(browser);
   313         this._executeSoon(function () {
   314           // _getGlobalValue may be either sync or async, so notify asyncly so
   315           // observers are guaranteed consistent behavior.
   316           Services.obs.notifyObservers(null, "browser-fullZoom:zoomReset", "");
   317         });
   318       }
   319     });
   320     this._removePref(browser);
   321   },
   323   /**
   324    * Set the zoom level for a given browser.
   325    *
   326    * Per nsPresContext::setFullZoom, we can set the zoom to its current value
   327    * without significant impact on performance, as the setting is only applied
   328    * if it differs from the current setting.  In fact getting the zoom and then
   329    * checking ourselves if it differs costs more.
   330    *
   331    * And perhaps we should always set the zoom even if it was more expensive,
   332    * since nsDocumentViewer::SetTextZoom claims that child documents can have
   333    * a different text zoom (although it would be unusual), and it implies that
   334    * those child text zooms should get updated when the parent zoom gets set,
   335    * and perhaps the same is true for full zoom
   336    * (although nsDocumentViewer::SetFullZoom doesn't mention it).
   337    *
   338    * So when we apply new zoom values to the browser, we simply set the zoom.
   339    * We don't check first to see if the new value is the same as the current
   340    * one.
   341    *
   342    * @param aValue     The zoom level value.
   343    * @param aBrowser   The zoom is set in this browser.  Required.
   344    * @param aCallback  If given, it's asynchronously called when complete.
   345    */
   346   _applyPrefToZoom: function FullZoom__applyPrefToZoom(aValue, aBrowser, aCallback) {
   347     if (!this.siteSpecific || gInPrintPreviewMode) {
   348       this._executeSoon(aCallback);
   349       return;
   350     }
   352     // The browser is sometimes half-destroyed because this method is called
   353     // by content pref service callbacks, which themselves can be called at any
   354     // time, even after browsers are closed.
   355     if (!aBrowser.parentNode || aBrowser.isSyntheticDocument) {
   356       this._executeSoon(aCallback);
   357       return;
   358     }
   360     if (aValue !== undefined) {
   361       ZoomManager.setZoomForBrowser(aBrowser, this._ensureValid(aValue));
   362       this._ignorePendingZoomAccesses(aBrowser);
   363       this._executeSoon(aCallback);
   364       return;
   365     }
   367     let token = this._getBrowserToken(aBrowser);
   368     this._getGlobalValue(aBrowser, function (value) {
   369       if (token.isCurrent) {
   370         ZoomManager.setZoomForBrowser(aBrowser, value === undefined ? 1 : value);
   371         this._ignorePendingZoomAccesses(aBrowser);
   372       }
   373       this._executeSoon(aCallback);
   374     });
   375   },
   377   /**
   378    * Saves the zoom level of the page in the given browser to the content
   379    * prefs store.
   380    *
   381    * @param browser  The zoom of this browser will be saved.  Required.
   382    */
   383   _applyZoomToPref: function FullZoom__applyZoomToPref(browser) {
   384     Services.obs.notifyObservers(null, "browser-fullZoom:zoomChange", "");
   385     if (!this.siteSpecific ||
   386         gInPrintPreviewMode ||
   387         browser.isSyntheticDocument)
   388       return;
   390     this._cps2.set(browser.currentURI.spec, this.name,
   391                    ZoomManager.getZoomForBrowser(browser),
   392                    this._loadContextFromBrowser(browser), {
   393       handleCompletion: function () {
   394         this._isNextContentPrefChangeInternal = true;
   395       }.bind(this),
   396     });
   397   },
   399   /**
   400    * Removes from the content prefs store the zoom level of the given browser.
   401    *
   402    * @param browser  The zoom of this browser will be removed.  Required.
   403    */
   404   _removePref: function FullZoom__removePref(browser) {
   405     Services.obs.notifyObservers(null, "browser-fullZoom:zoomReset", "");
   406     if (browser.isSyntheticDocument)
   407       return;
   408     let ctxt = this._loadContextFromBrowser(browser);
   409     this._cps2.removeByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
   410       handleCompletion: function () {
   411         this._isNextContentPrefChangeInternal = true;
   412       }.bind(this),
   413     });
   414   },
   416   //**************************************************************************//
   417   // Utilities
   419   /**
   420    * Returns the zoom change token of the given browser.  Asynchronous
   421    * operations that access the given browser's zoom should use this method to
   422    * capture the token before starting and use token.isCurrent to determine if
   423    * it's safe to access the zoom when done.  If token.isCurrent is false, then
   424    * after the async operation started, either the browser's zoom was changed or
   425    * the browser was destroyed, and depending on what the operation is doing, it
   426    * may no longer be safe to set and get its zoom.
   427    *
   428    * @param browser  The token of this browser will be returned.
   429    * @return  An object with an "isCurrent" getter.
   430    */
   431   _getBrowserToken: function FullZoom__getBrowserToken(browser) {
   432     let map = this._browserTokenMap;
   433     if (!map.has(browser))
   434       map.set(browser, 0);
   435     return {
   436       token: map.get(browser),
   437       get isCurrent() {
   438         // At this point, the browser may have been destructed and unbound but
   439         // its outer ID not removed from the map because outer-window-destroyed
   440         // hasn't been received yet.  In that case, the browser is unusable, it
   441         // has no properties, so return false.  Check for this case by getting a
   442         // property, say, docShell.
   443         return map.get(browser) === this.token && browser.parentNode;
   444       },
   445     };
   446   },
   448   /**
   449    * Increments the zoom change token for the given browser so that pending
   450    * async operations know that it may be unsafe to access they zoom when they
   451    * finish.
   452    *
   453    * @param browser  Pending accesses in this browser will be ignored.
   454    */
   455   _ignorePendingZoomAccesses: function FullZoom__ignorePendingZoomAccesses(browser) {
   456     let map = this._browserTokenMap;
   457     map.set(browser, (map.get(browser) || 0) + 1);
   458   },
   460   _ensureValid: function FullZoom__ensureValid(aValue) {
   461     // Note that undefined is a valid value for aValue that indicates a known-
   462     // not-to-exist value.
   463     if (isNaN(aValue))
   464       return 1;
   466     if (aValue < ZoomManager.MIN)
   467       return ZoomManager.MIN;
   469     if (aValue > ZoomManager.MAX)
   470       return ZoomManager.MAX;
   472     return aValue;
   473   },
   475   /**
   476    * Gets the global browser.content.full-zoom content preference.
   477    *
   478    * WARNING: callback may be called synchronously or asynchronously.  The
   479    * reason is that it's usually desirable to avoid turns of the event loop
   480    * where possible, since they can lead to visible, jarring jumps in zoom
   481    * level.  It's not always possible to avoid them, though.  As a convenience,
   482    * then, this method takes a callback and returns nothing.
   483    *
   484    * @param browser   The browser pertaining to the zoom.
   485    * @param callback  Synchronously or asynchronously called when done.  It's
   486    *                  bound to this object (FullZoom) and called as:
   487    *                    callback(prefValue)
   488    */
   489   _getGlobalValue: function FullZoom__getGlobalValue(browser, callback) {
   490     // * !("_globalValue" in this) => global value not yet cached.
   491     // * this._globalValue === undefined => global value known not to exist.
   492     // * Otherwise, this._globalValue is a number, the global value.
   493     if ("_globalValue" in this) {
   494       callback.call(this, this._globalValue, true);
   495       return;
   496     }
   497     let value = undefined;
   498     this._cps2.getGlobal(this.name, this._loadContextFromBrowser(browser), {
   499       handleResult: function (pref) value = pref.value,
   500       handleCompletion: function (reason) {
   501         this._globalValue = this._ensureValid(value);
   502         callback.call(this, this._globalValue);
   503       }.bind(this)
   504     });
   505   },
   507   /**
   508    * Gets the load context from the given Browser.
   509    *
   510    * @param Browser  The Browser whose load context will be returned.
   511    * @return        The nsILoadContext of the given Browser.
   512    */
   513   _loadContextFromBrowser: function FullZoom__loadContextFromBrowser(browser) {
   514     return browser.loadContext;
   515   },
   517   /**
   518    * Asynchronously broadcasts "browser-fullZoom:location-change" so that
   519    * listeners can be notified when the zoom levels on those pages change.
   520    * The notification is always asynchronous so that observers are guaranteed a
   521    * consistent behavior.
   522    */
   523   _notifyOnLocationChange: function FullZoom__notifyOnLocationChange() {
   524     this._executeSoon(function () {
   525       Services.obs.notifyObservers(null, "browser-fullZoom:location-change", "");
   526     });
   527   },
   529   _executeSoon: function FullZoom__executeSoon(callback) {
   530     if (!callback)
   531       return;
   532     Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
   533   },
   534 };

mercurial