Wed, 31 Dec 2014 13:27:57 +0100
Ignore runtime configuration files generated during quality assurance.
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 };