1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/base/content/browser-fullZoom.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,534 @@ 1.4 +/* 1.5 +#ifdef 0 1.6 + * This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. 1.9 +#endif 1.10 + */ 1.11 + 1.12 +/** 1.13 + * Controls the "full zoom" setting and its site-specific preferences. 1.14 + */ 1.15 +var FullZoom = { 1.16 + // Identifies the setting in the content prefs database. 1.17 + name: "browser.content.full-zoom", 1.18 + 1.19 + // browser.zoom.siteSpecific preference cache 1.20 + _siteSpecificPref: undefined, 1.21 + 1.22 + // browser.zoom.updateBackgroundTabs preference cache 1.23 + updateBackgroundTabs: undefined, 1.24 + 1.25 + // One of the possible values for the mousewheel.* preferences. 1.26 + // From EventStateManager.h. 1.27 + ACTION_ZOOM: 3, 1.28 + 1.29 + // This maps the browser to monotonically increasing integer 1.30 + // tokens. _browserTokenMap[browser] is increased each time the zoom is 1.31 + // changed in the browser. See _getBrowserToken and _ignorePendingZoomAccesses. 1.32 + _browserTokenMap: new WeakMap(), 1.33 + 1.34 + get siteSpecific() { 1.35 + return this._siteSpecificPref; 1.36 + }, 1.37 + 1.38 + //**************************************************************************// 1.39 + // nsISupports 1.40 + 1.41 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMEventListener, 1.42 + Ci.nsIObserver, 1.43 + Ci.nsIContentPrefObserver, 1.44 + Ci.nsISupportsWeakReference, 1.45 + Ci.nsISupports]), 1.46 + 1.47 + //**************************************************************************// 1.48 + // Initialization & Destruction 1.49 + 1.50 + init: function FullZoom_init() { 1.51 + // Listen for scrollwheel events so we can save scrollwheel-based changes. 1.52 + window.addEventListener("DOMMouseScroll", this, false); 1.53 + 1.54 + // Register ourselves with the service so we know when our pref changes. 1.55 + this._cps2 = Cc["@mozilla.org/content-pref/service;1"]. 1.56 + getService(Ci.nsIContentPrefService2); 1.57 + this._cps2.addObserverForName(this.name, this); 1.58 + 1.59 + this._siteSpecificPref = 1.60 + gPrefService.getBoolPref("browser.zoom.siteSpecific"); 1.61 + this.updateBackgroundTabs = 1.62 + gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs"); 1.63 + // Listen for changes to the browser.zoom branch so we can enable/disable 1.64 + // updating background tabs and per-site saving and restoring of zoom levels. 1.65 + gPrefService.addObserver("browser.zoom.", this, true); 1.66 + }, 1.67 + 1.68 + destroy: function FullZoom_destroy() { 1.69 + gPrefService.removeObserver("browser.zoom.", this); 1.70 + this._cps2.removeObserverForName(this.name, this); 1.71 + window.removeEventListener("DOMMouseScroll", this, false); 1.72 + }, 1.73 + 1.74 + 1.75 + //**************************************************************************// 1.76 + // Event Handlers 1.77 + 1.78 + // nsIDOMEventListener 1.79 + 1.80 + handleEvent: function FullZoom_handleEvent(event) { 1.81 + switch (event.type) { 1.82 + case "DOMMouseScroll": 1.83 + this._handleMouseScrolled(event); 1.84 + break; 1.85 + } 1.86 + }, 1.87 + 1.88 + _handleMouseScrolled: function FullZoom__handleMouseScrolled(event) { 1.89 + // Construct the "mousewheel action" pref key corresponding to this event. 1.90 + // Based on EventStateManager::WheelPrefs::GetBasePrefName(). 1.91 + var pref = "mousewheel."; 1.92 + 1.93 + var pressedModifierCount = event.shiftKey + event.ctrlKey + event.altKey + 1.94 + event.metaKey + event.getModifierState("OS"); 1.95 + if (pressedModifierCount != 1) { 1.96 + pref += "default."; 1.97 + } else if (event.shiftKey) { 1.98 + pref += "with_shift."; 1.99 + } else if (event.ctrlKey) { 1.100 + pref += "with_control."; 1.101 + } else if (event.altKey) { 1.102 + pref += "with_alt."; 1.103 + } else if (event.metaKey) { 1.104 + pref += "with_meta."; 1.105 + } else { 1.106 + pref += "with_win."; 1.107 + } 1.108 + 1.109 + pref += "action"; 1.110 + 1.111 + // Don't do anything if this isn't a "zoom" scroll event. 1.112 + var isZoomEvent = false; 1.113 + try { 1.114 + isZoomEvent = (gPrefService.getIntPref(pref) == this.ACTION_ZOOM); 1.115 + } catch (e) {} 1.116 + if (!isZoomEvent) 1.117 + return; 1.118 + 1.119 + // XXX Lazily cache all the possible action prefs so we don't have to get 1.120 + // them anew from the pref service for every scroll event? We'd have to 1.121 + // make sure to observe them so we can update the cache when they change. 1.122 + 1.123 + // We have to call _applyZoomToPref in a timeout because we handle the 1.124 + // event before the event state manager has a chance to apply the zoom 1.125 + // during EventStateManager::PostHandleEvent. 1.126 + let browser = gBrowser.selectedBrowser; 1.127 + let token = this._getBrowserToken(browser); 1.128 + window.setTimeout(function () { 1.129 + if (token.isCurrent) 1.130 + this._applyZoomToPref(browser); 1.131 + }.bind(this), 0); 1.132 + }, 1.133 + 1.134 + // nsIObserver 1.135 + 1.136 + observe: function (aSubject, aTopic, aData) { 1.137 + switch (aTopic) { 1.138 + case "nsPref:changed": 1.139 + switch (aData) { 1.140 + case "browser.zoom.siteSpecific": 1.141 + this._siteSpecificPref = 1.142 + gPrefService.getBoolPref("browser.zoom.siteSpecific"); 1.143 + break; 1.144 + case "browser.zoom.updateBackgroundTabs": 1.145 + this.updateBackgroundTabs = 1.146 + gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs"); 1.147 + break; 1.148 + } 1.149 + break; 1.150 + } 1.151 + }, 1.152 + 1.153 + // nsIContentPrefObserver 1.154 + 1.155 + onContentPrefSet: function FullZoom_onContentPrefSet(aGroup, aName, aValue) { 1.156 + this._onContentPrefChanged(aGroup, aValue); 1.157 + }, 1.158 + 1.159 + onContentPrefRemoved: function FullZoom_onContentPrefRemoved(aGroup, aName) { 1.160 + this._onContentPrefChanged(aGroup, undefined); 1.161 + }, 1.162 + 1.163 + /** 1.164 + * Appropriately updates the zoom level after a content preference has 1.165 + * changed. 1.166 + * 1.167 + * @param aGroup The group of the changed preference. 1.168 + * @param aValue The new value of the changed preference. Pass undefined to 1.169 + * indicate the preference's removal. 1.170 + */ 1.171 + _onContentPrefChanged: function FullZoom__onContentPrefChanged(aGroup, aValue) { 1.172 + if (this._isNextContentPrefChangeInternal) { 1.173 + // Ignore changes that FullZoom itself makes. This works because the 1.174 + // content pref service calls callbacks before notifying observers, and it 1.175 + // does both in the same turn of the event loop. 1.176 + delete this._isNextContentPrefChangeInternal; 1.177 + return; 1.178 + } 1.179 + 1.180 + let browser = gBrowser.selectedBrowser; 1.181 + if (!browser.currentURI) 1.182 + return; 1.183 + 1.184 + let domain = this._cps2.extractDomain(browser.currentURI.spec); 1.185 + if (aGroup) { 1.186 + if (aGroup == domain) 1.187 + this._applyPrefToZoom(aValue, browser); 1.188 + return; 1.189 + } 1.190 + 1.191 + this._globalValue = aValue === undefined ? aValue : 1.192 + this._ensureValid(aValue); 1.193 + 1.194 + // If the current page doesn't have a site-specific preference, then its 1.195 + // zoom should be set to the new global preference now that the global 1.196 + // preference has changed. 1.197 + let hasPref = false; 1.198 + let ctxt = this._loadContextFromBrowser(browser); 1.199 + let token = this._getBrowserToken(browser); 1.200 + this._cps2.getByDomainAndName(browser.currentURI.spec, this.name, ctxt, { 1.201 + handleResult: function () hasPref = true, 1.202 + handleCompletion: function () { 1.203 + if (!hasPref && token.isCurrent) 1.204 + this._applyPrefToZoom(undefined, browser); 1.205 + }.bind(this) 1.206 + }); 1.207 + }, 1.208 + 1.209 + // location change observer 1.210 + 1.211 + /** 1.212 + * Called when the location of a tab changes. 1.213 + * When that happens, we need to update the current zoom level if appropriate. 1.214 + * 1.215 + * @param aURI 1.216 + * A URI object representing the new location. 1.217 + * @param aIsTabSwitch 1.218 + * Whether this location change has happened because of a tab switch. 1.219 + * @param aBrowser 1.220 + * (optional) browser object displaying the document 1.221 + */ 1.222 + onLocationChange: function FullZoom_onLocationChange(aURI, aIsTabSwitch, aBrowser) { 1.223 + // Ignore all pending async zoom accesses in the browser. Pending accesses 1.224 + // that started before the location change will be prevented from applying 1.225 + // to the new location. 1.226 + let browser = aBrowser || gBrowser.selectedBrowser; 1.227 + this._ignorePendingZoomAccesses(browser); 1.228 + 1.229 + if (!aURI || (aIsTabSwitch && !this.siteSpecific)) { 1.230 + this._notifyOnLocationChange(); 1.231 + return; 1.232 + } 1.233 + 1.234 + // Avoid the cps roundtrip and apply the default/global pref. 1.235 + if (aURI.spec == "about:blank") { 1.236 + this._applyPrefToZoom(undefined, browser, 1.237 + this._notifyOnLocationChange.bind(this)); 1.238 + return; 1.239 + } 1.240 + 1.241 + // Media documents should always start at 1, and are not affected by prefs. 1.242 + if (!aIsTabSwitch && browser.isSyntheticDocument) { 1.243 + ZoomManager.setZoomForBrowser(browser, 1); 1.244 + // _ignorePendingZoomAccesses already called above, so no need here. 1.245 + this._notifyOnLocationChange(); 1.246 + return; 1.247 + } 1.248 + 1.249 + // See if the zoom pref is cached. 1.250 + let ctxt = this._loadContextFromBrowser(browser); 1.251 + let pref = this._cps2.getCachedByDomainAndName(aURI.spec, this.name, ctxt); 1.252 + if (pref) { 1.253 + this._applyPrefToZoom(pref.value, browser, 1.254 + this._notifyOnLocationChange.bind(this)); 1.255 + return; 1.256 + } 1.257 + 1.258 + // It's not cached, so we have to asynchronously fetch it. 1.259 + let value = undefined; 1.260 + let token = this._getBrowserToken(browser); 1.261 + this._cps2.getByDomainAndName(aURI.spec, this.name, ctxt, { 1.262 + handleResult: function (resultPref) value = resultPref.value, 1.263 + handleCompletion: function () { 1.264 + if (!token.isCurrent) { 1.265 + this._notifyOnLocationChange(); 1.266 + return; 1.267 + } 1.268 + this._applyPrefToZoom(value, browser, 1.269 + this._notifyOnLocationChange.bind(this)); 1.270 + }.bind(this) 1.271 + }); 1.272 + }, 1.273 + 1.274 + // update state of zoom type menu item 1.275 + 1.276 + updateMenu: function FullZoom_updateMenu() { 1.277 + var menuItem = document.getElementById("toggle_zoom"); 1.278 + 1.279 + menuItem.setAttribute("checked", !ZoomManager.useFullZoom); 1.280 + }, 1.281 + 1.282 + //**************************************************************************// 1.283 + // Setting & Pref Manipulation 1.284 + 1.285 + /** 1.286 + * Reduces the zoom level of the page in the current browser. 1.287 + */ 1.288 + reduce: function FullZoom_reduce() { 1.289 + ZoomManager.reduce(); 1.290 + let browser = gBrowser.selectedBrowser; 1.291 + this._ignorePendingZoomAccesses(browser); 1.292 + this._applyZoomToPref(browser); 1.293 + }, 1.294 + 1.295 + /** 1.296 + * Enlarges the zoom level of the page in the current browser. 1.297 + */ 1.298 + enlarge: function FullZoom_enlarge() { 1.299 + ZoomManager.enlarge(); 1.300 + let browser = gBrowser.selectedBrowser; 1.301 + this._ignorePendingZoomAccesses(browser); 1.302 + this._applyZoomToPref(browser); 1.303 + }, 1.304 + 1.305 + /** 1.306 + * Sets the zoom level of the page in the current browser to the global zoom 1.307 + * level. 1.308 + */ 1.309 + reset: function FullZoom_reset() { 1.310 + let browser = gBrowser.selectedBrowser; 1.311 + let token = this._getBrowserToken(browser); 1.312 + this._getGlobalValue(browser, function (value) { 1.313 + if (token.isCurrent) { 1.314 + ZoomManager.setZoomForBrowser(browser, value === undefined ? 1 : value); 1.315 + this._ignorePendingZoomAccesses(browser); 1.316 + this._executeSoon(function () { 1.317 + // _getGlobalValue may be either sync or async, so notify asyncly so 1.318 + // observers are guaranteed consistent behavior. 1.319 + Services.obs.notifyObservers(null, "browser-fullZoom:zoomReset", ""); 1.320 + }); 1.321 + } 1.322 + }); 1.323 + this._removePref(browser); 1.324 + }, 1.325 + 1.326 + /** 1.327 + * Set the zoom level for a given browser. 1.328 + * 1.329 + * Per nsPresContext::setFullZoom, we can set the zoom to its current value 1.330 + * without significant impact on performance, as the setting is only applied 1.331 + * if it differs from the current setting. In fact getting the zoom and then 1.332 + * checking ourselves if it differs costs more. 1.333 + * 1.334 + * And perhaps we should always set the zoom even if it was more expensive, 1.335 + * since nsDocumentViewer::SetTextZoom claims that child documents can have 1.336 + * a different text zoom (although it would be unusual), and it implies that 1.337 + * those child text zooms should get updated when the parent zoom gets set, 1.338 + * and perhaps the same is true for full zoom 1.339 + * (although nsDocumentViewer::SetFullZoom doesn't mention it). 1.340 + * 1.341 + * So when we apply new zoom values to the browser, we simply set the zoom. 1.342 + * We don't check first to see if the new value is the same as the current 1.343 + * one. 1.344 + * 1.345 + * @param aValue The zoom level value. 1.346 + * @param aBrowser The zoom is set in this browser. Required. 1.347 + * @param aCallback If given, it's asynchronously called when complete. 1.348 + */ 1.349 + _applyPrefToZoom: function FullZoom__applyPrefToZoom(aValue, aBrowser, aCallback) { 1.350 + if (!this.siteSpecific || gInPrintPreviewMode) { 1.351 + this._executeSoon(aCallback); 1.352 + return; 1.353 + } 1.354 + 1.355 + // The browser is sometimes half-destroyed because this method is called 1.356 + // by content pref service callbacks, which themselves can be called at any 1.357 + // time, even after browsers are closed. 1.358 + if (!aBrowser.parentNode || aBrowser.isSyntheticDocument) { 1.359 + this._executeSoon(aCallback); 1.360 + return; 1.361 + } 1.362 + 1.363 + if (aValue !== undefined) { 1.364 + ZoomManager.setZoomForBrowser(aBrowser, this._ensureValid(aValue)); 1.365 + this._ignorePendingZoomAccesses(aBrowser); 1.366 + this._executeSoon(aCallback); 1.367 + return; 1.368 + } 1.369 + 1.370 + let token = this._getBrowserToken(aBrowser); 1.371 + this._getGlobalValue(aBrowser, function (value) { 1.372 + if (token.isCurrent) { 1.373 + ZoomManager.setZoomForBrowser(aBrowser, value === undefined ? 1 : value); 1.374 + this._ignorePendingZoomAccesses(aBrowser); 1.375 + } 1.376 + this._executeSoon(aCallback); 1.377 + }); 1.378 + }, 1.379 + 1.380 + /** 1.381 + * Saves the zoom level of the page in the given browser to the content 1.382 + * prefs store. 1.383 + * 1.384 + * @param browser The zoom of this browser will be saved. Required. 1.385 + */ 1.386 + _applyZoomToPref: function FullZoom__applyZoomToPref(browser) { 1.387 + Services.obs.notifyObservers(null, "browser-fullZoom:zoomChange", ""); 1.388 + if (!this.siteSpecific || 1.389 + gInPrintPreviewMode || 1.390 + browser.isSyntheticDocument) 1.391 + return; 1.392 + 1.393 + this._cps2.set(browser.currentURI.spec, this.name, 1.394 + ZoomManager.getZoomForBrowser(browser), 1.395 + this._loadContextFromBrowser(browser), { 1.396 + handleCompletion: function () { 1.397 + this._isNextContentPrefChangeInternal = true; 1.398 + }.bind(this), 1.399 + }); 1.400 + }, 1.401 + 1.402 + /** 1.403 + * Removes from the content prefs store the zoom level of the given browser. 1.404 + * 1.405 + * @param browser The zoom of this browser will be removed. Required. 1.406 + */ 1.407 + _removePref: function FullZoom__removePref(browser) { 1.408 + Services.obs.notifyObservers(null, "browser-fullZoom:zoomReset", ""); 1.409 + if (browser.isSyntheticDocument) 1.410 + return; 1.411 + let ctxt = this._loadContextFromBrowser(browser); 1.412 + this._cps2.removeByDomainAndName(browser.currentURI.spec, this.name, ctxt, { 1.413 + handleCompletion: function () { 1.414 + this._isNextContentPrefChangeInternal = true; 1.415 + }.bind(this), 1.416 + }); 1.417 + }, 1.418 + 1.419 + //**************************************************************************// 1.420 + // Utilities 1.421 + 1.422 + /** 1.423 + * Returns the zoom change token of the given browser. Asynchronous 1.424 + * operations that access the given browser's zoom should use this method to 1.425 + * capture the token before starting and use token.isCurrent to determine if 1.426 + * it's safe to access the zoom when done. If token.isCurrent is false, then 1.427 + * after the async operation started, either the browser's zoom was changed or 1.428 + * the browser was destroyed, and depending on what the operation is doing, it 1.429 + * may no longer be safe to set and get its zoom. 1.430 + * 1.431 + * @param browser The token of this browser will be returned. 1.432 + * @return An object with an "isCurrent" getter. 1.433 + */ 1.434 + _getBrowserToken: function FullZoom__getBrowserToken(browser) { 1.435 + let map = this._browserTokenMap; 1.436 + if (!map.has(browser)) 1.437 + map.set(browser, 0); 1.438 + return { 1.439 + token: map.get(browser), 1.440 + get isCurrent() { 1.441 + // At this point, the browser may have been destructed and unbound but 1.442 + // its outer ID not removed from the map because outer-window-destroyed 1.443 + // hasn't been received yet. In that case, the browser is unusable, it 1.444 + // has no properties, so return false. Check for this case by getting a 1.445 + // property, say, docShell. 1.446 + return map.get(browser) === this.token && browser.parentNode; 1.447 + }, 1.448 + }; 1.449 + }, 1.450 + 1.451 + /** 1.452 + * Increments the zoom change token for the given browser so that pending 1.453 + * async operations know that it may be unsafe to access they zoom when they 1.454 + * finish. 1.455 + * 1.456 + * @param browser Pending accesses in this browser will be ignored. 1.457 + */ 1.458 + _ignorePendingZoomAccesses: function FullZoom__ignorePendingZoomAccesses(browser) { 1.459 + let map = this._browserTokenMap; 1.460 + map.set(browser, (map.get(browser) || 0) + 1); 1.461 + }, 1.462 + 1.463 + _ensureValid: function FullZoom__ensureValid(aValue) { 1.464 + // Note that undefined is a valid value for aValue that indicates a known- 1.465 + // not-to-exist value. 1.466 + if (isNaN(aValue)) 1.467 + return 1; 1.468 + 1.469 + if (aValue < ZoomManager.MIN) 1.470 + return ZoomManager.MIN; 1.471 + 1.472 + if (aValue > ZoomManager.MAX) 1.473 + return ZoomManager.MAX; 1.474 + 1.475 + return aValue; 1.476 + }, 1.477 + 1.478 + /** 1.479 + * Gets the global browser.content.full-zoom content preference. 1.480 + * 1.481 + * WARNING: callback may be called synchronously or asynchronously. The 1.482 + * reason is that it's usually desirable to avoid turns of the event loop 1.483 + * where possible, since they can lead to visible, jarring jumps in zoom 1.484 + * level. It's not always possible to avoid them, though. As a convenience, 1.485 + * then, this method takes a callback and returns nothing. 1.486 + * 1.487 + * @param browser The browser pertaining to the zoom. 1.488 + * @param callback Synchronously or asynchronously called when done. It's 1.489 + * bound to this object (FullZoom) and called as: 1.490 + * callback(prefValue) 1.491 + */ 1.492 + _getGlobalValue: function FullZoom__getGlobalValue(browser, callback) { 1.493 + // * !("_globalValue" in this) => global value not yet cached. 1.494 + // * this._globalValue === undefined => global value known not to exist. 1.495 + // * Otherwise, this._globalValue is a number, the global value. 1.496 + if ("_globalValue" in this) { 1.497 + callback.call(this, this._globalValue, true); 1.498 + return; 1.499 + } 1.500 + let value = undefined; 1.501 + this._cps2.getGlobal(this.name, this._loadContextFromBrowser(browser), { 1.502 + handleResult: function (pref) value = pref.value, 1.503 + handleCompletion: function (reason) { 1.504 + this._globalValue = this._ensureValid(value); 1.505 + callback.call(this, this._globalValue); 1.506 + }.bind(this) 1.507 + }); 1.508 + }, 1.509 + 1.510 + /** 1.511 + * Gets the load context from the given Browser. 1.512 + * 1.513 + * @param Browser The Browser whose load context will be returned. 1.514 + * @return The nsILoadContext of the given Browser. 1.515 + */ 1.516 + _loadContextFromBrowser: function FullZoom__loadContextFromBrowser(browser) { 1.517 + return browser.loadContext; 1.518 + }, 1.519 + 1.520 + /** 1.521 + * Asynchronously broadcasts "browser-fullZoom:location-change" so that 1.522 + * listeners can be notified when the zoom levels on those pages change. 1.523 + * The notification is always asynchronous so that observers are guaranteed a 1.524 + * consistent behavior. 1.525 + */ 1.526 + _notifyOnLocationChange: function FullZoom__notifyOnLocationChange() { 1.527 + this._executeSoon(function () { 1.528 + Services.obs.notifyObservers(null, "browser-fullZoom:location-change", ""); 1.529 + }); 1.530 + }, 1.531 + 1.532 + _executeSoon: function FullZoom__executeSoon(callback) { 1.533 + if (!callback) 1.534 + return; 1.535 + Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); 1.536 + }, 1.537 +};