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